Chrome Extension Analytics and GDPR: When You Need Consent
7 min readModerok team
When Chrome extension analytics needs consent, why the ePrivacy device-storage rule decides it before GDPR does, and how to gate an SDK in MV3.
If you are working out whether your Chrome extension analytics needs GDPR consent, first fix which rule you are reading. GDPR governs the lawful basis for processing personal data. Article 5(3) of the ePrivacy Directive is a separate rule, governing whether you may write anything to the user's device at all, and it applies whether or not what you wrote is personal data. An analytics SDK that saves an id into chrome.storage is doing the thing that second rule is about, from its first write. This post is not legal advice.
TL;DR: The GDPR question most extension developers ask is really an ePrivacy question. As the EDPB quotes it, Article 5(3) covers "the storing of information, or the gaining of access to information already stored, in the terminal equipment of a subscriber or user", and "the term used is not 'personal data', but 'information'". Writing an analytics id to
chrome.storage.localis that storage, so the rule is consent unless an exception applies, not a debate about whether a random UUID identifies anyone.
Chrome extension analytics and GDPR: two rules, not one
The EDPB's Guidelines 2/2023 on the technical scope of Article 5(3) (version 2.0, adopted 7 October 2024) set out the test in three criteria. The first disposes of the question developers usually start with: "It should be noted that the term used is not 'personal data', but 'information'."
The same guidelines back that with the Court of Justice: "That protection applies to any information stored in such terminal equipment, regardless of whether or not it is personal data". So the familiar argument, that a random id with no name attached is not personal data and so needs no consent, does not reach this rule.
Does the rule reach a Chrome extension SDK?
The closest use case in the guidelines is "local processing", where a value is produced on the device and then sent out. Their conclusion: "The fact that this information is being produced locally does not preclude the application of Article 5(3) ePD." Their use cases cover web and app tracking and IoT, not browser extensions, so an extension is that reasoning applied rather than a worked example in the text.
The UK rule reads the same way. The ICO's guidance on the PECR rules, finalised in April 2026, quotes regulation 6 as a prohibition on storing or accessing information "in the terminal equipment of a subscriber or user", says "The rules cover any use of storage and access technologies", and treats SDKs embedded in apps as covered because they store or access information on a device.
What an extension analytics SDK actually stores and sends
For Moderok's SDK, the main writes and transmissions are:
- One key in
chrome.storage.localholding the profile id, last daily-ping date, pending events and resolved config. - One key in
chrome.storage.syncholding the profile id and last ping date. With Chrome Sync on it can follow the user's account across profiles and synced browsers, so it is not a device-local identifier, and it is one reason your own count and the store's diverge (divergence docs). - Each event carries its own id, the profile id (a random UUID), the name, a timestamp, your flat properties, and a context block: SDK version, extension id and version, browser and version, OS, locale from
navigator.language, and the sending surface. Each batch is posted with your app key and a send timestamp. - Five events go out with no
track()call of your own:__first_open,__daily_ping,__install,__updateand__error. Error capture is on unless you passtrackErrors: false, and sends the capture kind, error name, message, stack, filename, line, column and calling Chrome API, plus a fingerprint and whether it was handled. - With
trackUninstalls: true, thechrome.runtime.setUninstallURLtarget carries your app key and that profile id as query parameters.
Custom properties are whatever your code passes, so no SDK can promise there is no personal data in them. The string, number and boolean restriction is a shape constraint, not a privacy filter.
Do the exceptions cover analytics?
The exemptions are jurisdictional: the EDPB declines to address them because they "should be analysed on a case-by-case basis accounting for the relevant member state transposition(s)", so there is no single EU answer.
The UK is documented more concretely. The ICO's exceptions page lists a strictly necessary exception, limited to storage "essential to provide the service the subscriber or user requests", judged "from the point of view of the subscriber or user, not your own". Product analytics rarely clears that bar.
It also lists a statistical purposes exception, which looks promising until you read the conditions. The output must be "aggregate statistical information that you cannot use to identify people", and "The statistical purposes exception does not allow you to monitor or track individual visitors to your service." Even where it applies you still owe "clear and comprehensive information about the purpose, and a 'simple and free' means to object."
Read that against how extension analytics works. A persistent id exists so you can tell a returning user from a new one, which is how daily actives and retention are computed. Keep that id and you are closer to monitoring an individual than to aggregate-only statistics.
Where no exception applies, the ICO's myth-busting post closes the usual escape hatch: "Where PECR requires consent and you are processing personal data, you can't rely on legitimate interests under the UK GDPR."
Chrome Web Store policy adds a separate duty
Chrome's own rules apply to all your users. Its user data FAQ permits extensions to "collect analytics or performance data where it is reasonably necessary to maintain, secure, or measure" the reliability of "the extension's disclosed functionality."
It also sets where consent must happen: "The prominent disclosure and consent must occur within the Product's user interface", and not "only in a privacy policy, terms of service, or similar document." There is no web page to put a banner on, so the surface is an extension page of yours. The store-side form is covered in the Chrome Web Store data collection disclosure.
Gating an SDK on consent in MV3, and what it costs
With Moderok there is no opt-out flag in the config, so the gate is whether you call init() at all. That carries an MV3-specific cost, because init() registers the chrome.runtime.onInstalled listener, which Chrome's runtime reference says fires on first install, on update to a new version, and when Chrome itself updates. Chrome's service worker events guide requires handlers to be "declared in the global scope", not "nested inside functions".
Defer init() behind an async consent read and that listener is registered inside a callback rather than on initial script execution, so a firing can be missed; for a user who declines, it is never registered at all. What the gate costs you is __install and __update. You do still get __first_open, which the SDK enqueues the first time it bootstraps without a stored id.
import { Moderok } from "@moderok/sdk";
// Top level, so Chrome can dispatch to it.
chrome.runtime.onInstalled.addListener((details) => {
if (details.reason === "install") {
chrome.tabs.create({ url: chrome.runtime.getURL("onboarding.html") });
}
});
chrome.storage.local.get("analyticsConsent", ({ analyticsConsent }) => {
if (analyticsConsent === "granted") {
Moderok.init({ appKey: "mk_a1b2c3d4e5f6g7h8", trackErrors: false });
}
});
Withdrawal is harder, because two things that look like off switches are not. Calling track() before init() makes the SDK auto-initialize from a config it stored in chrome.storage.local, so a stray track() restarts it after you stop calling init(). And shutdown() disposes the error tracker and clears the flush timers, but leaves the client initialized and its queue in place, so a later track() still enqueues and can trigger a send at batch size. Deleting the storage keys does not settle it either: the SDK holds state in memory and rewrites the record on its next persist. So for a user who has withdrawn, call neither init() nor track().
What consent does not settle
Consent is one obligation among several. You still need the store's privacy-practices declarations, a privacy policy, and a statement in your own UI of what you collect, and Firefox adds a separate manifest-level declaration. Gating is also separate from permissions: see what permissions Chrome extension analytics needs.
No vendor can hand you compliance, because the decisions that matter (which events you send, which properties ride along, how long you keep them) are yours. What a vendor can do is be precise about what its SDK writes and sends: the identifier and storage side is in the privacy docs, the events that arrive without a track() call are in the automatic events reference, and the product page shows what the dashboard builds from them.