4.4 KiB
tracker.ts
(no submodules)
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=0disables 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
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
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 AnalyticsTrackernew 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.