Install in a Firefox extension
Using a framework?
There are guides for WXT, Plasmo and CRXJS, and a WXT starter template that is already measured.
The SDK is the same one Chrome uses. What differs is the manifest, the background lifecycle, and one uninstall-tracking behaviour that silently breaks if you follow the Chrome guide here. That is why this is a separate page.
1. Install the package
npm install @analyticstrend/extensionBeta versioning
The API is settled enough to build on, but AnalyticsTrend is in beta and breaking changes will land in minor versions until 1.0.0. Pin an exact version if that matters to you.
2. Initialise in the background script
// background.js
import { initBackground } from "@analyticstrend/extension";
const analytics = initBackground({
writeKey: "wk_your_key_here",
uninstallTracking: true,
});
analytics.track("extension_started");Identical to Chrome, and that is deliberate. The SDK resolves Firefox's browser namespace or Chromium's chrome namespace itself, so you do not need a polyfill and you do not write two code paths. Firefox would tolerate a top-level await here, but Chrome will not run a service worker that uses one, so code shared between the two should not.
From @analyticstrend/extension 0.6.0 every batch carries your manifest's version, so Versions works with no more code.
3. Declare the manifest
Firefox supports Manifest V3, but background pages are event pages rather than service workers, and Firefox requires an explicit add-on id for several APIs:
{
"manifest_version": 3,
"background": { "scripts": ["background.js"], "type": "module" },
"permissions": ["storage"],
"browser_specific_settings": {
"gecko": {
"id": "your-extension@example.com",
"strict_min_version": "140.0",
"data_collection_permissions": {
"required": ["none"],
"optional": ["technicalAndInteraction", "locationInfo"]
}
}
}
}Note background.scripts, not background.service_worker. A manifest carrying only the Chrome form will load in Firefox and never run your background code, which presents as "no events at all" with nothing in the console to explain it.
4. Declare what you collect, and respect the answer
Since November 2025, new Firefox extensions must declare data collection in the manifest, and from 2026 Mozilla requires it of every extension. Analytics fall under technicalAndInteraction, which Mozilla's schema accepts only as optional — Firefox shows it as a toggle during installation, and the person can change it afterwards in the add-on's settings.
Firefox does not enforce the answer technically. It records the choice and leaves honouring it to the extension, so an SDK that ignored it would quietly put every extension using it in breach of policy. This one does not: when the permission is declared and not granted, track drops the event rather than queueing it, and nothing is stored or sent. If the person turns it on later, sending resumes without a restart.
None of this affects Chrome, which has no such permission. And it only applies if you declare the permission: an extension that declares nothing has offered no toggle, so there is nothing to honour and the SDK sends as normal.
The collector also works out a country, and in some countries a state or province, from each request's address. Mozilla counts that as locationInfo, so the manifest above declares it too, as a second optional toggle. From 0.4.0 the SDK honours it on its own: with location turned off and analytics on, events are still sent, marked so the collector works no country or region out of them, and the uninstall link carries the same choice. Turning analytics off still stops everything.
To offer a way back after someone declines, request it from a click:
import { getBrowserApi, hasDataConsent, ANALYTICS_DATA_PERMISSION } from "@analyticstrend/extension";
button.addEventListener("click", async () => {
// Firefox refuses a permission request that did not come from a user action.
await getBrowserApi().permissions.request({ data_collection: [ANALYTICS_DATA_PERMISSION] });
});
// Anywhere you want to show whether analytics are on:
const on = await hasDataConsent(getBrowserApi());The plain example does exactly this, in its popup.
The uninstall-tracking difference
This is the one that bites.
Chrome remembers an uninstall URL across service-worker restarts. Firefox does not. Its event page must call setUninstallURL again on every run of the background script, or uninstall tracking stops working the first time the page unloads, which is within about a minute of install.
Passing uninstallTracking: true to initBackground handles it, because initBackground runs on every background start on both browsers. If you instead call enableUninstallTracking yourself, call it unconditionally at the top level:
// Correct on both browsers.
enableUninstallTracking({ writeKey, anonymousId, endpoint });// Works on Chrome forever. Stops working on Firefox within the minute.
browser.runtime.onInstalled.addListener(() => {
enableUninstallTracking({ writeKey, anonymousId, endpoint });
});onInstalled fires once. On Chrome the URL it set persists, so the bug never shows itself. On Firefox the event page unloads, the URL is forgotten, and uninstalls quietly stop being recorded — with a dashboard that looks fine, just wrong.
Before you submit to AMO
Mozilla reviews add-ons by hand, and the reviewer reads your source. The store policy page covers what they look for and what to write in your data-collection disclosure.
Verify it
Load the add-on temporarily from about:debugging, use it, then check the Events screen in your dashboard. Then remove it and confirm an uninstall event arrives, since that is the path this page exists to protect.
One catch if you declare technicalAndInteraction: a temporary add-on never goes through the install flow, so it starts with that permission not granted, and the SDK correctly sends nothing. Request it from a click first, as the example extension's popup does, or you will be debugging a silence that is working as intended.