Skip to content

Firefox data_collection_permissions: what your extension has to declare

7 min readModerok team

How to fill in browser_specific_settings.gecko.data_collection_permissions for a Firefox extension, and where analytics belongs in it.

If addons.mozilla.org refuses your submission until you declare data collection, the key it wants is browser_specific_settings.gecko.data_collection_permissions, and it has two lists: required and optional. An extension that collects and transmits no personal data declares required: ["none"] and is done. An extension that sends anonymous usage metrics keeps that same required: ["none"] and adds technicalAndInteraction to optional, which is the only list that value is allowed to appear in. Everything else you might collect, from search terms to page content, goes in one of the two lists under its own category name.

The shape of the key

data_collection_permissions lives inside the Gecko block, next to the add-on id:

{
  "manifest_version": 3,
  "name": "My extension",
  "version": "1.0.0",
  "permissions": ["storage"],
  "browser_specific_settings": {
    "gecko": {
      "id": "my-extension@example.com",
      "data_collection_permissions": {
        "required": ["none"]
      }
    }
  }
}

required is the data your extension has to collect and transmit in order to work. It contains either the single value none or one or more of authenticationInfo, bookmarksInfo, browsingActivity, financialAndPaymentInfo, healthInfo, locationInfo, personalCommunications, personallyIdentifyingInfo, searchTerms, websiteActivity, and websiteContent.

optional is data the user can turn on or off without losing the extension. It accepts the same categories plus one extra value, technicalAndInteraction, and does not accept none.

The split matters at install time. Anything in required is a condition of installing. Anything in optional is a choice the user can revisit later in about:addons, under Permissions and data.

What the none exclusivity rule actually covers

none is not a filler value for "nothing else", and the rule around it is easy to over-read: it is scoped to the required list, not to the manifest as a whole. An extension whose only collection is optional still declares required: ["none"], which is how it states that nothing at all is collected without consent, and lists the optional categories beside it.

In Gecko, the check attached to the required list is checkValidRequiredDataCollection, and it fires only when that list has more than one entry:

if (value.length > 1 && value.includes("none")) {
  // strips "none", logs a developer warning
}

In addons-linter the error is NONE_DATA_COLLECTION_IS_EXCLUSIVE, whose text is scoped the same way: "none" must not be specified with other required data collection permissions. The linter's own test suite asserts that required: ["none"] alongside optional: ["technicalAndInteraction", "locationInfo"] validates clean.

You also cannot dodge the question by leaving required out. The addons-linter schema marks it mandatory with a minimum of one item, so an object carrying only an optional list is rejected outright. For an extension whose only transmission is optional telemetry, that gives you:

"data_collection_permissions": {
  "required": ["none"],
  "optional": ["technicalAndInteraction"]
}

What the rule forbids is required: ["none", "websiteActivity"]. There Gecko drops none, warns, and enforces the real category. If you require data, name it.

Where analytics belongs: technicalAndInteraction

technicalAndInteraction is the category for what an extension analytics SDK does, which Mozilla describes as covering things like metrics on feature usage. It differs from every other value in two ways:

  1. It can only be optional. You cannot make usage telemetry a condition of installation.
  2. It is surfaced during installation, as a control in the optional section of the install panel, enabled by default so the user can opt out. Other optional permissions are neither shown at install nor granted by default, and you request those later, when you need them.

Do not under-declare. technicalAndInteraction covers feature usage and technical detail about the extension itself. If your telemetry also ships the URLs a user visits, the text of a page, or what they typed into a search box, that is websiteActivity, websiteContent, or searchTerms, and hashing does not turn it back into a technical metric. The discipline that keeps a Chrome Web Store data collection disclosure to one box keeps this key short.

Checking the grant before you send anything

Because technicalAndInteraction is optional, the user may have switched it off. The permissions API takes data collection permissions in a data_collection array, alongside the familiar permissions and origins. Do not call it unguarded: Mozilla's documented feature detection is to look for the data_collection key in browser.permissions.getAll(), and if it is absent the browser is too old to be managing consent, so you fall back to your own stored setting.

async function telemetryAllowed() {
  try {
    const all = await browser.permissions.getAll();
    if (!("data_collection" in all)) {
      const { telemetryOptIn } = await browser.storage.local.get("telemetryOptIn");
      return telemetryOptIn === true;
    }
    return all.data_collection.includes("technicalAndInteraction");
  } catch {
    return false;
  }
}

For values not surfaced at install, prompt with browser.permissions.request({ data_collection: ["locationInfo"] }) from a user action, such as a click in your options page.

Older Firefox is your problem, not Firefox's

The built-in consent experience applies on Firefox 140 and later on desktop, and Firefox 142 and later on Android. If your extension collects data and lands on an older build, Firefox is not asking anyone on your behalf, and Extension Workshop gives three ways out: set strict_min_version so those versions cannot install it, turn the data collection off there, or show a consent experience of your own. For a new extension the first two are the cheap options, and the stored flag in the fallback branch above is the second one.

If you go the version route, write it as a dotted string: "140.0" under gecko and "142.0" under gecko_android. A bare "140" fails the linter's version pattern.

Gating an analytics SDK on the answer

Check first, initialize second. With Moderok that is a few lines in your background script:

import { Moderok } from "@moderok/sdk";

browser.runtime.onInstalled.addListener(({ reason, previousVersion }) => {
  // Only these two. A browser_update firing is not an install.
  if (reason !== "install" && reason !== "update") return;
  void browser.storage.local.set({ pendingLifecycle: { reason, previousVersion } });
});

async function start() {
  if (!(await telemetryAllowed())) return;
  Moderok.init({ appKey: "mk_your_app_key" });

  const { pendingLifecycle } = await browser.storage.local.get("pendingLifecycle");
  if (pendingLifecycle) {
    Moderok.track(`extension_${pendingLifecycle.reason}`, pendingLifecycle);
    await browser.storage.local.remove("pendingLifecycle");
  }
}

void start();

Two details make that ordering load bearing. First, Moderok.track() called before init() triggers an auto-init from any config already saved in chrome.storage.local, so a stray track() on the denied path would quietly start the client again on a profile that had consented before. Keep every track() call inside the branch that passed the check.

Second, Moderok.init() is synchronous and registers its chrome.runtime.onInstalled listener before anything else, which is how the SDK normally captures __install and __update for you. A permissions check is an asynchronous round trip, so a gated init() runs at least a full event loop turn later, and a fresh install's lifecycle event can fire inside that gap. That race is why the listener above sits at the top level and stashes the reason for replay. Top level registration matters in Firefox anyway: a Firefox MV3 extension runs your background code as an event page rather than a service worker, and top level listeners are the ones that survive it being unloaded and restarted.

Who has to do this, and when

Mozilla announced in October 2025 that from November 3, 2025 all new Firefox extensions must declare data collection with this key, with the stated plan to extend that to every extension during the first half of 2026. In addons-linter today the absent key is a warning, raised on any extension rather than only new ones, and its description still calls the key required for new extensions and required for new versions of existing ones "in the future". Treat that as a countdown, not an exemption.

Check your tooling before you add the key, though. The linter validates data_collection_permissions only when its data collection option is enabled; without it, the same key is rejected outright with "The data_collection_permissions property is reserved." If you get that error, the manifest is fine and your addons-linter or web-ext is too old.

Two rules hold either way. An extension required to use the key and not setting it correctly is blocked from submission to AMO for signing, with a message explaining why. And once any version ships the key, every later version has to keep it.

Chrome has no equivalent. The same information goes into the Privacy practices tab of your Web Store listing, as categories and Limited Use certifications rather than a machine readable declaration in the install prompt.

Keep the declaration small on purpose

Staying at required: ["none"] is a question of what your analytics actually sends. Moderok generates a random anonymous id, keeps it in chrome.storage.local, and mirrors the id and last ping date to chrome.storage.sync so a profile reset does not create a phantom new user. Each event carries coarse context: SDK version, extension id and version, browser and browser version, OS, locale, and which surface the event came from. It reads no page content, sets no cookies, does no device fingerprinting, and asks for no host permissions.

Two things to account for honestly. Automatic error capture is on unless you pass trackErrors: false, and those __error events carry your own code's error name, message, stack, filename, line and column, plus a hash used to group repeats. And the properties you pass to track() are yours to keep clean: put a page URL or an email address in one and you have changed your declaration.

If you are adding analytics to a Firefox add-on and want the manifest side of it to stay short and honest, Moderok is a reasonable place to start.