first commit
This commit is contained in:
@@ -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.
|
||||
Reference in New Issue
Block a user