# @suaveplan/analytics-browser - Browser-tier client-side product analytics: AnalyticsTracker / BatchTransport / BeaconTransport / SessionManager with consent gate, PII scrubber, head-sampling, durable StorageBackend (memory + Web Storage), Zod-validated events, AbortController + timeoutMs on every fetch, OTel counters + histograms, and a typed EventBus lifecycle bus on @suaveplan/events. Browser-tier client-side product analytics — `AnalyticsTracker`, `BatchTransport`, `BeaconTransport`, and `SessionManager` with consent gate, PII scrubber, head sampling, durable storage, Zod-validated events, AbortController + timeoutMs on every fetch, and OpenTelemetry counters / histograms. Lifecycle observability is exposed through the `@suaveplan/events` `EventBus`. ## Documentation tree - [consent](./src/consent/consent.md) — Implements the `ConsentPolicy` SPI bundled with `@suaveplan/analytics-browser`. The tracker calls `policy.isAllowed(eve… - [consent](./src/consent/consent.md) — Implements the `ConsentPolicy` SPI bundled with `@suaveplan/analytics-browser`. The tracker calls `policy.isAllowed(eve… - [error-codes](./src/error-codes/error-codes.md) — Defines the stable error-code registry and typed error class for `@suaveplan/analytics-browser`. The registry uses `@su… - [error-codes](./src/error-codes/error-codes.md) — Defines the stable error-code registry and typed error class for `@suaveplan/analytics-browser`. The registry uses `@su… - [id](./src/internal/id/id.md) — Internal cryptographically-random ID generator shared by the analytics tracker, the session manager, and any consumer t… - [id](./src/internal/id/id.md) — Internal cryptographically-random ID generator shared by the analytics tracker, the session manager, and any consumer t… - [pii](./src/pii/pii.md) — Implements field-level PII scrubbing for `@suaveplan/analytics-browser`. `scrubPII(event, rules)` takes an `AnalyticsEv… - [pii](./src/pii/pii.md) — Implements field-level PII scrubbing for `@suaveplan/analytics-browser`. `scrubPII(event, rules)` takes an `AnalyticsEv… - [sampling](./src/sampling/sampling.md) — Implements head-based sampling for `@suaveplan/analytics-browser`. The tracker calls `sampler.shouldSample(event)` afte… - [sampling](./src/sampling/sampling.md) — Implements head-based sampling for `@suaveplan/analytics-browser`. The tracker calls `sampler.shouldSample(event)` afte… - [schema](./src/schema/analytics-event-schema.md) — Defines the canonical Zod schema for `AnalyticsEvent` and a `validateAnalyticsEvent` parse helper. The schema lets cons… - [analytics-event-schema](./src/schema/analytics-event-schema.md) — Defines the canonical Zod schema for `AnalyticsEvent` and a `validateAnalyticsEvent` parse helper. The schema lets cons… - [session](./src/session/session.md) — Tracks the lifecycle of a single browser session for `@suaveplan/analytics-browser`. The `SessionManager` owns the sess… - [session](./src/session/session.md) — Tracks the lifecycle of a single browser session for `@suaveplan/analytics-browser`. The `SessionManager` owns the sess… - [storage](./src/storage/storage.md) — Implements the `StorageBackend` SPI bundled with `@suaveplan/analytics-browser`. Two backends ship: `MemoryStorageBacke… - [storage](./src/storage/storage.md) — Implements the `StorageBackend` SPI bundled with `@suaveplan/analytics-browser`. Two backends ship: `MemoryStorageBacke… - [telemetry](./src/telemetry/telemetry.md) — Defines the OpenTelemetry instruments emitted by `@suaveplan/analytics-browser`. Three counters cover the accept / flus… - [telemetry](./src/telemetry/telemetry.md) — Defines the OpenTelemetry instruments emitted by `@suaveplan/analytics-browser`. Three counters cover the accept / flus… - [tracker](./src/tracker/tracker.md) — `AnalyticsTracker` is the central orchestrator of `@suaveplan/analytics-browser`. It accepts user-facing events through… - [tracker](./src/tracker/tracker.md) — `AnalyticsTracker` is the central orchestrator of `@suaveplan/analytics-browser`. It accepts user-facing events through… - [batch](./src/transport/batch.md) — Implements `BatchTransport` — the HTTP transport used by `AnalyticsTracker` for the steady-state, non-unload flush path… - [beacon](./src/transport/beacon.md) — Implements `BeaconTransport` — the page-unload-safe transport used by `AnalyticsTracker.shutdown()` and any consumer wi… ## Install ```bash bun add @suaveplan/analytics-browser ``` ## Quick start ```ts import { AnalyticsTracker, BatchTransport } from "@suaveplan/analytics-browser"; const tracker = new AnalyticsTracker( new BatchTransport({ endpoint: "/api/analytics" }), ); tracker.identify("user-123"); tracker.track("clicked", { surface: "header" }); await tracker.flush(); ``` ## Subpath exports | Path | Contents | |---|---| | `@suaveplan/analytics-browser` | Everything (single barrel). | | `@suaveplan/analytics-browser/tracker` | `AnalyticsTracker`. | | `@suaveplan/analytics-browser/transport` | `BatchTransport`, `BeaconTransport`. | | `@suaveplan/analytics-browser/session` | `SessionManager`. | | `@suaveplan/analytics-browser/consent` | `AllowAllPolicy`, `DoNotTrackPolicy`. | | `@suaveplan/analytics-browser/pii` | `scrubPII`. | | `@suaveplan/analytics-browser/sampling` | `HeadSampler`. | | `@suaveplan/analytics-browser/storage` | `MemoryStorageBackend`, `WebStorageBackend`. | | `@suaveplan/analytics-browser/schema` | `AnalyticsEventSchema`, `validateAnalyticsEvent`. | | `@suaveplan/analytics-browser/error-codes` | `ANALYTICS_BROWSER_CODES`, `AnalyticsBrowserError`. | | `@suaveplan/analytics-browser/telemetry` | OTel counters / histograms + helpers. | | `@suaveplan/analytics-browser/types` | Type-only exports. | ## Consent gate The tracker calls `consentPolicy.isAllowed(event)` before sampling, scrubbing, or queueing. Two built-ins ship: - `AllowAllPolicy` — permits every event (default). - `DoNotTrackPolicy` — honours `navigator.doNotTrack === "1"`. ```ts import { AnalyticsTracker } from "@suaveplan/analytics-browser/tracker"; import { DoNotTrackPolicy } from "@suaveplan/analytics-browser/consent"; const tracker = new AnalyticsTracker(transport, { consentPolicy: new DoNotTrackPolicy(), }); ``` Implement `ConsentPolicy` yourself to honour a cookie banner / IAB TCF / regional toggle. ## PII scrubber ```ts import { AnalyticsTracker } from "@suaveplan/analytics-browser/tracker"; const tracker = new AnalyticsTracker(transport, { scrubRules: [ { field: "email", strategy: "redact" }, // → "[REDACTED]" { field: "phone", strategy: "hash" }, // → 8-char FNV-1a hex { field: "card.number", strategy: "drop" }, ], }); ``` Dotted field paths walk into nested `properties` objects. The FNV-1a fingerprint is deterministic across calls — useful for cross-event correlation without revealing the value. ## Sampling ```ts import { HeadSampler } from "@suaveplan/analytics-browser/sampling"; const sampler = new HeadSampler(0.1, { error_reported: 1, // always keep errors clicked: 1, // always keep clicks }); ``` `HeadSampler` clamps rates to `[0, 1]`; `NaN` clamps to `0`. The constructor accepts an injectable `random: () => number` so deterministic tests are trivial. ## Durable storage ```ts import { AnalyticsTracker } from "@suaveplan/analytics-browser/tracker"; import { WebStorageBackend } from "@suaveplan/analytics-browser/storage"; const tracker = new AnalyticsTracker(transport, { storage: new WebStorageBackend({ storage: localStorage, key: "@my-app/analytics", maxBytes: 1_048_576, }), }); // Restore events persisted across reload. await tracker.restore(); ``` The bundled backends are `MemoryStorageBackend` (default) and `WebStorageBackend`. An IndexedDB adapter will slot into the same SPI once `@suaveplan/storage-browser` lands; the contract (`enqueue` / `dequeueAll` / `clear` / `getDepth`) will not change. ## Unload-safe flushing ```ts import { BeaconTransport } from "@suaveplan/analytics-browser/transport"; const beacon = new BeaconTransport({ endpoint: "/api/analytics", fallbackToFetch: true, }); window.addEventListener("visibilitychange", () => { if (document.visibilityState === "hidden") { void tracker.flush(); } }); window.addEventListener("pagehide", () => { void tracker.flush(); }); ``` `BeaconTransport` uses `navigator.sendBeacon` first and optionally falls back to `fetch({ keepalive: true })`. When the host rejects the beacon and fallback is disabled, the transport throws `AnalyticsBrowserError(BEACON_REJECTED)`. ## Lifecycle event bus ```ts tracker.events.on("track", ({ event }) => {}); tracker.events.on("flush", ({ eventCount, success, latencyMs }) => {}); tracker.events.on("drop", ({ reason, event }) => {}); tracker.events.on("error", ({ error, context }) => {}); tracker.events.on("shutdown", () => {}); ``` The bus is a real `@suaveplan/events` `EventBus` — every payload is a single object (not a tuple). ## OpenTelemetry `@suaveplan/analytics-browser` declares five OTel instruments under the meter name `@suaveplan/analytics-browser`: | Instrument | Kind | Tags | |---|---|---| | `analytics_browser.events_tracked_total` | counter | `name` | | `analytics_browser.events_flushed_total` | counter | `status` (`success` / `failure`) | | `analytics_browser.events_dropped_total` | counter | `reason` (`consent` / `sampling` / `queue_full` / `shutdown` / `invalid`) | | `analytics_browser.batch_size` | histogram | — | | `analytics_browser.flush_latency_ms` | histogram | — (`ms`) | No setup is required — the instruments are no-ops when no `MeterProvider` is registered. ## Migration from upstream `@suaveplan/analytics-browser@1.0.6` The only behavioural change is the lifecycle bus shape. Upstream used `EventEmitter` with tuple payloads: ```ts // upstream tracker.events.on("track", (event) => {}); tracker.events.on("flush", (count, success) => {}); ``` uses `EventBus` with single-object payloads: ```ts // suaveplan tracker.events.on("track", ({ event }) => {}); tracker.events.on("flush", ({ eventCount, success, latencyMs }) => {}); ``` Constructor signature, public method names, and behavioural semantics of `track` / `identify` / `flush` / `shutdown` are preserved.