Files
2026-05-27 23:47:53 +01:00
..
2026-05-27 23:47:53 +01:00
2026-05-27 23:47:53 +01:00
2026-05-27 23:47:53 +01:00
2026-05-27 23:47:53 +01:00
2026-05-27 23:47:53 +01:00
2026-05-27 23:47:53 +01:00
2026-05-27 23:47:53 +01:00

@suaveplan/analytics-browser - Browser-tier client-side product analytics: AnalyticsTracker / BatchTransport / BeaconTransport / SessionManager with consent gate, PII scrubber, head-sampling, durable StorageBackend (memory + Web Storage), Zod-validated events, AbortController + timeoutMs on every fetch, OTel counters + histograms, and a typed EventBus lifecycle bus on @suaveplan/events.

Browser-tier client-side product analytics — AnalyticsTracker, BatchTransport, BeaconTransport, and SessionManager with consent gate, PII scrubber, head sampling, durable storage, Zod-validated events, AbortController + timeoutMs on every fetch, and OpenTelemetry counters / histograms. Lifecycle observability is exposed through the @suaveplan/events EventBus.

Documentation tree

  • consent — Implements the ConsentPolicy SPI bundled with @suaveplan/analytics-browser. The tracker calls `policy.isAllowed(eve…
    • consent — Implements the ConsentPolicy SPI bundled with @suaveplan/analytics-browser. The tracker calls `policy.isAllowed(eve…
  • error-codes — Defines the stable error-code registry and typed error class for @suaveplan/analytics-browser. The registry uses `@su…
    • error-codes — Defines the stable error-code registry and typed error class for @suaveplan/analytics-browser. The registry uses `@su…
  • id — Internal cryptographically-random ID generator shared by the analytics tracker, the session manager, and any consumer t…
    • id — Internal cryptographically-random ID generator shared by the analytics tracker, the session manager, and any consumer t…
  • pii — Implements field-level PII scrubbing for @suaveplan/analytics-browser. scrubPII(event, rules) takes an `AnalyticsEv…
    • pii — Implements field-level PII scrubbing for @suaveplan/analytics-browser. scrubPII(event, rules) takes an `AnalyticsEv…
  • sampling — Implements head-based sampling for @suaveplan/analytics-browser. The tracker calls sampler.shouldSample(event) afte…
    • sampling — Implements head-based sampling for @suaveplan/analytics-browser. The tracker calls sampler.shouldSample(event) afte…
  • schema — Defines the canonical Zod schema for AnalyticsEvent and a validateAnalyticsEvent parse helper. The schema lets cons…
    • analytics-event-schema — Defines the canonical Zod schema for AnalyticsEvent and a validateAnalyticsEvent parse helper. The schema lets cons…
  • session — Tracks the lifecycle of a single browser session for @suaveplan/analytics-browser. The SessionManager owns the sess…
    • session — Tracks the lifecycle of a single browser session for @suaveplan/analytics-browser. The SessionManager owns the sess…
  • storage — Implements the StorageBackend SPI bundled with @suaveplan/analytics-browser. Two backends ship: `MemoryStorageBacke…
    • storage — Implements the StorageBackend SPI bundled with @suaveplan/analytics-browser. Two backends ship: `MemoryStorageBacke…
  • telemetry — Defines the OpenTelemetry instruments emitted by @suaveplan/analytics-browser. Three counters cover the accept / flus…
    • telemetry — Defines the OpenTelemetry instruments emitted by @suaveplan/analytics-browser. Three counters cover the accept / flus…
  • tracker — AnalyticsTracker is the central orchestrator of @suaveplan/analytics-browser. It accepts user-facing events through…
    • tracker — AnalyticsTracker is the central orchestrator of @suaveplan/analytics-browser. It accepts user-facing events through…
  • batch — Implements BatchTransport — the HTTP transport used by AnalyticsTracker for the steady-state, non-unload flush path…
  • beacon — Implements BeaconTransport — the page-unload-safe transport used by AnalyticsTracker.shutdown() and any consumer wi…

Install

bun add @suaveplan/analytics-browser

Quick start

import { AnalyticsTracker, BatchTransport } from "@suaveplan/analytics-browser";

const tracker = new AnalyticsTracker(
    new BatchTransport({ endpoint: "/api/analytics" }),
);

tracker.identify("user-123");
tracker.track("clicked", { surface: "header" });
await tracker.flush();

Subpath exports

Path Contents
@suaveplan/analytics-browser Everything (single barrel).
@suaveplan/analytics-browser/tracker AnalyticsTracker.
@suaveplan/analytics-browser/transport BatchTransport, BeaconTransport.
@suaveplan/analytics-browser/session SessionManager.
@suaveplan/analytics-browser/consent AllowAllPolicy, DoNotTrackPolicy.
@suaveplan/analytics-browser/pii scrubPII.
@suaveplan/analytics-browser/sampling HeadSampler.
@suaveplan/analytics-browser/storage MemoryStorageBackend, WebStorageBackend.
@suaveplan/analytics-browser/schema AnalyticsEventSchema, validateAnalyticsEvent.
@suaveplan/analytics-browser/error-codes ANALYTICS_BROWSER_CODES, AnalyticsBrowserError.
@suaveplan/analytics-browser/telemetry OTel counters / histograms + helpers.
@suaveplan/analytics-browser/types Type-only exports.

The tracker calls consentPolicy.isAllowed(event) before sampling, scrubbing, or queueing. Two built-ins ship:

  • AllowAllPolicy — permits every event (default).
  • DoNotTrackPolicy — honours navigator.doNotTrack === "1".
import { AnalyticsTracker } from "@suaveplan/analytics-browser/tracker";
import { DoNotTrackPolicy } from "@suaveplan/analytics-browser/consent";

const tracker = new AnalyticsTracker(transport, {
    consentPolicy: new DoNotTrackPolicy(),
});

Implement ConsentPolicy yourself to honour a cookie banner / IAB TCF / regional toggle.

PII scrubber

import { AnalyticsTracker } from "@suaveplan/analytics-browser/tracker";

const tracker = new AnalyticsTracker(transport, {
    scrubRules: [
        { field: "email", strategy: "redact" }, // → "[REDACTED]"
        { field: "phone", strategy: "hash" },   // → 8-char FNV-1a hex
        { field: "card.number", strategy: "drop" },
    ],
});

Dotted field paths walk into nested properties objects. The FNV-1a fingerprint is deterministic across calls — useful for cross-event correlation without revealing the value.

Sampling

import { HeadSampler } from "@suaveplan/analytics-browser/sampling";

const sampler = new HeadSampler(0.1, {
    error_reported: 1, // always keep errors
    clicked: 1,        // always keep clicks
});

HeadSampler clamps rates to [0, 1]; NaN clamps to 0. The constructor accepts an injectable random: () => number so deterministic tests are trivial.

Durable storage

import { AnalyticsTracker } from "@suaveplan/analytics-browser/tracker";
import { WebStorageBackend } from "@suaveplan/analytics-browser/storage";

const tracker = new AnalyticsTracker(transport, {
    storage: new WebStorageBackend({
        storage: localStorage,
        key: "@my-app/analytics",
        maxBytes: 1_048_576,
    }),
});

// Restore events persisted across reload.
await tracker.restore();

The bundled backends are MemoryStorageBackend (default) and WebStorageBackend. An IndexedDB adapter will slot into the same SPI once @suaveplan/storage-browser lands; the contract (enqueue / dequeueAll / clear / getDepth) will not change.

Unload-safe flushing

import { BeaconTransport } from "@suaveplan/analytics-browser/transport";

const beacon = new BeaconTransport({
    endpoint: "/api/analytics",
    fallbackToFetch: true,
});

window.addEventListener("visibilitychange", () => {
    if (document.visibilityState === "hidden") {
        void tracker.flush();
    }
});

window.addEventListener("pagehide", () => {
    void tracker.flush();
});

BeaconTransport uses navigator.sendBeacon first and optionally falls back to fetch({ keepalive: true }). When the host rejects the beacon and fallback is disabled, the transport throws AnalyticsBrowserError(BEACON_REJECTED).

Lifecycle event bus

tracker.events.on("track", ({ event }) => {});
tracker.events.on("flush", ({ eventCount, success, latencyMs }) => {});
tracker.events.on("drop", ({ reason, event }) => {});
tracker.events.on("error", ({ error, context }) => {});
tracker.events.on("shutdown", () => {});

The bus is a real @suaveplan/events EventBus<AnalyticsEventMap> — every payload is a single object (not a tuple).

OpenTelemetry

@suaveplan/analytics-browser declares five OTel instruments under the meter name @suaveplan/analytics-browser:

Instrument Kind Tags
analytics_browser.events_tracked_total counter name
analytics_browser.events_flushed_total counter status (success / failure)
analytics_browser.events_dropped_total counter reason (consent / sampling / queue_full / shutdown / invalid)
analytics_browser.batch_size histogram —
analytics_browser.flush_latency_ms histogram — (ms)

No setup is required — the instruments are no-ops when no MeterProvider is registered.

Migration from upstream @suaveplan/analytics-browser@1.0.6

The only behavioural change is the lifecycle bus shape. Upstream used EventEmitter with tuple payloads:

// upstream
tracker.events.on("track", (event) => {});
tracker.events.on("flush", (count, success) => {});

uses EventBus with single-object payloads:

// suaveplan
tracker.events.on("track", ({ event }) => {});
tracker.events.on("flush", ({ eventCount, success, latencyMs }) => {});

Constructor signature, public method names, and behavioural semantics of track / identify / flush / shutdown are preserved.