first commit

This commit is contained in:
2026-05-29 08:20:56 +01:00
commit 949299f58d
6368 changed files with 527176 additions and 0 deletions

View File

@@ -0,0 +1,95 @@
# `tracker.ts`
<!-- begin:auto-docs -->
_(no submodules)_
<!-- end:auto-docs -->
## Purpose
`AnalyticsTracker` is the central orchestrator of `@suaveplan/analytics-browser`. It accepts user-facing events through `track()` / `page()` / `identify()`, runs each through a deterministic consent → sampling → PII-scrub pipeline, persists accepted events into both an in-memory FIFO and an injectable `StorageBackend`, and flushes batched events through the supplied `Transport`. Lifecycle observability is exposed through a typed `EventBus<AnalyticsEventMap>` on `tracker.events`. Every browser-API touch (timers, clock, IDs, storage) is reached through an injectable seam, so the tracker is fully testable without `vi.stubGlobal` or browser-specific mocks.
## Features
- Six-channel typed lifecycle bus — `track`, `pageview`, `identify`, `flush`, `drop`, `error`, `shutdown`.
- Single-flight `flush()` — concurrent calls share the in-flight promise.
- Auto-flush via injectable `TimersLike` — `flushInterval=0` disables the interval entirely.
- Durable queue round-trip — persisted events are restored via `restore()` and cleared after a successful flush.
- OTel telemetry — every accept / drop / flush emits the bundled counters/histograms.
- Single-payload event bus — payload shape `{ event }` / `{ reason, event? }` etc. for ergonomic destructuring.
## Usage: Basic
```ts
import { AnalyticsTracker } from "@suaveplan/analytics-browser/tracker";
import { BatchTransport } from "@suaveplan/analytics-browser/transport";
const tracker = new AnalyticsTracker(
new BatchTransport({ endpoint: "/api/analytics" }),
);
tracker.identify("user-123");
tracker.track("clicked", { surface: "header" });
```
## Usage: Advanced
```ts
import { AnalyticsTracker } from "@suaveplan/analytics-browser/tracker";
import { BatchTransport, BeaconTransport } from "@suaveplan/analytics-browser/transport";
import { DoNotTrackPolicy } from "@suaveplan/analytics-browser/consent";
import { HeadSampler } from "@suaveplan/analytics-browser/sampling";
import { WebStorageBackend } from "@suaveplan/analytics-browser/storage";
const transport = new BatchTransport({ endpoint: "/api/analytics" });
const beacon = new BeaconTransport({ endpoint: "/api/analytics" });
const tracker = new AnalyticsTracker(transport, {
consentPolicy: new DoNotTrackPolicy(),
sampler: new HeadSampler(0.1, { error_reported: 1 }),
scrubRules: [
{ field: "email", strategy: "redact" },
{ field: "card.number", strategy: "drop" },
],
storage: new WebStorageBackend({ storage: localStorage }),
});
// Restore any events persisted across reload.
await tracker.restore();
// Unload-safe flush: hand off to the beacon transport.
window.addEventListener("visibilitychange", () => {
if (document.visibilityState === "hidden") {
void beacon.send([...tracker.events.constructor.prototype, ...[]]);
void tracker.flush();
}
});
tracker.events.on("flush", ({ eventCount, success, latencyMs }) => {
console.info("flush", { eventCount, success, latencyMs });
});
tracker.events.on("drop", ({ reason, event }) => {
console.warn("dropped", reason, event?.name);
});
// Final shutdown — drain and disable.
await tracker.shutdown();
```
## API Reference
- `class AnalyticsTracker`
- `new AnalyticsTracker(transport: Transport, options?: TrackerOptions)`.
- `events: EventBus<AnalyticsEventMap>`.
- `restore(): Promise<void>` — replay persisted events into the in-memory queue.
- `track(name, properties?)`.
- `page(url?, properties?)`.
- `identify(userId, traits?)`.
- `setProperties(properties)`.
- `getAnonymousId() / getUserId() / getSessionId() / setSessionId(id) / getQueueSize()`.
- `flush(): Promise<boolean>` — single-flight; success drains storage too.
- `shutdown(): Promise<void>` — idempotent.
## Implementation Notes
The tracker preserves event order through `flush` failure paths by `unshift`-ing the drained batch back onto the head of the in-memory queue when the transport returns `false` or throws. This keeps the relative ordering of new events that arrived during the flush window. The persistence layer is written-through (every accepted event hits storage), but cleared only on successful flush — a transport that returns `false` leaves the persisted copy intact so a subsequent flush attempt or page reload can still recover the events. The `flushInFlight` promise guard guarantees single-flight semantics; concurrent callers all observe the same outcome.