187 lines
6.6 KiB
Markdown
187 lines
6.6 KiB
Markdown
# 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`.
|