Files
skills/docs/observability/monitor/analytics-browser/src/tracker/tracker.md
2026-05-29 08:20:56 +01:00

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=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

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 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.