|
| 1 | +# 9. Vendor-Agnostic Privacy-Aware Data Layer |
| 2 | + |
| 3 | +Date: 2026-03-16 |
| 4 | + |
| 5 | +## Status |
| 6 | + |
| 7 | +Accepted |
| 8 | + |
| 9 | +## Context |
| 10 | + |
| 11 | +The portal needs analytics and observability to understand user behavior, measure feature adoption, and support operational monitoring. Multiple analytics vendors (e.g., Mixpanel, Google Analytics, Adobe Analytics) may be used across different state deployments, and vendor choices may change over time. Directly integrating vendor SDKs throughout the codebase would create tight coupling, make vendor switches expensive, and risk leaking PII to vendors that should not receive it. |
| 12 | + |
| 13 | +We need a single, canonical source of truth for page metadata, user attributes, and tracked events that: |
| 14 | + |
| 15 | +- Decouples application code from any specific analytics vendor. |
| 16 | +- Enforces privacy scoping so PII is only accessible to authorized consumers. |
| 17 | +- Supports multiple concurrent vendor integrations without code changes. |
| 18 | +- Is understandable and maintainable by state partner agencies after handoff. |
| 19 | + |
| 20 | +## Decision |
| 21 | + |
| 22 | +We adopt a **vendor-agnostic data layer** following the [W3C Customer Experience Digital Data Layer (CEDDL)](https://www.w3.org/2013/12/ceddl-201312.pdf) recommendations. The implementation lives in `src/SEBT.Portal.Web/src/lib/data-layer.ts` as a framework-agnostic TypeScript class. |
| 23 | + |
| 24 | +**Canonical data structure** bound to `window.digitalData`: |
| 25 | + |
| 26 | +- `page` — page-level metadata with `category` and `attribute` sub-objects. |
| 27 | +- `user` — user-level data with a `profile` sub-object. |
| 28 | +- `event[]` — append-only event log with `eventName`, `eventData`, `timeStamp`, and `scope`. |
| 29 | +- `privacy.accessCategories` — declared access scopes. |
| 30 | +- `initialized` — boolean flag indicating readiness. |
| 31 | + |
| 32 | +**Privacy-aware scoping:** |
| 33 | + |
| 34 | +- Each data element can be assigned one or more access scopes (e.g., `"default"`, `"analytics"`, `"marketing"`). |
| 35 | +- Page data is publicly readable by default (no scope restriction). |
| 36 | +- User data automatically receives `"default"` scope, restricting access unless explicitly broadened. |
| 37 | +- Scope inheritance walks the path hierarchy from specific to general, so child elements inherit parent scope restrictions. |
| 38 | +- Scope metadata is stored in a private `Map`, not on the data objects themselves. |
| 39 | + |
| 40 | +**Loose coupling via DOM CustomEvents:** |
| 41 | + |
| 42 | +- All mutations emit namespaced `CustomEvent`s on `document` (e.g., `digitalData:PageElementSet`, `digitalData:UserProfileSet`, `digitalData:EventTracked`). |
| 43 | +- Vendor integration bridges subscribe to these events and forward data according to their scope permissions. |
| 44 | +- A global `DataLayer:Initialized` event signals readiness. |
| 45 | +- An `eventTypes` object on the root provides type-safe event name constants for bridge consumers. |
| 46 | + |
| 47 | +**React integration** uses a `DataLayerProvider` component that initializes the data layer once on mount via `useRef`, wrapped as the outermost provider in the app layout. |
| 48 | + |
| 49 | +## Consequences |
| 50 | + |
| 51 | +- **Application code** calls `digitalData.page.set(...)`, `digitalData.user.set(...)`, and `digitalData.trackEvent(...)` without knowing which vendors consume the data. |
| 52 | +- **Adding a new vendor** requires only a new event listener bridge — no changes to application code or the data layer itself. |
| 53 | +- **Removing a vendor** means removing its bridge. No application code changes. |
| 54 | +- **PII protection** is enforced at the data layer boundary. A vendor bridge for `"analytics"` scope cannot read user data that only has `"default"` scope. |
| 55 | +- **State partners** can configure different vendor bridges per deployment without modifying the core portal. |
| 56 | +- **Testing** is straightforward because the class is framework-agnostic and testable with jsdom. |
| 57 | + |
| 58 | +## References |
| 59 | + |
| 60 | +- W3C CEDDL specification: https://www.w3.org/2013/12/ceddl-201312.pdf |
| 61 | +- Implementation: `src/SEBT.Portal.Web/src/lib/data-layer.ts` |
| 62 | +- Tests: `src/SEBT.Portal.Web/src/lib/data-layer.test.ts` |
| 63 | +- Provider: `src/SEBT.Portal.Web/src/providers/DataLayerProvider.tsx` |
0 commit comments