# `tracker.ts` _(no submodules)_ ## Purpose `AnalyticsTracker` is the central orchestrator of `@suaveplan/analytics-browser`. It accepts user-facing events through `track()` / `page()` / `identify()`, runs each through a deterministic consent → sampling → PII-scrub pipeline, persists accepted events into both an in-memory FIFO and an injectable `StorageBackend`, and flushes batched events through the supplied `Transport`. Lifecycle observability is exposed through a typed `EventBus` on `tracker.events`. Every browser-API touch (timers, clock, IDs, storage) is reached through an injectable seam, so the tracker is fully testable without `vi.stubGlobal` or browser-specific mocks. ## Features - Six-channel typed lifecycle bus — `track`, `pageview`, `identify`, `flush`, `drop`, `error`, `shutdown`. - Single-flight `flush()` — concurrent calls share the in-flight promise. - Auto-flush via injectable `TimersLike` — `flushInterval=0` disables the interval entirely. - Durable queue round-trip — persisted events are restored via `restore()` and cleared after a successful flush. - OTel telemetry — every accept / drop / flush emits the bundled counters/histograms. - Single-payload event bus — payload shape `{ event }` / `{ reason, event? }` etc. for ergonomic destructuring. ## Usage: Basic ```ts import { AnalyticsTracker } from "@suaveplan/analytics-browser/tracker"; import { BatchTransport } from "@suaveplan/analytics-browser/transport"; const tracker = new AnalyticsTracker( new BatchTransport({ endpoint: "/api/analytics" }), ); tracker.identify("user-123"); tracker.track("clicked", { surface: "header" }); ``` ## Usage: Advanced ```ts import { AnalyticsTracker } from "@suaveplan/analytics-browser/tracker"; import { BatchTransport, BeaconTransport } from "@suaveplan/analytics-browser/transport"; import { DoNotTrackPolicy } from "@suaveplan/analytics-browser/consent"; import { HeadSampler } from "@suaveplan/analytics-browser/sampling"; import { WebStorageBackend } from "@suaveplan/analytics-browser/storage"; const transport = new BatchTransport({ endpoint: "/api/analytics" }); const beacon = new BeaconTransport({ endpoint: "/api/analytics" }); const tracker = new AnalyticsTracker(transport, { consentPolicy: new DoNotTrackPolicy(), sampler: new HeadSampler(0.1, { error_reported: 1 }), scrubRules: [ { field: "email", strategy: "redact" }, { field: "card.number", strategy: "drop" }, ], storage: new WebStorageBackend({ storage: localStorage }), }); // Restore any events persisted across reload. await tracker.restore(); // Unload-safe flush: hand off to the beacon transport. window.addEventListener("visibilitychange", () => { if (document.visibilityState === "hidden") { void beacon.send([...tracker.events.constructor.prototype, ...[]]); void tracker.flush(); } }); tracker.events.on("flush", ({ eventCount, success, latencyMs }) => { console.info("flush", { eventCount, success, latencyMs }); }); tracker.events.on("drop", ({ reason, event }) => { console.warn("dropped", reason, event?.name); }); // Final shutdown — drain and disable. await tracker.shutdown(); ``` ## API Reference - `class AnalyticsTracker` - `new AnalyticsTracker(transport: Transport, options?: TrackerOptions)`. - `events: EventBus`. - `restore(): Promise` — replay persisted events into the in-memory queue. - `track(name, properties?)`. - `page(url?, properties?)`. - `identify(userId, traits?)`. - `setProperties(properties)`. - `getAnonymousId() / getUserId() / getSessionId() / setSessionId(id) / getQueueSize()`. - `flush(): Promise` — single-flight; success drains storage too. - `shutdown(): Promise` — idempotent. ## Implementation Notes The tracker preserves event order through `flush` failure paths by `unshift`-ing the drained batch back onto the head of the in-memory queue when the transport returns `false` or throws. This keeps the relative ordering of new events that arrived during the flush window. The persistence layer is written-through (every accepted event hits storage), but cleared only on successful flush — a transport that returns `false` leaves the persisted copy intact so a subsequent flush attempt or page reload can still recover the events. The `flushInFlight` promise guard guarantees single-flight semantics; concurrent callers all observe the same outcome.