til/umami-sends-next-to-its-script.mdtil
date
2026-11-10
tags
[analytics, cloudflare]

TIL

Umami's tracker sends events next to wherever the script was loaded from

The Umami tracker doesn’t need to be told where to send events. It takes the folder its own <script src> came from and posts to api/send beside it:

<script src="/_a/s.js">   →   POST /_a/api/send

That makes first-party analytics two paths on your own domain. I checked it in a browser on both sites I run it on, an ecommerce storefront and this one: each posts to /_a/api/send on its own host.

The setup on Cloudflare

The site is static, so the Worker only exists for those two paths. Everything else is served straight from the build output without invoking it:

// wrangler.jsonc
"main": "worker/index.ts",
"assets": {
  "directory": "./dist",
  "binding": "ASSETS",
  "run_worker_first": ["/_a/*"]
},
"services": [{ "binding": "UMAMI_EDGE", "service": "umami-edge" }]

The Worker maps /_a/s.js and /_a/api/send to the analytics edge Worker through a service binding, and does three small things on the way:

  • drops the Cookie header, so the site’s cookies never leave it;
  • passes Cloudflare’s cf object along, so Umami still sees the visitor’s country and city, not the Worker’s;
  • answers 502 if the call fails, because analytics must never break a page.

The edge Worker keeps the checks in one place for every site: an origin allowlist, a website-ID allowlist and a per-IP rate limit. Until a site is on both lists its events get a 403, which is exactly what this one got in the few minutes between turning the tracker on and deploying the allowlist.

Two details worth setting

  • data-domains="imtoanle.com" on the script tag, so dev, preview and workers.dev visits aren’t counted.
  • Nothing extra for client-side navigation: Umami follows history.pushState, so Astro’s view transitions still produce page views.

Why bother

The browser never makes a third-party request. There’s no tracker host for a blocker to drop, and because the request is same-origin, there’s no CORS preflight either.

↑↓ select ↵ open esc close