docs(adr): align ADR-024 IAnalytics with shipped interface

The implemented IAnalytics refined the signature ADR-024 sketched:
events are attributed to the user set by identify() (the standard SDK
model) rather than passed per track() call, the attribute parameter is
named `attributes` consistently, and AnalyticsUser stays id-only with
traits riding identify()'s second argument. The code is the more
idiomatic contract and docs/guides/analytics.md already matched it —
update the ADR's interface block, manifest example, and PII-boundary
section to describe what shipped.
This commit is contained in:
2026-05-20 17:01:23 +02:00
parent a6e75dc94e
commit 708a85ee6d

View File

@@ -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<string, AnalyticsAttributeValue>;
};
export interface IAnalytics {
/** Emit a named event. The conformance gate cross-checks `event` against the manifest's analyticsEvents. */
track(
event: string,
properties?: Record<string, AnalyticsAttributeValue>,
user?: AnalyticsUser,
attributes?: Record<string, AnalyticsAttributeValue>,
): 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<string, AnalyticsAttributeValue>,
): void;
/** Client-side route change. Server impls no-op by convention. */
pageView(
path: string,
properties?: Record<string, AnalyticsAttributeValue>,
attributes?: Record<string, AnalyticsAttributeValue>,
): 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<string, AnalyticsAttributeValue>`. 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<string, AnalyticsAttributeValue>` 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).