Plausible or Umami in a Chrome Extension: What Works in a Service Worker
8 min readModerok team
Putting Plausible or Umami in a Chrome extension: why the page snippet cannot run in an MV3 service worker, and what their HTTP APIs count as a visitor.
If you want Plausible in a Chrome extension (or Umami, which has the same shape), the documented install does not apply. Both tell you to put a tracking snippet in the <head> of your website (Plausible and Umami, both as of September 2026), and a Manifest V3 background service worker has no <head> to put it in. Both publish an HTTP endpoint you can call with fetch() from the worker, and that works. The thing to check before you build on it is who supplies the identity: Plausible's visitor id is a hash that includes the request's User-Agent (Plausible, as of September 2026), and Chrome drops a User-Agent your fetch() sets.
TL;DR: Both browser installs are snippets for the
<head>of a website (Plausible and Umami, both as of September 2026), and a service worker "cannot access the DOM" (Chrome). Their HTTP APIs do work fromfetch(), but Plausible computes a visitor ashash(daily_salt + website_domain + ip_address + user_agent)with a salt that "is rotated and deleted every 24 hours" (Plausible, as of September 2026), so an install is not something you can follow across days.
The 30-second comparison
| What | What the page says | Source |
|---|---|---|
| Plausible browser install | "insert a Plausible tracking snippet into the header section of your site. Place the snippet within the <head> … </head> tags." | Plausible docs, as of September 2026 |
| Plausible HTTP API | POST /api/event with domain, name and url; "The raw value of User-Agent is used to calculate the user_id which identifies a unique visitor in Plausible." | Plausible docs, as of September 2026 |
| Plausible visitor id | hash(daily_salt + website_domain + ip_address + user_agent); "The salt is rotated and deleted every 24 hours." | Plausible data policy, as of September 2026 |
| Umami browser install | "Copy the code and insert it into the <head> section of your website." | Umami docs, as of September 2026 |
| Umami HTTP API | POST /api/send; "you need to send a proper User-Agent HTTP header or your request won't be registered" | Umami docs, as of September 2026 |
| Umami visitor id | A session is "identified by a unique hash generated from the visitor's IP address, user agent, and website ID" | Umami docs, as of September 2026 |
| Umami Distinct ID | "A Distinct ID is a unique identifier assigned to a user, either anonymously or when they log in."; set via the id property on the send payload | Umami docs, as of September 2026 |
| That header in Chrome | "Chrome still silently drops the header from Fetch requests" | MDN, forbidden request header |
| Plausible hosted price | Starter plan, "$9 /month" for "Up to 10k monthly pageviews", after a "30-day free trial" | Plausible pricing, as of September 2026 |
| Moderok install | npm package, the storage permission, and Moderok.init({ appKey }) in the service worker | Moderok docs |
Round 1Why Plausible in a Chrome extension cannot use the snippet
Both document the same install: the tracking snippet goes in the <head> of your website (Plausible and Umami, both as of September 2026, quoted above).
Two things stop that in an MV3 extension. The service worker "cannot access the DOM, though you can use it if needed with offscreen documents" (Chrome), so there is nothing to insert a snippet into. And to the extent a snippet fetches its tracker script from the vendor's host, that is the remotely hosted code Chrome rules out: "In Manifest V3, all of your extension's logic must be part of the extension package. You can no longer load and execute remotely hosted files according to Chrome Web Store policy", with "Any library hosted on a CDN" given as an example (Chrome).
What is left is the HTTP API you call yourself. Plausible documents POST /api/event with domain, name and url required, a Content-Type that "Must be either application/json or text/plain", and says it "is useful when tracking Android or iOS mobile apps, or for server side tracking" (Plausible, as of September 2026). In a worker that is a few lines:
await fetch("https://plausible.io/api/event", {
method: "POST",
headers: { "Content-Type": "text/plain" },
body: JSON.stringify({
domain: "your-site.example",
name: "feature_used",
url: "https://your-site.example/extension/background",
props: { feature: "export" },
}),
});
That snippet is incomplete on purpose: the same page marks User-Agent required and warns that "If these headers are not sent exactly as required, unique visitor counting will not work as intended" (Plausible, as of September 2026).
Umami's POST /api/send is the same idea wrapped in { type: "event", payload: { website, hostname, url, name, data } }, and "for /api/send requests you do not need to send an authentication token" (Umami, as of September 2026). If that POST comes back rejected, why a Chrome extension fetch is blocked by CORS policy covers the two ways out.
Notice what you are filling in: Plausible's url is "URL of the page where the event was triggered" (Plausible, as of September 2026), and Umami's tracker collects page context by default (Umami, as of September 2026). A service worker has no page title, URL, referrer or screen, so you invent a stable fake page URL and keep using it.
Moderok, on the narrow question of install path. Its documented setup is an npm package, the storage permission, and Moderok.init({ appKey }) at the top level of the service worker (docs). The two install pages above are written for websites, which is the job they do.
Round 2Who counts as a user
Plausible's data policy gives the visitor identifier as hash(daily_salt + website_domain + ip_address + user_agent) and says "The salt is rotated and deleted every 24 hours" (Plausible, as of September 2026). Its events API repeats the client-side half: "The raw value of User-Agent is used to calculate the user_id which identifies a unique visitor in Plausible" (Plausible, as of September 2026). Umami's scheme has the same shape. A session there is "identified by a unique hash generated from the visitor's IP address, user agent, and website ID" (Umami, as of September 2026), using "a rotating salt that changes at the start of every month" (Umami, as of September 2026), and /api/send will not register a request without "a proper User-Agent HTTP header" (Umami, as of September 2026).
Your fetch() call cannot choose that header. MDN's note on forbidden request headers says User-Agent "used to be forbidden, but no longer is. However, Chrome still silently drops the header from Fetch requests" (MDN). Whatever value reaches Plausible or Umami is therefore the browser's, not yours. Chrome documents one other lever: user-agent is in the list of request headers that declarativeNetRequest's append operation supports (Chrome). Whether that works on your own extension's requests, and what permissions it needs, is worth testing first.
Two consequences follow. Requests arriving with the same IP address and User-Agent, such as two installs in one household, hash to the same visitor in either product. And with Plausible the salt is deleted every 24 hours, so yesterday's hash cannot be matched to today's. That is deliberate: the same page says the approach "prevents tracking users across days while still providing useful aggregate analytics" (Plausible, as of September 2026). Umami's monthly salt leaves more room, and it will also take an id of your own: since v2.18.0 you can set a Distinct ID by "adding the id property to the event payload when using the Sending stats API directly" (Umami, as of September 2026). That is the one route here by which an extension generates its own anonymous id, stores it, and sends it.
Moderok's SDK goes the other way: it creates a random anonymous id per extension profile and stores it with chrome.storage.local, with no cookies and no fingerprinting (docs). That id can come back through browser profile sync, so treat it as a profile identifier rather than a device one.
Moderok, with Umami second. Moderok creates and persists the id for you; Umami accepts one if you build and store it yourself. Plausible rules the question out by design, which is its stated intent.
Round 3What you still have to write
With either HTTP API the endpoint is the whole product surface. Installs, updates, uninstalls, first open, a once-per-day active ping, a retry when the worker is torn down mid-request: each is code you write against chrome.runtime.onInstalled and friends. Plausible's props carries "a maximum of 30 key-value pairs" per event (Plausible, as of September 2026).
Moderok's SDK sends __install, __update, __first_open, __daily_ping and __error without you calling track() (docs), in a client of 5.7 kB gzipped with zero runtime dependencies. Its queue is capped and debounced, and retryable failures are persisted and retried with backoff.
Moderok, because the extension lifecycle is the default rather than the integration work.
Round 4Running it on your own hardware
"Plausible Community Edition (CE) is the free, self-hosted, AGPL-licensed release: you run it on your own server and manage everything yourself" (Plausible, as of September 2026). Umami documents installing it yourself on a Node.js server with a PostgreSQL database (Umami, as of September 2026).
Plausible and Umami. If the database must sit on a machine you control, they answer directly.
When Plausible or Umami is the right choice
Pick one of them when the extension is not the main event. If you already run Plausible or Umami for your site, a small wrapper puts extension events in the same dashboard, and one less tool to maintain beats a better data model in a second product.
Pick Plausible in particular when its identity model is the point: a visitor hash from a salt deleted every day is a strong thing to put in front of users and reviewers. Pick Umami when you want the rows in a database you operate.
Pick something extension-shaped when your questions are: how many installs stuck, what happened after the last update, which features the people who stayed use. On counting installs, see how many users your Chrome extension really has; for the wider field, the Chrome extension analytics roundup and the four-way comparison including Umami.
If you would rather not build that wiring, read the Moderok getting started guide: npm install, one permission, an init() call in the service worker. And see what the dashboard reports.