Skip to content

Chrome Extension Retention: Measuring Day-7 and Day-30

7 min readModerok team

Chrome extension retention is not in the Web Store dashboard as an install cohort. How to define day-7 and day-30 retention and compute it in MV3.

Chrome extension retention, in the day-7 and day-30 sense every other product category uses, is not a number the Chrome Web Store gives you. Its metrics page mentions weekly user retention sliced by country, language, operating system, and item version, and that is the extent of it. For day-7 and day-30 you need two things from your own code: a stable anchor for day 0, and a record of which days each installation came back. Here is how to define both so the percentage means something.

What the Web Store already tells you, and what it does not

Google's metrics page spends one sentence on retention and never says what "weekly user retention" computes, over which window, or against which denominator. What is clear is that it is grouped by attribute and reported weekly, not by install date, so it cannot answer "of the people who installed on 1 September, how many were still here on 8 September."

A note in the Users section narrows it further: "The Users stats only captures installations; it doesn't monitor whether users are active or not." So even the published retention is retention of installations, not usage: an extension nobody has opened in two months counts the same as one used hourly, right up until it is removed.

Chrome extension retention: pick a definition before you write the query

"Retention" hides three calculations, and a number quoted without saying which one you used is uninterpretable.

  • N-day retention. Active on exactly day N. Strictest and noisiest: an extension used every Monday gives a jagged curve.
  • Bounded-window retention. Active anywhere in a fixed window after day N, say days 30 to 36. Smoother, and cohorts stay comparable because each gets a window of the same length.
  • Unbounded retention. Active on day N or any day after. Intuitive, and the trap below.

Then decide what "active" means, which is harder in an extension: there is no page load to hang it on. An MV3 service worker can be woken by an alarm or a content script message with the user doing nothing, because Chrome's lifecycle documentation says that when a worker has gone dormant, an incoming event will revive it. A worker that nothing wakes does not report at all. The two workable definitions are laid out in how to measure daily, weekly and monthly active users: an active profile (the extension ran that day) or an engaged user (the person did something you count as use). The first is closer to "still installed and the browser opened"; the second responds to product work. Label whichever you pick.

Anchoring day 0

Chrome reports installations through chrome.runtime.onInstalled. The event carries a reason, and only "install" can be a new user: "update", "chrome_update" and "shared_module_update" are all the same installation continuing. The reverse does not hold, which is point 2 below.

// background.js, registered at the top level so the worker can be revived for it
chrome.runtime.onInstalled.addListener(async ({ reason }) => {
  if (reason !== "install") return;
  const { cohort } = await chrome.storage.local.get("cohort");
  if (cohort?.installDay) return; // never overwrite an existing day 0
  await chrome.storage.local.set({
    cohort: { installDay: new Date().toISOString().slice(0, 10) }, // UTC
  });
});

Three details decide whether that anchor holds:

  1. Register the listener at the top level. Chrome's service worker guidance says handlers "need to be declared in the global scope" so that they are registered synchronously on initial script execution. A listener added after an await may not exist when Chrome revives the worker to deliver the event.
  2. Write the day once, and expect "install" to fire for people who are not new. Moderok's docs enumerate three causes: a managed Chromebook in ephemeral mode whose profile is deleted at the next Chrome start, a silent repair reinstall of a corrupted extension, and one person running several Chrome profiles. The guard stops the first case overwriting a real day 0 only while the profile survives: a wipe takes chrome.storage.local with it, guard included, and that person rejoins as a new cohort member. Mirroring your day-0 record to chrome.storage.sync recovers some of them where Chrome Sync is on.
  3. Stamp it in UTC, the same way you stamp activity days. A day-0 stamp in local time against activity days in UTC is off by one for every user outside UTC, permanently and without anyone travelling, and the error lands in the day-1 column.

If an id has no install event, because you added analytics to an extension that already had users, fall back to its first activity day and keep those cohorts out of anything you present as retention. They are a backfill, not new users.

Computing day-7 and day-30

The input is one row per identifier per active day, deduplicated on (profile_id, day). Assume day and day0 are DATE columns, so day0 + 7 is date arithmetic.

WITH cohort AS (
  SELECT profile_id, install_day AS day0 FROM installs
)
SELECT
  c.day0,
  COUNT(DISTINCT c.profile_id) AS cohort_size,
  COUNT(DISTINCT CASE WHEN a.day = c.day0 + 7 THEN a.profile_id END) AS d7_exact,
  COUNT(DISTINCT CASE WHEN a.day BETWEEN c.day0 + 7  AND c.day0 + 13 THEN a.profile_id END) AS d7_window,
  COUNT(DISTINCT CASE WHEN a.day BETWEEN c.day0 + 30 AND c.day0 + 36 THEN a.profile_id END) AS d30_window
FROM cohort c
LEFT JOIN activity a ON a.profile_id = c.profile_id
WHERE c.day0 <= CURRENT_DATE - 37
GROUP BY c.day0
ORDER BY c.day0;

Divide each column by cohort_size for the percentage. Two things there are load-bearing.

The LEFT JOIN keeps installations with no matching activity rows in the denominator. An inner join drops them, and a profile that installed and never came back is precisely the case retention exists to count.

The windows are closed at both ends. The tempting form of unbounded retention, a.day >= c.day0 + 30, hands each cohort a lookahead as long as its own age: the oldest cohort has had every day since day 30 to qualify, the youngest one the filter admits has had a week. The number then rises with cohort age for a mechanical reason. If you want unbounded retention anyway, cap the lookahead at the same length for every cohort and say what that length is.

The filter that is doing the real work

WHERE c.day0 <= CURRENT_DATE - 37 is the line easiest to leave out, and leaving it out produces a cliff at the right-hand edge of the chart. (The widest window strictly needs 36 days; the extra one absorbs a cohort whose last window day is still being written.)

A cohort that installed nine days ago has had nine days to reach day 30. Its day-30 retention is not low, it is undefined. Include it and you divide a number that can only grow by a denominator that is already final, so recent cohorts report near zero and the chart shows a collapse that is pure arithmetic. Compare only cohorts that have had the full window, put the cutoff on the chart, and publish the window alongside any headline number.

Retention is not one minus your uninstall rate

Deriving retention from uninstalls is tempting, and the two disagree in both directions.

Uninstall pings come from chrome.runtime.setUninstallURL, which Chrome documents as "the URL to be visited upon uninstallation", with a maximum of 1023 characters. That is the whole contract: a tab, opened at removal time. Chrome documents no ping for any other way an installation can stop existing, and a departure that never opens that tab is one you never see, which is a reason any uninstall rate you measure is a floor.

Retention fails the other way: an installation still present but dormant is not retained under an engaged-user definition, even though nobody removed it. Those are the interesting users, reachable in a way an uninstaller is not.

Compare the curve against itself

Treat any day-7 or day-30 benchmark you find for extensions as unverified until you can see its definition and its source. Most of the numbers that surface for this query are app retention benchmarks, and an extension has no sessions, no foreground launches and no store-supplied cohorts to make them comparable.

So hold the definition fixed and plot cohort over cohort. Changes to onboarding and first-run behaviour land in the day-1 and day-7 columns first, because those cohorts mature soonest. Day-30 moves last, and it tells you whether the habit stuck.

Moderok's SDK emits the raw material without any track() calls of your own, once the package is installed, the storage permission is declared and Moderok.init() runs: __install when Chrome reports a new install, __first_open the first time a new anonymous id is created, and __daily_ping, documented as at most once per UTC day per user after startup. Those are your day-0 candidates and your activity rows, and they deserve the scepticism of the rest of this post: raw install events carry the duplicate-install problem above, and the dashboard's Installs metric strips one class of it, the ephemeral Chromebook phantoms, passing the rest through. The dashboard reports daily, weekly and monthly actives, engaged users, and uninstalls by how long the installation lasted; the cohort curve above is yours to build from the events. To get the numbers around it without building the pipeline, see what Moderok measures.