Skip to content

How Many Users Does My Chrome Extension Have?

6 min readModerok team

How many users does my Chrome extension have? What Web Store Installs and Weekly Users each count, and how to measure active users yourself.

If you are asking how many users your Chrome extension has, the honest answer is that Chrome never gives you that number. The Web Store developer dashboard reports install requests and Weekly Users, which is a rolling seven-day count of browsers that loaded your extension, and both arrive on a delay. Neither is a count of people. For an active-user number you can query on your own terms, emit a heartbeat from your extension and count distinct ids yourself. This post covers what each number counts and how to build the one you are missing.

TL;DR: Installs counts install requests, Weekly Users counts browsers that loaded the extension in a seven-day window, and the store's stats are documented as delayed. To get an active-user figure, store a random id in chrome.storage, send one ping per UTC day, and count distinct ids over a window. Expect your figures not to match the store's.

How many users does my Chrome extension have: the numbers you can get

NumberWhat it countsWhere it comes from
InstallsInstall requests from Chrome users, including failed and unknown-outcome requestsWeb Store developer dashboard
Weekly UsersChrome browsers that loaded your extension at least once in a rolling seven-day windowWeb Store developer dashboard
UninstallsUninstall requests from Chrome browsers. A separate, narrower signal fires on user-initiated removes only, if you set chrome.runtime.setUninstallURLWeb Store dashboard, or your own uninstall URL
Active profilesDistinct analytics ids that sent an event in a window, under whatever rule you defineYour own instrumentation

The first three are given to you. Only the fourth lets you choose the definition, which is why only it can answer "how many profiles were active yesterday."

"User" means four different denominators

The confusion here is not about accuracy, it is about denominators. Four different things get called a user:

  1. An install request. One increment per request, not per person. Install twice, that is two installs.
  2. A browser profile. Weekly Users is keyed to browsers that loaded the extension, so one person with a work profile, a personal profile, and a laptop can be three.
  3. An analytics profile. A random id your code generates and stores. It is stable as long as the storage it lives in survives.
  4. A person. Nothing in the extension platform gives you this without asking users to sign in, which is a product decision, not an analytics one.

You cannot convert between these by arithmetic. Total installs minus total uninstalls does not equal Weekly Users, because you would be subtracting a per-request count from a per-browser count and comparing the result to a third measurement. We covered that mismatch in Chrome Web Store Weekly Users vs. Installs. Pick one denominator, name it in your own reporting, and stop comparing across them.

Why the store cannot answer the question on demand

Even if you accept Weekly Users as your proxy for current users, the dashboard has three properties that make it a poor operational metric:

  • It lags. Store metrics are documented as delayed rather than current, so "today" in the dashboard is not today.
  • The window is fixed, and so are the slices. Seven days is the window, and the dashboard's Weekly Users view slices it by country, language, operating system, and item version. Those are the questions you get to ask. "How many people used the feature I shipped on Tuesday" is not among them.
  • It measures loading, not use. A browser counts if it loaded your extension. Whether anyone opened your popup, ran your feature, or even noticed it is still installed does not enter into it.

For "am I growing," the trend line is fine. For anything about your own features, it is the wrong instrument.

How to count active users yourself in MV3

The mechanics are small. You need a stable id, a once-per-day rule, and somewhere to send it. The Manifest V3 constraint that trips people up is that nothing in memory survives: the worker is torn down when idle, so module-level variables are gone on the next wake. Both the id and the date of the last ping have to live in chrome.storage.

// background.js, an MV3 service worker
const utcDay = () => new Date().toISOString().slice(0, 10);

async function heartbeat() {
  const stored = await chrome.storage.local.get(["profileId", "lastPingDate"]);

  let id = stored.profileId;
  if (!id) {
    id = crypto.randomUUID();
    await chrome.storage.local.set({ profileId: id });
  }

  const today = utcDay();
  if (stored.lastPingDate === today) return;

  try {
    const res = await fetch("https://example.com/ping", {
      method: "POST",
      headers: { "Content-Type": "text/plain;charset=UTF-8" },
      body: JSON.stringify({ id, date: today }),
    });
    // Only mark the day as counted once the server accepted it. Advancing the
    // date on a failed request silently drops that day for this profile.
    if (!res.ok) return;
  } catch {
    return; // offline or DNS failure: try again on the next wake
  }

  await chrome.storage.local.set({ lastPingDate: today });
}

chrome.runtime.onStartup.addListener(heartbeat);
chrome.runtime.onInstalled.addListener(heartbeat);

Four details in that snippet matter more than they look:

Register listeners at the top level. In MV3, listeners have to be registered synchronously when the worker script evaluates. Register chrome.runtime.onStartup inside a callback or after an await and the event can fire before your listener exists, losing the wake.

onStartup and onInstalled are not the only wakes. The worker also starts for alarms, messages, and your own events. Call the heartbeat on those paths too: the stored date makes it a no-op for the rest of the day.

Content-Type: text/plain;charset=UTF-8 avoids a CORS preflight. A JSON content type triggers an OPTIONS request your endpoint has to answer, and skipping host_permissions requires the right CORS headers on the response. That surface is covered in which permissions an analytics SDK actually needs.

Daily is a rule, not a truth. A UTC-day heartbeat counts profiles that had a running worker that day. It is defensible precisely because the rule is explicit.

Why your numbers will not match the store's

Your figures will not line up with the dashboard's, and which cause bites depends on which store number you hold them against. Moderok's docs page on install count divergence documents these for install counts, and the mechanics apply to any client-side counter:

  • Ephemeral profiles. Managed Chromebooks can be configured to wipe the profile at logout, clearing chrome.storage.local. The next login mints a brand new id, every school day. This is the one cause that inflates an active-profile count as well as an install count.
  • Multi-profile users. Three Chrome profiles on one laptop are three ids and three install events. Against Weekly Users it is closer to a wash, since that metric counts browsers too.
  • Repair reinstalls. When Chrome re-downloads an extension it considers damaged, chrome.runtime.onInstalled fires with reason: "install" again. That inflates install events only: storage survives, so the id does not change.
  • Day boundaries. If you bucket in UTC and the store buckets in Pacific, single-day comparisons are offset by hours. Compare over weeks.

Compare directionally, over long windows, and never as two measurements of one quantity.

What a purpose-built SDK does instead

This is the machinery Moderok ships so you do not maintain it. After the service worker starts, the SDK sends a __daily_ping when its stored UTC date shows that profile has not pinged yet today, and it advances the stored date only once the server has accepted the event. It generates a random profile id and mirrors it to chrome.storage.sync, so an ephemeral profile wipe can recover the same id instead of minting a new one, which means the id is not confined to a single device. Alongside the ping it emits __install, __first_open, __update, and __error automatically; the full list is in the automatic events docs.

Counting distinct profile ids that pinged in a window gives you daily, weekly, and monthly active profiles under one rule. It counts analytics profiles, not unique humans, and browser sync means one id can appear on more than one device. State that caveat in your own reporting too.

If you want the number without building the pipeline, install @moderok/sdk, add the storage permission, and call Moderok.init() in your background worker. You can see what the dashboard shows first.