# Design: enterprise-analytics-browser ## Tier Browser-tier. `package.json` sets `"browser"` field; no `node:` imports in `src/`. Uses `crypto.getRandomValues` (Web Crypto), `navigator.sendBeacon`, DOM `Storage`, `fetch`, `setInterval`/`setTimeout`. All DOM-API touches are funnelled through narrow injectable seams so tests run under either `happy-dom` (default vitest env) or a synthetic adapter. ## Module layout ``` packages/observability/monitor/analytics-browser/ package.json tsconfig.lib.json vite.config.ts vitest.config.ts README.md src/ index.ts (barrel — explicit named re-exports) error-codes/ error-codes.ts error-codes.test.ts error-codes.md index.ts types/ analytics.types.ts analytics.types.md constants/ index.ts internal/ id/ id.ts id.test.ts id.md index.ts schema/ analytics-event-schema.ts analytics-event-schema.test.ts analytics-event-schema.md index.ts consent/ consent.ts consent.test.ts consent.md index.ts pii/ pii.ts pii.test.ts pii.md index.ts sampling/ sampling.ts sampling.test.ts sampling.md index.ts storage/ storage.ts storage.test.ts storage.md index.ts session/ session.ts session.test.ts session.md index.ts transport/ batch.ts batch.test.ts batch.md beacon.ts beacon.test.ts beacon.md index.ts tracker/ tracker.ts tracker.test.ts tracker.md index.ts telemetry/ telemetry.ts telemetry.test.ts telemetry.md index.ts tests/ setup/ polyfills.ts integration.test.ts ``` ## Injectable seams The tracker has zero hard DOM dependencies. Every browser API is reached through an injected adapter: - `Clock` (`@suaveplan/types`) — `now() → number` for timestamps. - `TimersLike` (`@suaveplan/types`) — `setInterval` / `clearInterval` / `setTimeout` / `clearTimeout`. - `Transport` — produces side effects (`fetch` in `BatchTransport`, `sendBeacon` in `BeaconTransport`); tests pass a synchronous fake or an in-process HTTP route. - `StorageBackend` — durable queue persistence. - `ConsentPolicy` — gate decision; defaults to `AllowAllPolicy`. - `Sampler` — sampling decision; default `HeadSampler(1.0)` (sample everything). - `IdGenerator` — for anonymous / session IDs; defaults to web-crypto `generateId()`. Beacon transport accepts an injected `sendBeacon` function (defaults to `navigator.sendBeacon.bind(navigator)`) and a fetch fallback so tests can drive the path without `happy-dom` quirks. ## Event-bus shape ```ts export interface AnalyticsEventMap extends EventMap { track: { event: AnalyticsEvent }; pageview: { url: string; properties: Record }; identify: { userId: string; traits?: Record }; flush: { eventCount: number; success: boolean; latencyMs: number }; drop: { reason: DropReason; event?: AnalyticsEvent }; error: { error: Error; context: string }; shutdown: Record; } ``` Single-payload object on every channel — matches `EventBus.emit(type, payload)`. ## Durable queue `AnalyticsTracker` writes every accepted event into both the in-memory FIFO and the injected `StorageBackend`. On flush success, both copies clear together. On flush failure, in-memory copy is restored; storage copy stays. On boot, `restore()` drains the storage backend back into the in-memory queue. Bundled backends: - `MemoryStorageBackend` — Map-based, default; suitable for SSR or tests. - `WebStorageBackend` — DOM `Storage` (`localStorage` / `sessionStorage`); JSON-encoded; `maxBytes` watermark with `QUEUE_FULL` ejection. `IndexedDBQueueStorage` is intentionally out of scope until `@suaveplan/storage-browser` lands; the SPI is shaped so an IndexedDB adapter slots in without API churn. ## Consent + sampling + PII pipeline ```text track(name, props) └─ build AnalyticsEvent └─ ConsentPolicy.isAllowed(event) ─── false → drop{reason:"consent"}, counter, return └─ Sampler.shouldSample(event) ─ false → drop{reason:"sampling"}, counter, return └─ scrubPII(event, rules) ── applied if rules present └─ StorageBackend.enqueue + in-memory queue.push └─ events.emit("track", { event }) └─ counter increment ``` ## Transport `BatchTransport.send(events)`: 1. Build `{ events }` JSON body. 2. Spawn `AbortController`; arm with `setTimeoutMs` (default 10_000 ms). 3. `await fetch(endpoint, { method:"POST", headers, body, signal })`. 4. `response.ok` → success; `4xx` → `TRANSPORT_FAILED` (no retry); timeout → `TRANSPORT_TIMEOUT`; `5xx` / network → exponential backoff (`retryDelay * 2 ** attempt`, capped at `maxRetries`). `BeaconTransport.send(events)`: 1. `navigator.sendBeacon(endpoint, blob)` — returns boolean. 2. If `false` and `fallbackToFetch=true` → `fetch(endpoint, { method:"POST", keepalive:true, body })`. ## OTel telemetry Uses `@opentelemetry/api` `metrics.getMeter("@suaveplan/analytics-browser")`. Lazy-bound so no provider → no-op meter (no setup required for production). Tests install an `InMemoryMetricExporter` via `tests/setup/polyfills.ts`. ## Error registry `defineCodes("ANALYTICS_BROWSER", ["INVALID_OPTIONS", "TRANSPORT_TIMEOUT", "TRANSPORT_FAILED", "QUEUE_FULL", "STORAGE_UNAVAILABLE", "SHUTDOWN", "INVALID_EVENT", "BEACON_REJECTED"] as const)`. ## Tier purity rules - No `node:` import anywhere in `src/`. - All randomness via `crypto.getRandomValues` — fail-safe via Web Crypto polyfill in tests. - No top-level `navigator.*` access at module load — only inside method bodies guarded by `typeof navigator !== "undefined"`. ## File-size budget - `tracker.ts` upstream is 161 LOC. Genesis adds queue persistence + consent + sampling + telemetry hooks — projected ~220 LOC, well within the 300 hard cap. - `transport/batch.ts` ~120 LOC. - `transport/beacon.ts` ~70 LOC. - `storage/storage.ts` ~140 LOC (two backends co-located). - `session/session.ts` ~150 LOC. - All test files ≤ 500 LOC. ## Test strategy - Real `@suaveplan/events` `EventBus` — no mocks. - Real `happy-dom` for DOM globals. - `fetch` injected as a real in-process handler (records call + returns `Response`). - `setInterval`/`clearInterval` driven by a `FakeTimers` adapter from `@suaveplan/testing`. - OTel `InMemoryMetricExporter` for assertion of every counter / histogram. - 100/100/100/100 coverage gate enforced via `vitest.config.ts` `thresholds`.