Files
2026-05-27 23:29:53 +01:00

144 lines
7.6 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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<AnalyticsEventMap>` 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<AnalyticsEventMap>` from `@suaveplan/events`. The genesis `@suaveplan/events` substrate exposes `EventBus<TMap>` (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<AnalyticsEventMap>` |
| `@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<AnalyticsEventMap>;
constructor(transport: Transport, options?: Partial<TrackerOptions>);
track(name: string, properties?: Record<string, unknown>): void;
page(url?: string, properties?: Record<string, unknown>): void;
identify(userId: string, traits?: Record<string, unknown>): void;
setProperties(properties: Record<string, unknown>): void;
getAnonymousId(): string;
getUserId(): string | undefined;
getSessionId(): string;
setSessionId(sessionId: string): void;
getQueueSize(): number;
flush(): Promise<boolean>;
shutdown(): Promise<void>;
}
// Transports
export interface Transport {
send(events: AnalyticsEvent[]): Promise<boolean>;
shutdown(): Promise<void>;
}
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<void>;
dequeueAll(): Promise<AnalyticsEvent[]>;
clear(): Promise<void>;
getDepth(): Promise<number>;
}
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<string, number>); }
// Schema
export const AnalyticsEventSchema: z.ZodType<AnalyticsEvent>;
// Errors
export const ANALYTICS_BROWSER_CODES;
export class AnalyticsBrowserError extends SuaveplanError<AnalyticsBrowserCode> {}
// 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<AnalyticsEventMap>` 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.