diff --git a/docs/decisions/adr-024-product-analytics-channel.md b/docs/decisions/adr-024-product-analytics-channel.md index 858d069..45ed913 100644 --- a/docs/decisions/adr-024-product-analytics-channel.md +++ b/docs/decisions/adr-024-product-analytics-channel.md @@ -50,28 +50,30 @@ export type AnalyticsAttributeValue = string | number | boolean; export type AnalyticsUser = { /** Stable identifier for this user. Joins events across sessions + devices. */ id: string; - /** - * Traits associated with the user. Consumer-owned PII policy applies here — - * see ADR-024 § "PII boundary" + the consumer's privacy/cookie consent layer. - */ - traits?: Record; }; export interface IAnalytics { /** Emit a named event. The conformance gate cross-checks `event` against the manifest's analyticsEvents. */ track( event: string, - properties?: Record, - user?: AnalyticsUser, + attributes?: Record, ): void; - /** Associate traits with a user. Not gated by the manifest — it's identity establishment, not an event. */ - identify(user: AnalyticsUser): void; + /** + * Associate the current analytics session with a user. The optional + * `attributes` carry user traits (plan, signup date, etc.) — see + * ADR-024 § "PII boundary". Not gated by the manifest — it's identity + * establishment, not an event. + */ + identify( + user: AnalyticsUser, + attributes?: Record, + ): void; /** Client-side route change. Server impls no-op by convention. */ pageView( path: string, - properties?: Record, + attributes?: Record, ): void; /** Drain the in-memory batch. Wire into SIGTERM / beforeExit / serverless-response-finish handlers. */ @@ -79,7 +81,7 @@ export interface IAnalytics { } ``` -Four methods. Mirrors Segment Spec's core minus the rarely-needed (`group`, `alias`, `screen`). `flush()` is on the interface because every meaningful server-side SDK batches by default and event loss on shutdown is the most common analytics bug in serverless deployments. +Four methods. Mirrors Segment Spec's core minus the rarely-needed (`group`, `alias`, `screen`). Events are attributed to the user established by the most recent `identify()` call — the standard SDK model — so `track()` carries only the event name and its attributes, and `AnalyticsUser` stays minimal (`id` only); traits ride the `attributes` argument of `identify()`. `flush()` is on the interface because every meaningful server-side SDK batches by default and event loss on shutdown is the most common analytics bug in serverless deployments. ### Manifest field @@ -97,7 +99,7 @@ useCases: { } ``` -`analyticsEvents` is an array of event slug literals. Same syntax as `audits` / `publishes` / `consumes`. The use case body emits via `analytics.track(slug, properties, user?)` calls. +`analyticsEvents` is an array of event slug literals. Same syntax as `audits` / `publishes` / `consumes`. The use case body emits via `analytics.track(slug, attributes?)` calls. ### Brand + wrapper @@ -140,14 +142,14 @@ This is the only place the analytics channel structurally diverges from `ILogger The observability surface (`ILogger`, `ITracer`) is bound to ADR-017 §7: id-only user context, `sendDefaultPii: false` everywhere, CI grep gate, server-side PII scrubbing at the OTel processor layer. That policy is deliberate and remains untouched. -The analytics surface is **structurally permissive**. `AnalyticsUser.traits` accepts arbitrary `Record`. The template makes no claim about what's allowed in it. Consumer is responsible for: +The analytics surface is **structurally permissive**. The `attributes` argument of `identify()` accepts arbitrary `Record` user traits. The template makes no claim about what's allowed in it. Consumer is responsible for: - Cookie consent and legal basis (e.g. GDPR Art. 6) - Retention policy in the analytics backend - Trait allowlist enforcement in their application code (if they want stricter than "permissive") - DSAR / right-to-erasure plumbing -This is documented in the interface's doc-comment on `AnalyticsUser.traits` and in `docs/guides/analytics.md`. No CI guardrail enforces it — the cross-cutting CI gates (ADR-022 library trace covering the backend, ADR-023 supply-chain scan) cover the actionable surface. +This is documented in the interface's doc-comment on `identify()` and in `docs/guides/analytics.md`. No CI guardrail enforces it — the cross-cutting CI gates (ADR-022 library trace covering the backend, ADR-023 supply-chain scan) cover the actionable surface. The reason this divergence is explicit rather than emergent: analytics PII is product/legal scope, not template scope. Conflating it with observability PII would either cripple analytics (id-only is unworkable for funnel analysis) or weaken observability (allowing traits in `ILogger.setUser` would erode ADR-017's grep gate).