Initial Commit

This commit is contained in:
2026-05-27 23:29:53 +01:00
commit fbc70f96e6
10000 changed files with 1102152 additions and 0 deletions

View File

@@ -0,0 +1,186 @@
# 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<string, unknown> };
identify: { userId: string; traits?: Record<string, unknown> };
flush: { eventCount: number; success: boolean; latencyMs: number };
drop: { reason: DropReason; event?: AnalyticsEvent };
error: { error: Error; context: string };
shutdown: Record<string, never>;
}
```
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`.