# OpenSpec Change: enterprise-analytics-browser - **Change ID**: enterprise-analytics-browser - **Package**: `@suaveplan/analytics-browser` - **Target path**: `packages/observability/monitor/analytics-browser` - **Status**: PROPOSED - **Date**: 2026-05-25 - **Upstream reference**: `/mnt/s/web/suaveplan/enterprise/analytics-browser.md` - **Design**: ./design.md - **Spec delta**: ./specs/analytics-browser/spec.md --- ## Summary Land `@suaveplan/analytics-browser` on genesis at enterprise quality. The package provides browser-tier client-side product analytics: `AnalyticsTracker` (queue + auto-flush + identify + global props + session pin), `BatchTransport` (HTTP POST + retry + 4xx short-circuit + AbortController/timeout), `BeaconTransport` (`navigator.sendBeacon` / `fetch keepalive` for page-unload), and `SessionManager` (id + page views + duration + referrer + inactivity timeout). Adds enterprise-grade additions absent from upstream: AbortController + timeoutMs on every fetch, injectable `Transport`/`StorageBackend`/`Clock`/`TimersLike`, durable offline queue (via injected `StorageBackend` adapter — in-memory + localStorage adapters bundled, IndexedDB adapter shipped when `@suaveplan/storage-browser` lands), consent gate (`DoNotTrackPolicy` + `AllowAllPolicy` + `ConsentPolicy` SPI), PII scrubber (`scrubPII` + redact/hash/drop strategies), Zod-validated `AnalyticsEventSchema`, sampling (head-based + per-event override), shared `generateId()`, OTel counters/histograms, `ANALYTICS_BROWSER.*` error registry, and a unified `EventBus` lifecycle bus consumed via the genesis `@suaveplan/events` substrate. --- ## Current-state audit (genesis) Fresh build — does not exist on genesis. Listed `–` in `STATUS.md` line 548. ## Upstream audit - `packages/browser/analytics-browser/` upstream is 1.0.6, ~390 LOC across 4 files (tracker 161, transport 88, session 144, types 97). - Upstream consumes `EventEmitter` from `@suaveplan/events`. The genesis `@suaveplan/events` substrate exposes `EventBus` (single-payload signature) — migration translates tuple-style payloads into single-object payloads on the same `EventMap` shape. - Upstream uses `crypto.getRandomValues` for IDs duplicated across `tracker.ts` and `session.ts`. Genesis extracts to `src/internal/id/id.ts`. ## Available workspace deps on genesis | Package | Role | |---|---| | `@suaveplan/events` (`workspace:^`) | `EventBus` | | `@suaveplan/error` (`workspace:^`) | `defineCodes` / `SuaveplanError` | | `@suaveplan/types` (`workspace:^`) | `Clock`, `TimersLike`, `LoggerLike` contracts | | `@suaveplan/logger` (`workspace:^`) | optional structured logging via `LoggerLike` | | `@suaveplan/opentelemetry` (`workspace:^`) | OTel resource builder (peer; runtime via `@opentelemetry/api`) | | `@opentelemetry/api` (`catalog:observability`) | OTel meter / counters / histograms | | `zod` (`catalog:forms`) | `AnalyticsEvent` Zod schema | | `tslib` (`catalog:build`) | runtime helpers | Dev: `vitest catalog:testing`, `@vitest/coverage-istanbul catalog:testing`, `happy-dom catalog:testing`, `@suaveplan/testing workspace:^`, `@opentelemetry/sdk-metrics`. `@suaveplan/storage-browser` is NOT on Verdaccio yet — durable queue ships against an injectable `StorageBackend` SPI with bundled `MemoryStorageBackend` and `WebStorageBackend` (works against any DOM `Storage` — `localStorage` / `sessionStorage`). ## Preserved + extended public surface ```ts // Tracker export class AnalyticsTracker { readonly events: EventBus; constructor(transport: Transport, options?: Partial); track(name: string, properties?: Record): void; page(url?: string, properties?: Record): void; identify(userId: string, traits?: Record): void; setProperties(properties: Record): void; getAnonymousId(): string; getUserId(): string | undefined; getSessionId(): string; setSessionId(sessionId: string): void; getQueueSize(): number; flush(): Promise; shutdown(): Promise; } // Transports export interface Transport { send(events: AnalyticsEvent[]): Promise; shutdown(): Promise; } export class BatchTransport implements Transport { /* AbortController + timeoutMs + retry */ } export class BeaconTransport implements Transport { /* sendBeacon + keepalive fallback */ } // Session export class SessionManager { /* getSessionId / recordPageView / touch / getDuration / renew / end / getInfo */ } // Storage backends export interface StorageBackend { enqueue(events: AnalyticsEvent[]): Promise; dequeueAll(): Promise; clear(): Promise; getDepth(): Promise; } export class MemoryStorageBackend implements StorageBackend {} export class WebStorageBackend implements StorageBackend { constructor(opts: { storage: Storage; key?: string; maxBytes?: number }); } // Consent export interface ConsentPolicy { isAllowed(event: AnalyticsEvent): boolean; } export class AllowAllPolicy implements ConsentPolicy {} export class DoNotTrackPolicy implements ConsentPolicy {} // PII scrubber export type ScrubStrategy = "redact" | "hash" | "drop"; export interface ScrubRule { field: string; strategy: ScrubStrategy; } export function scrubPII(event: AnalyticsEvent, rules: readonly ScrubRule[]): AnalyticsEvent; // Sampling export interface Sampler { shouldSample(event: AnalyticsEvent): boolean; } export class HeadSampler implements Sampler { constructor(rate: number, overrides?: Record); } // Schema export const AnalyticsEventSchema: z.ZodType; // Errors export const ANALYTICS_BROWSER_CODES; export class AnalyticsBrowserError extends SuaveplanError {} // Telemetry export const eventsTrackedCounter: Counter; export const eventsFlushedCounter: Counter; export const eventsDroppedCounter: Counter; export const batchSizeHistogram: Histogram; export const flushLatencyHistogram: Histogram; ``` Subpath exports: `"."`, `"./tracker"`, `"./transport"`, `"./session"`, `"./consent"`, `"./pii"`, `"./sampling"`, `"./storage"`, `"./schema"`, `"./error-codes"`, `"./telemetry"`, `"./types"`. ## Error codes `ANALYTICS_BROWSER.INVALID_OPTIONS`, `ANALYTICS_BROWSER.TRANSPORT_TIMEOUT`, `ANALYTICS_BROWSER.TRANSPORT_FAILED`, `ANALYTICS_BROWSER.QUEUE_FULL`, `ANALYTICS_BROWSER.STORAGE_UNAVAILABLE`, `ANALYTICS_BROWSER.SHUTDOWN`, `ANALYTICS_BROWSER.INVALID_EVENT`, `ANALYTICS_BROWSER.BEACON_REJECTED`. ## OTel instruments - Counter `analytics_browser.events_tracked_total{name}` - Counter `analytics_browser.events_flushed_total{status}` (`success` / `failure`) - Counter `analytics_browser.events_dropped_total{reason}` (`consent`, `sampling`, `queue_full`, `shutdown`, `invalid`) - Histogram `analytics_browser.batch_size` - Histogram `analytics_browser.flush_latency_ms` ## Impact ### Affected dependents No dependents on genesis yet (verified by `bunx turbo ls --filter=...@suaveplan/analytics-browser`). Downstream: React SPA shells, R3F apps. New code paths are additive — existing instantiation `new AnalyticsTracker(transport)` continues to work. ### Breaking changes None. ### Migration notes Upstream API (`new AnalyticsTracker(transport, config)`) preserved verbatim with one signature evolution: the lifecycle bus is `EventBus` instead of `EventEmitter`. The map's payload shape changes from tuple `[event]` to single-object `{ event }` — call sites change `.on("track", (event) => …)` to `.on("track", ({ event }) => …)`. This is the only mechanical change required by the genesis `@suaveplan/events` substrate.