Skip to content

Commit 63570f6

Browse files
authored
Merge branch 'main' into feat/DC-115
2 parents a7b0cb9 + 74e02d6 commit 63570f6

102 files changed

Lines changed: 9402 additions & 2285 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.husky/pre-commit

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -93,12 +93,20 @@ if [ "$BACKEND_CHANGES" = "true" ]; then
9393

9494
if command -v dotnet &> /dev/null; then
9595
# Verify code formatting matches .editorconfig (fails if changes needed)
96-
dotnet format SEBT.Portal.sln --verify-no-changes --verbosity quiet || {
96+
# Exclude sibling repos (state-connector, dc-connector, co-connector) that get
97+
# pulled in via conditional ProjectReference but live outside this repository.
98+
dotnet format SEBT.Portal.sln --verify-no-changes --verbosity quiet \
99+
--exclude ../sebt-self-service-portal-state-connector/ \
100+
--exclude ../sebt-self-service-portal-dc-connector/ \
101+
--exclude ../sebt-self-service-portal-co-connector/ || {
97102
log_error ".NET code formatting check failed - run 'dotnet format SEBT.Portal.sln' to fix"
98103
exit 1
99104
}
100-
101-
dotnet format SEBT.Portal.sln analyzers --verify-no-changes --verbosity quiet || {
105+
106+
dotnet format SEBT.Portal.sln analyzers --verify-no-changes --verbosity quiet \
107+
--exclude ../sebt-self-service-portal-state-connector/ \
108+
--exclude ../sebt-self-service-portal-dc-connector/ \
109+
--exclude ../sebt-self-service-portal-co-connector/ || {
102110
log_error ".NET analyzer checks failed - fix the reported issues"
103111
exit 1
104112
}
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
# 9. State Backend Health Checks Report Degraded, Not Unhealthy
2+
3+
Date: 2026-03-17
4+
5+
## Status
6+
7+
Accepted
8+
9+
## Context
10+
11+
State connector plugins register health checks that verify connectivity to their backend systems (DC's SQL database, CO's CBMS API). Initially these checks reported `Unhealthy` when the backend was misconfigured or unreachable. In ASP.NET Core's health check framework, the overall `/health` endpoint status is the worst individual check status — so a single `Unhealthy` check makes the entire endpoint report `Unhealthy`, which can cause container orchestrators to kill and replace the container.
12+
13+
The portal can still serve requests (login, static pages, cached data) when a state backend is down. Recycling the container won't fix a downstream outage and can cascade into a worse situation.
14+
15+
## Decision
16+
17+
State connector health checks always report `Degraded` — never `Unhealthy` — regardless of whether the issue is missing configuration or an unreachable backend. The structured JSON response from `/health` includes per-check descriptions and exception details, so monitoring and alerting can still distinguish between misconfiguration and connectivity failures.
18+
19+
### Alternatives considered
20+
21+
- **Report `Unhealthy` for connectivity failures, `Degraded` for missing config.** More semantically precise, but the operational consequence (container recycling) is undesirable in both cases.
22+
- **Have connectors report `Unhealthy` but remap to `Degraded` at the portal level.** ASP.NET Core's overall status is the worst individual status with no built-in remapping. A portal-side wrapper could intercept results, but adds complexity for the same operational outcome.
23+
- **Make the failure status configurable via appsettings.** Flexibility for future states, but adds a configuration knob that would likely be set once and forgotten — and increases maintenance burden for state partners. Can revisit if needed as more states onboard.
24+
25+
## Consequences
26+
27+
- The `/health` endpoint never returns `Unhealthy` due to a state backend issue, preventing aggressive container recycling.
28+
- Monitoring must inspect the per-check details (description, exception) in the JSON response rather than relying solely on the top-level status to detect backend outages.
29+
- The portal's own health (e.g., its database) can still report `Unhealthy` if needed in the future — this decision applies only to state connector checks.
Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
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`

package.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -27,8 +27,8 @@
2727
"devDependencies": {
2828
"concurrently": "^9.2.1",
2929
"husky": "^9.1.7",
30-
"knip": "^5.71.0",
31-
"lint-staged": "^16.2.7",
30+
"knip": "^5.87.0",
31+
"lint-staged": "^16.4.0",
3232
"tsx": "^4.21.0"
3333
},
3434
"engines": {

0 commit comments

Comments
 (0)