How to Find Out Why Users Uninstall Your Chrome Extension
6 min readModerok team
How to find out why users uninstall your Chrome extension using the one page Chrome opens on removal, uninstall tenure, and events logged earlier.
Chrome never tells you why someone removed your extension. There is no reason code, and chrome.runtime has no uninstall event you can listen for, because your code is deleted before you could run it. You get one hook, chrome.runtime.setUninstallURL(), which opens a page after removal. So finding out why users uninstall your Chrome extension comes down to three things: the single question you ask on that exit page, how long the person had the extension installed, and the events and errors you recorded for them before they left. The third one is the only one you can still change today.
TL;DR: Ask one multiple-choice question on the
setUninstallURLpage and expect a low response rate, because you cannot ask twice. Then read uninstalls against tenure, because a same-day removal and a month-old removal are different problems with different fixes. Neither reading is possible unless you instrumented one activation event and error capture beforehand.
Why Chrome will not tell you why users uninstall
chrome.runtime has no uninstall event. The runtime API reference lists onInstalled, onStartup, onEnabled, onSuspend and a dozen more, and nothing for removal. That is mechanical rather than an oversight: the extension is deleted before any callback of yours could run.
The management API does have an onUninstalled event, described in its reference as "Fired when an app or extension has been uninstalled," but it needs the management permission, whose documented install warning is "Manage your apps, extensions, and themes." according to Chrome's permissions list. The page does not say an extension receives that event for its own removal, and your own code is gone either way, so it is not a route to your own uninstall reasons.
That leaves setUninstallURL(), and it only ever carries one page load. How to track uninstalls with setUninstallURL covers the mechanics, the http:/https: scheme rule, the documented 1023-character maximum and the Firefox differences. What follows is what to do with that one page load.
The one question you get to ask
Design the exit page for a person who has already left and will not come back.
- One question, radio buttons, no required free text. Anything longer is competing with the close-tab button. "Which of these is closest to why you removed it?" with a short option list and an optional comment box is the ceiling.
- No sign-in, and no email field above the fold. They just removed your software. An account gate reads as a second ask.
- The page knows nothing except what is in the URL. It is an ordinary web page, not an extension context, so
chrome.storageis not available to it even while the extension is installed: the Storage API is reached from extension contexts such as the service worker and content scripts. Anything the page needs has to travel in the query string, inside that 1023-character budget, so keep it to a couple of short parameters. - A tenure number cannot be baked in once. The URL is fixed at the moment you register it, so an install age written at install time still reads zero months later. Chrome extension uninstall rate has the refresh pattern and the bucket your pre-existing install base needs.
- You cannot ask twice. There is no retry, no reminder, no second render. Treat the response rate as low and the answers as directional, not as a measurement.
The survey is worth building, but nothing makes anyone answer it, so plan for a thin sample. What every departure leaves behind instead is a timestamp.
Read the tenure, not just the count
The same ten removals mean completely different things depending on how long those people had the extension installed, which is why Moderok's Trends view has an uninstalls-by-tenure metric rather than only an uninstall count. Its buckets begin Same day, 1-6 days, 1-4 weeks and run out to 12+ months.
Each band sends you somewhere different, and the move in every case is to pair it with something else you measured:
- Same day. They installed, looked, and left. The cross-check is your activation rate: of the installs in that period, how many fired your core action event even once? An activation rate near zero means the failure lands before your code does anything interesting.
- 1-6 days. They tried it and it did not hold. Error events in the same window are the first place to look, especially ones on the path your activation event sits on.
- 1-4 weeks and beyond. Long enough that the extension was doing its job for a while. Line these up against your own release dates before anything else.
A cluster of removals that all begin within a day or two of one release date is the clearest signal you will get. Note that you have to line that up yourself: the uninstall ping is a bare page load issued by the browser, not an event sent by your extension, so it arrives with no extension version or browser context attached.
The evidence has to exist before they leave
This is the part that decides whether the exit page is useful or just a sad little form. Once someone has uninstalled, the only facts available about them are the ones you already collected, so the instrumentation has to be in place first. It is short:
// One event that means "this person got the value they came for"
Moderok.track("link_shortened", { source: "popup" });
// Errors on that same path, so a removal has something to correlate with
try {
await shortenLink(url);
} catch (error) {
Moderok.captureError(error, { action: "shorten_link" });
}
Uncaught exceptions and unhandled promise rejections are recorded as __error events in every context where you call init(). Handled failures need the explicit captureError call, and chrome.runtime.lastError needs captureLastError. See error tracking for the rest.
The activation event matters more than the error log. With one event meaning "the user did the thing the extension exists to do," you can separate people who never activated from people who activated and later left. Those two groups need opposite fixes, and no exit survey will split them for you. Name that event after the user intention, one per thing your extension exists to do, and do not change the name later.
To have the uninstall attributed to the same anonymous profile that fired those events, Moderok's SDK can register the uninstall URL for you:
Moderok.init({
appKey: "mk_your_app_key",
trackUninstalls: true,
uninstallUrl: "https://yoursite.com/goodbye",
});
With trackUninstalls on, the SDK points setUninstallURL at a Moderok endpoint carrying your app key and the profile id, and the uninstall tracking docs describe uninstallUrl as where the user is sent after the ping. Two practical notes: use an https: URL there, and remember that the parameters Moderok needs are its own. If your survey page wants context, put it in the uninstallUrl you pass, and register that URL in one place only: the SDK calls chrome.runtime.setUninstallURL() itself here, so a hand-rolled call elsewhere in the same extension is a second writer of one setting.
What you still will not learn
The ping reaches you only when a running browser opens that tab on your behalf, so it undercounts. A disabled extension is still installed and fires nothing, and a removal that no browser session handles has no tab to open at all. How to track uninstalls with setUninstallURL works through those cases and the Firefox differences.
And correlation stays correlation. Tenure bands, activation status and a release date tell you where to look, not what happened. The honest workflow is to form the hypothesis from the data, then confirm it with the few survey answers you get, your support inbox, and recent store reviews.
If you want installs, updates, uninstalls, net churn and uninstalls-by-tenure without building the pipeline, see what the dashboard reports or start with the getting started guide.