Initial Commit

This commit is contained in:
2026-05-27 23:47:53 +01:00
parent fbc70f96e6
commit 9b28db724f
18103 changed files with 1600554 additions and 0 deletions

View File

@@ -0,0 +1,53 @@
# `consent.ts`
<!-- begin:auto-docs -->
_(no submodules)_
<!-- end:auto-docs -->
## Purpose
Implements the `ConsentPolicy` SPI bundled with `@suaveplan/analytics-browser`. The tracker calls `policy.isAllowed(event)` before any sampling, scrubbing, or queueing decision, so a `false` return short-circuits the entire pipeline and emits a `drop` event with reason `consent`. Two built-in policies ship with the package: `AllowAllPolicy` (default) and `DoNotTrackPolicy` (honours `navigator.doNotTrack === "1"`). Downstream consumers can implement their own policy — e.g. one that consults a stored cookie-banner preference — by implementing the interface from `@suaveplan/analytics-browser/types`.
## Features
- Lazy navigator lookup — both policies are constructable in SSR / Node contexts where `navigator` is undefined; the policy simply permits the event in that case.
- Zero state — both built-ins are stateless and reentrant.
- Strict equality on `"1"` — matches the W3C Tracking Preference Expression spec; values `"0"` or `"unspecified"` permit tracking.
## Usage: Basic
```ts
import { DoNotTrackPolicy } from "@suaveplan/analytics-browser/consent";
import { AnalyticsTracker } from "@suaveplan/analytics-browser/tracker";
const tracker = new AnalyticsTracker(transport, {
consentPolicy: new DoNotTrackPolicy(),
});
tracker.track("page_loaded"); // dropped if navigator.doNotTrack === "1"
```
## Usage: Advanced
```ts
import type {
AnalyticsEvent,
ConsentPolicy,
} from "@suaveplan/analytics-browser/types";
class CookieBannerPolicy implements ConsentPolicy {
public constructor(private readonly read: () => "granted" | "denied") {}
public isAllowed(_event: AnalyticsEvent): boolean {
return this.read() === "granted";
}
}
```
## API Reference
- `class AllowAllPolicy implements ConsentPolicy` — `isAllowed(event) → true`.
- `class DoNotTrackPolicy implements ConsentPolicy` — returns `false` iff `typeof navigator !== "undefined" && navigator.doNotTrack === "1"`.
## Implementation Notes
`DoNotTrackPolicy` reads `navigator.doNotTrack` on every call rather than caching at construction time. This is deliberate: the spec lets the user toggle the preference at runtime in some browsers (notably in private windows), and caching would freeze the decision at construction. The cost is a single property read per event — negligible compared with the JSON serialisation that follows on the happy path. Both classes pass their `event` argument with a leading underscore name to signal it is intentionally unused; lint rules permit underscore-prefixed unused parameters.

View File

@@ -0,0 +1,68 @@
import { afterEach, describe, expect, it } from "@suaveplan/testing/runner";
import type { AnalyticsEvent } from "../types/analytics.types.js";
import { AllowAllPolicy, DoNotTrackPolicy } from "./consent.js";
const event: AnalyticsEvent = {
id: "i",
name: "n",
properties: {},
timestamp: "t",
sessionId: "s",
anonymousId: "a",
};
function setDnt(value: string | undefined): void {
if (typeof navigator === "undefined") {
return;
}
Object.defineProperty(navigator, "doNotTrack", {
configurable: true,
get: () => value,
});
}
afterEach(() => {
setDnt(undefined);
});
describe("AllowAllPolicy", () => {
it("permits every event", { timeout: 5000 }, () => {
const policy = new AllowAllPolicy();
expect(policy.isAllowed(event)).toBe(true);
});
});
describe("DoNotTrackPolicy", () => {
it("DoNotTrackPolicy blocks when navigator.doNotTrack === '1'", {
timeout: 5000,
}, () => {
setDnt("1");
const policy = new DoNotTrackPolicy();
expect(policy.isAllowed(event)).toBe(false);
});
it("permits events when DNT is not set", { timeout: 5000 }, () => {
setDnt("0");
const policy = new DoNotTrackPolicy();
expect(policy.isAllowed(event)).toBe(true);
});
it("permits events when navigator is not present (SSR)", {
timeout: 5000,
}, () => {
const realNav = globalThis.navigator;
try {
Object.defineProperty(globalThis, "navigator", {
value: undefined,
configurable: true,
});
const policy = new DoNotTrackPolicy();
expect(policy.isAllowed(event)).toBe(true);
} finally {
Object.defineProperty(globalThis, "navigator", {
value: realNav,
configurable: true,
});
}
});
});

View File

@@ -0,0 +1,29 @@
import type {
AnalyticsEvent,
ConsentPolicy,
} from "../types/analytics.types.js";
/**
* Permits every event. Default policy when none is supplied to the
* tracker.
*/
export class AllowAllPolicy implements ConsentPolicy {
public isAllowed(_event: AnalyticsEvent): boolean {
return true;
}
}
/**
* Honours the host's `Do Not Track` signal. Blocks every event when
* `navigator.doNotTrack === "1"`, otherwise permits every event. Reads
* the navigator lazily inside `isAllowed` so the policy is constructable
* in SSR / Node contexts where `navigator` does not exist.
*/
export class DoNotTrackPolicy implements ConsentPolicy {
public isAllowed(_event: AnalyticsEvent): boolean {
if (typeof navigator === "undefined") {
return true;
}
return navigator.doNotTrack !== "1";
}
}

View File

@@ -0,0 +1 @@
export { AllowAllPolicy, DoNotTrackPolicy } from "./consent.js";