Initial Commit
This commit is contained in:
@@ -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.
|
||||
@@ -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,
|
||||
});
|
||||
}
|
||||
});
|
||||
});
|
||||
@@ -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";
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
export { AllowAllPolicy, DoNotTrackPolicy } from "./consent.js";
|
||||
Reference in New Issue
Block a user