# Domain Entities — U4 Observability Integration
**No persisted entity.** U4 adds no table, no migration and no database column. It adds two configuration sections, one anonymous endpoint, and build-time frontend values.
---
## Concept Relationships
```mermaid
graph TD
obsconfig["Observability configuration
backend"]
dsn["Sentry DSN"]
env["Environment name"]
release["Release identifier"]
logging["Structured logger"]
corr["Correlation identifier"]
scrubber["Credential scrubber"]
sentrybe["Sentry client
backend"]
tunnel["Tunnel endpoint"]
vite["Vite build-time values
frontend"]
sentryfe["Sentry client
frontend"]
umami["Umami script component"]
apicfg["API base URL resolution"]
obsconfig --> dsn
obsconfig --> env
obsconfig --> release
dsn --> sentrybe
dsn --> tunnel
env --> sentrybe
release --> sentrybe
logging --> corr
logging --> sentrybe
scrubber -->|"filters before send"| sentrybe
vite --> sentryfe
vite --> umami
vite --> apicfg
sentryfe -->|"posts envelopes to"| tunnel
tunnel -->|"forwards to DSN host only"| sentrybe
classDef cfg fill:#fbd38d,stroke:#c05621,stroke-width:1px,color:#000;
classDef backend fill:#90cdf4,stroke:#2b6cb0,stroke-width:1px,color:#000;
classDef frontend fill:#d6bcfa,stroke:#6b46c1,stroke-width:1px,color:#000;
classDef guard fill:#9ae6b4,stroke:#2f855a,stroke-width:1px,color:#000;
class obsconfig,dsn,env,release,vite cfg;
class logging,corr,sentrybe,tunnel backend;
class sentryfe,umami,apicfg frontend;
class scrubber guard;
```
Text alternative: backend configuration supplies the Sentry DSN, environment and release; the structured logger attaches a correlation identifier and feeds Sentry through a credential scrubber; the frontend reads build-time values and routes its error envelopes through the same-origin tunnel, which forwards only to the configured DSN host.
---
## Backend configuration — `Observability` section
| Field | Type | Default | Purpose |
|---|---|---|---|
| `SentryDsn` | string | empty | Absent means Sentry is skipped entirely (BR-U4-08) |
| `Environment` | string | falls back to `ASPNETCORE_ENVIRONMENT` | Tag distinguishing test from production |
| `TunnelMaxPayloadBytes` | int | fixed default | Upper bound enforced by the tunnel (BR-U4-17) |
| `TracesSampleRate` | double | conservative default | Performance sampling; kept low so the free plan is not exhausted |
**Not configurable, deliberately**: the scrub list (BR-U4-13) and the tunnel's destination host (BR-U4-16). Both are security-critical, and making either configurable would create a way to switch the protection off — the scrub list by omission, the destination by turning the endpoint into a request-forgery primitive.
**Secret handling**: a Sentry DSN is not a secret in the usual sense — it identifies a project and permits event submission, and the frontend's copy is visible in the page source. It still comes from environment variables in production, consistent with D-16, rather than being committed.
---
## Correlation identifier
| Aspect | Detail |
|---|---|
| Purpose | Ties every log entry and Sentry event from one request together (SECURITY-03) |
| Scope | One request |
| Presence | On **every** log entry, not only on errors |
| Mechanism | **Not fixed here** — `HttpContext.TraceIdentifier` versus W3C `traceparent` is OPEN-01, decided in this unit's NFR Design |
Recorded as an entity because the choice affects the shape of every log entry, and NFR Design will settle it rather than leaving it to code generation.
---
## Credential scrubber
Not persisted; a filter applied to every outbound Sentry event.
| Removed | Reason |
|---|---|
| `Cookie` header, entire | Carries the `refreshToken`. Removing one cookie by rewriting the header is error-prone |
| `Authorization` header | Bearer token |
| `X-Master-Api-Key` header | Master/slave shared secret |
| Request body | Login and password-change bodies carry passwords |
| Retained | Reason |
|---|---|
| Method, path, query string | Diagnostic value. Note the invitation-token caveat below |
| User agent | Browser-specific failures |
| IP address | Attack-pattern recognition |
| Authenticated username | Whose session hit the problem |
| Correlation identifier | Ties the event to its log entries |
**Invitation-token caveat**: `/api/v1/Invitation/validate?token=…` puts a token in the query string, which is retained. It is single-use and time-limited rather than a standing credential, and the value of knowing which endpoint was called outweighs it. Documented so the trade-off is visible.
---
## Security event types
Six event shapes, not persisted — emitted as structured log entries that also become Sentry events.
| Event | Level | Context |
|---|---|---|
| Failed login | Warning | Endpoint, whether the account exists, correlation ID |
| Authorization denied | Warning | Endpoint, required policy, role held, correlation ID |
| Master API key rejected | Warning | Endpoint, calling host, correlation ID |
| Admin bypass rejected | Warning | Path, rejection reason class, correlation ID |
| Rate limit triggered | Warning | Limiter name, endpoint, correlation ID |
| Migration failure | Critical | Exception, attempt count, correlation ID |
All six sit at `Warning` or above, so they cross the Sentry event threshold (BR-U4-06) by construction rather than by coincidence.
**Rejection reason classes** for the admin bypass — `InvalidSignature`, `Expired`, `WrongIssuer`, `NotAdmin`, `Malformed` — never the token itself. The distinction matters: `InvalidSignature` suggests forgery, while `Expired` is usually an administrator with a stale tab.
---
## Frontend build-time values
| Variable | Purpose | Absent behaviour |
|---|---|---|
| `VITE_API_BASE_URL` | API origin | Same-origin (BR-U4-22) |
| `VITE_SENTRY_DSN` | Frontend Sentry project | Sentry skipped (BR-U4-26) |
| `VITE_APP_ENV` | Environment tag | Untagged |
| `VITE_UMAMI_SCRIPT_URL` | Umami script origin | Umami not loaded |
| `VITE_UMAMI_WEBSITE_ID` | Per-environment website ID | Nothing rendered (BR-U4-28) |
| `VITE_APP_TITLE` | Existing; unchanged | Defaults to `SlpModularCms` |
**Why these force two build artifacts** (D-15): Vite bakes them into the bundle at build time, so one `dist/` cannot carry both the test and production website IDs or environment tags. The CI workflow therefore produces one artifact per environment.
### Frontend config model change
`AppConfig.apiBaseUrl` gains one new legal value: the empty string, meaning same-origin. Its Zod schema becomes "empty string **or** valid absolute URL" — relaxed by exactly one case, not loosened to accept anything (BR-U4-24).
---
## Persistence Summary
| Question | Answer |
|---|---|
| New tables? | None |
| New migrations? | None |
| New configuration sections? | One backend section (`Observability`); frontend variables are build-time |
| New endpoints? | One — the anonymous Sentry tunnel |
| Secrets stored? | None. The DSN is not a standing credential and comes from environment variables |
| Data sent to third parties? | Error events to Sentry, page views to self-hosted Umami. Credentials scrubbed per BR-U4-13 |