Browser — @millimetric/track
@millimetric/track — the browser SDK. ~1.8 KB gzipped.
The browser SDK does the boring stuff so you don't have to: anonymous ID in localStorage, batched flushes, sendBeacon on pagehide, SPA history patching, attribution params captured per event, DNT/GPC bail-out.
Install (one of two ways)
A. Drop-in <script> snippet — for static sites, Webflow, Framer, marketing pages
<script async
src="https://api.millimetric.ai/v1/a.js"
data-key="pk_live_…"
data-host="https://api.millimetric.ai"></script>The Worker that ingests events also serves the browser loader from the same hostname, so one deploy + one DNS record is all you need.
That's it. The snippet:
Initialises the SDK with your
pk_*key.Auto-fires
$pageviewon load and on every SPA navigation.Exposes a global
window.mmyou can use for custom events:
<button onclick="window.mm.track('clicked_pricing', { plan: 'pro' })">
See plans
</button>B. npm — for React / Next / Vue / Svelte / Solid
You can call init() more than once with the same key — it's idempotent.
Public API
init(options)
track(event, properties?)
event is required, 1–128 chars. properties is free-form JSON, capped at 8 KB after JSON.stringify.
identify(userId, traits?)
Emits a $identify event and attaches user_id to every subsequent track() call within this session.
page(name?, properties?)
Emit a $pageview explicitly. Auto-fired by the SDK on init() and on SPA navigations; call it directly only if you need finer control (e.g. tracking a virtual page inside a modal).
flush()
Forces an immediate batch send. Useful before navigating away in a controlled flow.
getAnonymousId() / setAnonymousId(id)
What's captured automatically
On every event, the SDK attaches as properties:
Plus on the event itself:
The classifier on the server uses url and referrer to determine source/medium/campaign.
SPA navigation
The SDK patches history.pushState and history.replaceState and listens to popstate. Every navigation triggers a $pageview on the next microtask (so document.title reflects the new page).
If you'd rather track page views yourself:
Batching & sendBeacon
Events are queued in-memory.
Flushed when the queue hits 20 events or 2 seconds after the first queued event (whichever comes first).
On
pagehide/visibilitychange: hidden, the SDK callsnavigator.sendBeaconso in-flight events survive page exits.On a
5xxresponse, the batch is requeued at the head for the next attempt.On a
4xx, the batch is dropped (something is wrong with the payload).
DNT / Global Privacy Control
The SDK short-circuits everything — no fetches, no localStorage writes — when either of these is set:
Override only if you have a separate legal basis:
Cookies
None. The anonymous ID lives in localStorage under the key mm_aid. If localStorage is unavailable (private mode), the SDK falls back to an in-memory UUID that lasts for the tab's lifetime.
Bundle size
dist/a.js (CDN snippet, IIFE, minified)
~4.1 KB
Gzipped over the wire
~1.8 KB
npm ESM (tree-shakable)
similar, depends on your bundler
Examples
React
Next.js (App Router)
Vanilla HTML with explicit tracking
How a.js is served
The same Cloudflare Worker that ingests events serves the loader at GET /v1/a.js. There is no separate CDN — one Worker, one route, one DNS record (api.millimetric.ai).
Build & deploy
The predeploy hook runs scripts/build-snippet.mjs, which bundles packages/sdk-browser/src/snippet.ts with esbuild and embeds the minified IIFE into the Worker as a string. The route responds with:
Content-Type: application/javascript; charset=utf-8Cache-Control: public, max-age=300, stale-while-revalidate=86400Access-Control-Allow-Origin: *Cross-Origin-Resource-Policy: cross-origin
Point your hostname at it
In apps/api/wrangler.toml, uncomment the [[routes]] block:
With custom_domain = true, Cloudflare auto-creates a proxied DNS record on your first deploy — no manual DNS step. After that:
Last updated
Was this helpful?