Relocate the ~6k-file docs tree from the repo root into plugins/document-navigator/docs/ so the plugin is self-contained and portable. docs/superpowers/ (specs and plans) stays at the repo root. Moved via cp+rm rather than git mv because directory rename() fails with EPERM on this WSL /mnt DrvFs mount; git rename-detection pairs the identical-content files at commit time. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
10 KiB
@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
ConsentPolicySPI bundled with@suaveplan/analytics-browser. The tracker calls `policy.isAllowed(eve…- consent — Implements the
ConsentPolicySPI bundled with@suaveplan/analytics-browser. The tracker calls `policy.isAllowed(eve…
- consent — Implements the
- 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…
- error-codes — Defines the stable error-code registry and typed error class for
- 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…
- pii — Implements field-level PII scrubbing for
- sampling — Implements head-based sampling for
@suaveplan/analytics-browser. The tracker callssampler.shouldSample(event)afte…- sampling — Implements head-based sampling for
@suaveplan/analytics-browser. The tracker callssampler.shouldSample(event)afte…
- sampling — Implements head-based sampling for
- schema — Defines the canonical Zod schema for
AnalyticsEventand avalidateAnalyticsEventparse helper. The schema lets cons…- analytics-event-schema — Defines the canonical Zod schema for
AnalyticsEventand avalidateAnalyticsEventparse helper. The schema lets cons…
- analytics-event-schema — Defines the canonical Zod schema for
- session — Tracks the lifecycle of a single browser session for
@suaveplan/analytics-browser. TheSessionManagerowns the sess…- session — Tracks the lifecycle of a single browser session for
@suaveplan/analytics-browser. TheSessionManagerowns the sess…
- session — Tracks the lifecycle of a single browser session for
- storage — Implements the
StorageBackendSPI bundled with@suaveplan/analytics-browser. Two backends ship: `MemoryStorageBacke…- storage — Implements the
StorageBackendSPI bundled with@suaveplan/analytics-browser. Two backends ship: `MemoryStorageBacke…
- storage — Implements the
- 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…
- telemetry — Defines the OpenTelemetry instruments emitted by
- tracker —
AnalyticsTrackeris the central orchestrator of@suaveplan/analytics-browser. It accepts user-facing events through…- tracker —
AnalyticsTrackeris the central orchestrator of@suaveplan/analytics-browser. It accepts user-facing events through…
- tracker —
- batch — Implements
BatchTransport— the HTTP transport used byAnalyticsTrackerfor the steady-state, non-unload flush path… - beacon — Implements
BeaconTransport— the page-unload-safe transport used byAnalyticsTracker.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. |
Consent gate
The tracker calls consentPolicy.isAllowed(event) before sampling, scrubbing, or queueing. Two built-ins ship:
AllowAllPolicy— permits every event (default).DoNotTrackPolicy— honoursnavigator.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.