For the complete documentation index, see llms.txt. This page is also available as Markdown.

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 $pageview on load and on every SPA navigation.

  • Exposes a global window.mm you 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 calls navigator.sendBeacon so in-flight events survive page exits.

  • On a 5xx response, 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

Build
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-8

  • Cache-Control: public, max-age=300, stale-while-revalidate=86400

  • Access-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?