2026-06-12 · 7 min read

How to track uninstalls of a Chrome extension (and estimate the ones you can't see)

Uninstalls are the most important metric the store hides from you. The Chrome Web Store dashboard shows a rounded total, days late, with zero context — no version, no tenure, no pattern. Yet an uninstall is the strongest signal a user will ever send you. This is a practical guide to capturing it: the one mechanism browsers give you, the right server-side pattern, its blind spots, and how to estimate the churn it can't see.

The only hook you get: setUninstallURL

When a user removes your extension, there is no event, no callback, and no JavaScript left to run — your code is already gone. The single mechanism browsers provide is chrome.runtime.setUninstallURL(url): a URL, registered ahead of time, that the browser opens in a new tab the moment the extension is uninstalled. Chrome caps it at 1023 characters; Firefox supports it too.

That design dictates the architecture: everything you want to know about the uninstall must already be in the URL before it happens, and the logging must occur server-side when the URL is opened.

Register early, refresh often// In the background service worker — at install and on a periodic alarm:
await chrome.runtime.setUninstallURL(
  "https://your-server.example/u" +
    "?ins=" + installationId +     // anonymous random id
    "&t=" + serverIssuedToken +    // so bots can't pollute your data
    "&v=" + chrome.runtime.getManifest().version +
    "&ls=" + Date.now()            // "last seen", refreshed by the alarm
);

Three details separate a useful setup from a broken one:

1. Authenticate the hit.The URL will leak (it's visible in about:extensions internals and in your code). If your endpoint logs any GET blindly, crawlers and curious users will fabricate uninstalls. Issue a per-installation token from your server on first contact and reject hits without a valid one.

2. Refresh the parameters. The URL is a snapshot. If you set it once at install, every uninstall will report the install-day version and a meaningless last-seen. Re-register it on a chrome.alarmstick so version and last-seen stay fresh — that's what turns a raw count into "v3.2.1, last active 2 days ago, uninstalled after 6 weeks".

3. Make the landing page worth opening.The browser shows your page to the user. A blank 200 response wastes the only goodbye you get. A short, honest "sorry to see you go — what went wrong?" form converts a surprising fraction of uninstallers into actionable feedback. Keep it anonymous and say so: this mechanism has drawn legitimate privacy criticism when abused for tracking or guilt-tripping.

What setUninstallURL will never tell you

Know the blind spots before you trust the number. The URL does not open when: the user is offline at uninstall time, the whole browser or profile is deleted, the extension is removed via enterprise policy, or (historically) in some incognito-only setups. In practice the observed count is a floor, not the truth.

The complement is silence detection. If each installation sends one anonymous heartbeat per day, then an installation that was active daily and has now been silent for 14+ days is — for every practical purpose — churned, whether or not the uninstall page ever fired. Report the two numbers side by side: observed uninstalls (URL hits) and estimated churn (heartbeat silence). The honest picture is their sum.

The full pipeline, end to end

1. First event
server issues a per-installation uninstall token
2. Background alarm
re-registers the uninstall URL with fresh version + last-seen
3. Uninstall
browser opens the URL; server validates the token
4. Server log
uninstall recorded with version, browser, country, tenure
5. Daily job
heartbeat-silent installs counted as estimated churn
6. Dashboard
observed + estimated, by version and by cohort

None of this is exotic — it's an afternoon of plumbing, then a long tail of edge cases: token storage that survives service-worker death, alarms that re-arm after browser restarts, retry when the first registration races the first network call.

Or get it wired by default

extension.report ships this whole pipeline in its SDK: the uninstall token is issued on your first ingested event, setUninstallURL is registered and refreshed automatically with version and last-seen attached, hits are validated server-side, and silent churn is estimated from missing heartbeats. Uninstalls land in a live feed with the context that makes them actionable — version, browser, country, and how long the install lived.

Everything above, one callimport { initExtensionReport } from "@extension-report/js";

const sdk = initExtensionReport({ projectPublicKey: "pk_er_..." });