Skip to content

What Permissions Does Chrome Extension Analytics Need?

6 min readModerok team

The permissions an analytics SDK actually needs in a Manifest V3 extension, which ones add an install warning, and what to check before you pick one.

For anonymous product analytics in a Manifest V3 extension, the honest answer is one permission: storage. It does not produce an install warning, and it is there so the SDK can keep an anonymous id and a small queue of unsent events across service worker restarts. You do not need tabs, you do not need identity, and you only need host_permissions for your analytics endpoint if that endpoint does not return CORS headers. If a tool asks you for more than storage, that is a question to ask before you install it, not after.

TL;DR: "permissions": ["storage"] is the whole manifest change for most extension analytics. host_permissions is required only when the analytics server does not send Access-Control-Allow-Origin, and adding it puts a "read and change your data on <site>" line in the install prompt. Permissions and the Web Store privacy disclosure are separate obligations: minimal permissions do not exempt you from declaring what you collect.

The permission checklist for an analytics SDK

Here is what each permission a Chrome extension analytics library might ask for actually buys you, and what the user sees at install.

Manifest keyWhat the user sees at installDoes analytics need it?
"permissions": ["storage"]Nothing. storage is one of the permissions Chrome does not surface in the install prompt.Yes: An anonymous id and unsent events have to survive service worker termination. One entry covers the local, sync, and session areas, so an SDK that mirrors its id through chrome.storage.sync costs nothing extra in the manifest.
"permissions": ["alarms"]Nothing.Only if the SDK schedules flushes with chrome.alarms instead of a timer.
"host_permissions": ["https://api.example.com/*"]"Read and change your data on api.example.com".Only if the analytics endpoint does not return CORS headers.
"host_permissions": ["<all_urls>"]"Read and change all your data on all websites".Never, for analytics.
"permissions": ["tabs"]A browsing history warning.No: Reading tab URLs is page tracking, not extension analytics.
chrome.runtime.setUninstallURL()Nothing, and no manifest key at all.Optional. It is how uninstall pings work.

The row that matters is host_permissions: a bad default in someone else's SDK costs you an install prompt line you cannot remove without shipping an update.

Why analytics usually does not need host_permissions

A background service worker is not a web page, but its fetch() calls still go through CORS. Chrome gives an extension two ways to satisfy that:

  1. Declare the target origin in host_permissions. Chrome then treats the extension as privileged for that host and the request goes out without needing anything from the server.
  2. Do not declare it, and let the server answer with the CORS headers any web origin would need.

Option 2 is what a hosted analytics API is for. The server's job is to accept cross-origin POSTs, so it sends Access-Control-Allow-Origin and the request succeeds from an extension that declared nothing beyond storage. Chrome's cross-origin network requests guide documents the first path, saying a script in an extension service worker or foreground tab can talk to servers outside its origin as long as the extension requests host permissions. The second path is ordinary CORS doing its job.

A second detail decides whether this is smooth or annoying: preflight. A cross-origin POST with Content-Type: application/json is not a CORS "simple request", so the browser sends an OPTIONS preflight first and the server has to answer that too. A POST with Content-Type: text/plain;charset=UTF-8 is simple, so it goes out directly. That is why the Moderok SDK sends its batches as text/plain:

await fetch(endpoint, {
  method: "POST",
  headers: { "Content-Type": "text/plain;charset=UTF-8" },
  body: JSON.stringify(payload),
});

The body is still JSON; only the declared content type changes. If you are debugging a request that fails before it is sent, we went through the failure modes in detail in why your Chrome extension fetch is blocked by CORS policy.

How to test it before you commit

Do not take a vendor's word for which permissions their SDK needs. Load an unpacked extension whose manifest contains nothing but storage, open chrome://extensions, click the "service worker" link on the card, and post to the analytics endpoint from that console:

await fetch("https://api.example.com/v1/events", {
  method: "POST",
  headers: { "Content-Type": "text/plain;charset=UTF-8" },
  body: JSON.stringify({ test: true }),
});

If you get a response, the endpoint returns usable CORS headers and you will never need a host permission for it. If the console shows a CORS failure, that SDK costs you a host permission and an install warning. Better to learn that now than after users see the prompt.

Permissions are not the same as the privacy disclosure

Keeping your permission list short does not shorten the Chrome Web Store Privacy practices tab. Permissions describe capabilities the browser grants; the disclosure describes data your extension collects and sends. Adding analytics with only the storage permission still means you are collecting something, and you have to say so on the form and keep it consistent with what your extension actually does. We covered the categories and the Limited Use certifications in the Chrome Web Store data collection disclosure.

What matters for the disclosure is what the SDK sends, not what it declares. An SDK that quietly attaches tab URLs or an email address changes your answers even though its manifest looks clean.

Remotely hosted code is a separate constraint

Permissions are not the only thing that rules out an analytics option. Manifest V3 forbids remotely hosted code, so an analytics tool whose install instructions say to add a <script src="https://cdn..."> tag or to load its snippet at runtime is not shippable on the Chrome Web Store, whatever its permission story is. Reviewers reject it under a documented remote hosted code violation. The workable shape is an npm package that your bundler includes in the extension zip. Page-analytics snippets and tag managers do not survive the move to an extension for this reason.

What to ask before you pick a tool

Five questions, in the order they will bite you:

  1. What goes in the manifest? If the answer is anything more than storage plus, occasionally, alarms, ask why.
  2. Does the endpoint send CORS headers? Test it from a service worker console as above. This is the difference between zero install warnings and one.
  3. Is it shipped as a bundled package? Anything fetched at runtime is a Web Store rejection waiting to happen.
  4. What is in the payload? This drives your Privacy practices answers, not the permission list.
  5. What does it weigh? An analytics SDK is dead weight in every user's browser. The Moderok SDK is 5.7 kB gzipped with zero runtime dependencies, measured on the built moderok.min.js.

All of them are easier to answer before you write the instrumentation than after.

Where Moderok lands

Moderok's setup is three parts: install @moderok/sdk, add the storage permission, and call Moderok.init({ appKey: "mk_your_app_key_here" }) at the top level of your background service worker. There is no host_permissions entry, because the ingestion endpoint returns CORS headers and the SDK posts a simple content type. The SDK assigns a random analytics profile id rather than reading any account identity, and it may store that id with chrome.storage.sync as well as local, so the id can come back after a profile reset.

Applying question 4 to our own SDK: custom event properties are only what you pass to track(), but two things go out without you passing them. Every event carries context such as browser, OS, extension version and locale, and automatic error capture is on by default, sending uncaught exceptions and unhandled rejections as __error events with the message and stack from your own code (uncaught exceptions also carry the filename). Set trackErrors: false to turn that off. The events the SDK sends on its own are listed in the automatic events guide, error capture in the error tracking guide, and the manifest keys in the manifest and permissions guide.

If you want to see the reports before touching your manifest, the product page walks through them, and the three-part setup is in the getting started guide.