> For the complete documentation index, see [llms.txt](https://docs.millimetric.ai/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://docs.millimetric.ai/sdks/browser.md).

# Browser — @millimetric/track

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

```html
<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:

```html
<button onclick="window.mm.track('clicked_pricing', { plan: 'pro' })">
  See plans
</button>
```

### B. npm — for React / Next / Vue / Svelte / Solid

```bash
npm i @millimetric/track
```

```ts
import { init, track, identify, page } from "@millimetric/track";

init({ key: "pk_live_…" });
```

You can call `init()` more than once with the same key — it's idempotent.

## Public API

### `init(options)`

```ts
type InitOptions = {
  /** Your pk_* key. */
  key: string;
  /** Worker base URL. Defaults to same-origin. */
  host?: string;
  /** Auto-fire $pageview on init and on SPA navigation. Default true. */
  autoPageView?: boolean;
  /** Flush after this many ms even if batch isn't full. Default 2000. */
  flushIntervalMs?: number;
  /** Flush when queue reaches this size. Default 20. */
  flushBatchSize?: number;
  /** Override DNT / GPC checks. Default false. */
  ignoreOptOut?: boolean;
};
```

### `track(event, properties?)`

```ts
track("signup", { plan: "free" });
track("clicked_pricing");
```

`event` is required, 1–128 chars. `properties` is free-form JSON, capped at 8 KB after `JSON.stringify`.

### `identify(userId, traits?)`

```ts
identify(user.id, { email: user.email });
```

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).

```ts
page("checkout/payment", { step: 2 });
```

### `flush()`

```ts
await flush();
```

Forces an immediate batch send. Useful before navigating away in a controlled flow.

### `getAnonymousId()` / `setAnonymousId(id)`

```ts
const id = getAnonymousId();         // current UUID
setAnonymousId(req.cookies.aid);     // override e.g. from server-rendered HTML
```

## What's captured automatically

On every event, the SDK attaches as `properties`:

```ts
{
  utm_source, utm_medium, utm_campaign, utm_content, utm_term,
  fbclid, gclid, ttclid, msclkid, li_fat_id, dclid, gbraid, wbraid, yclid,
  $viewport_w, $viewport_h,
  $language,                  // navigator.language
  $timezone                   // Intl.DateTimeFormat().resolvedOptions().timeZone
}
```

Plus on the event itself:

```ts
{ url, path, referrer, anonymous_id, user_id, timestamp }
```

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:

```ts
init({ key, autoPageView: false });
// then call page() at your own discretion
```

## 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:

```js
navigator.doNotTrack === "1"
navigator.globalPrivacyControl === true
```

Override only if you have a separate legal basis:

```ts
init({ key, ignoreOptOut: true });
```

## 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

```tsx
import { useEffect } from "react";
import { init, track, identify } from "@millimetric/track";

export function AnalyticsRoot({ children }) {
  useEffect(() => {
    init({ key: import.meta.env.VITE_AOA_KEY });
  }, []);
  return children;
}

// later
function Pricing() {
  return (
    <button onClick={() => track("clicked_pricing", { plan: "pro" })}>
      See plans
    </button>
  );
}
```

### Next.js (App Router)

```tsx
// app/layout.tsx
"use client";
import { useEffect } from "react";
import { init } from "@millimetric/track";

export default function RootLayout({ children }) {
  useEffect(() => { init({ key: process.env.NEXT_PUBLIC_AOA_KEY! }); }, []);
  return <html>{children}</html>;
}
```

### Vanilla HTML with explicit tracking

```html
<script async
  src="https://api.millimetric.ai/v1/a.js"
  data-key="pk_live_…"
  data-host="https://api.millimetric.ai"></script>

<form onsubmit="window.mm.track('subscribed', { source: 'footer' })">
  …
</form>
```

## 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

```bash
pnpm --filter @millimetric/api 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:

```toml
[[routes]]
pattern = "api.millimetric.ai"
custom_domain = true
```

With `custom_domain = true`, Cloudflare auto-creates a proxied DNS record on your first deploy — no manual DNS step. After that:

```bash
curl -I https://api.millimetric.ai/v1/a.js
# HTTP/2 200
# content-type: application/javascript; charset=utf-8
# x-snippet-bytes: 4193
```


---

# Agent Instructions
This documentation is published with GitBook. GitBook is the documentation platform designed so that both humans and AI agents can read, navigate, and reason over technical content effectively. Learn more at gitbook.com.

## Querying This Documentation
If you need additional information that is not directly available in this page, you can query the documentation dynamically by asking a question.

Perform an HTTP GET request on the current page URL with the `ask` query parameter:

```
GET https://docs.millimetric.ai/sdks/browser.md?ask=<question>
```

The question should be specific, self-contained, and written in natural language.
The response will contain a direct answer to the question and relevant excerpts and sources from the documentation.

Use this mechanism when the answer is not explicitly present in the current page, you need clarification or additional context, or you want to retrieve related documentation sections.
