# Business Rules — U4 Observability Integration
---
## Reporting Decision Logic
```mermaid
graph TD
entry["Log entry or exception"]
console["Write to console
Information and above"]
dsn{"Sentry DSN configured ?"}
stop["Done: console only"]
level{"Level is Warning or above ?"}
crumb["Attach as breadcrumb
context for a future event"]
scrub["Scrub credentials from request context"]
event["Send as Sentry event
environment and release tagged"]
entry --> console
console --> dsn
dsn -->|no| stop
dsn -->|yes| level
level -->|no| crumb
level -->|yes| scrub
scrub --> event
classDef entrynode fill:#f6e05e,stroke:#c05621,stroke-width:1px,color:#000;
classDef decision fill:#90cdf4,stroke:#2b6cb0,stroke-width:1px,color:#000;
classDef step fill:#9ae6b4,stroke:#2f855a,stroke-width:1px,color:#000;
classDef neutral fill:#e2e8f0,stroke:#4a5568,stroke-width:1px,color:#000;
class entry entrynode;
class dsn,level decision;
class console,crumb,scrub,event step;
class stop neutral;
```
Text alternative: everything goes to the console; with a DSN configured, informational entries become breadcrumbs while warnings and above become Sentry events, always after credentials are scrubbed from the request context.
---
## Logging Rules
| ID | Rule |
|---|---|
| **BR-U4-01** | Structured console logging is always active, independent of Sentry. |
| **BR-U4-02** | Console threshold is `Information`, including framework categories. |
| **BR-U4-03** | Every log entry carries a correlation identifier. |
| **BR-U4-04** | Logging is registered **before** Sentry, so a Sentry initialisation problem is itself logged. |
| **BR-U4-05** | No log entry may contain a password, token, API key, connection string or cookie value. |
| **BR-U4-06** | Sentry **event** threshold is `Warning` and above. |
| **BR-U4-07** | Sentry **breadcrumb** threshold is `Information`, so events arrive with the trail that led to them. |
**Rationale for BR-U4-06 and BR-U4-07 — reconciling two answers rather than choosing between them**: Q3 = C asked for `Information` everywhere, and Q16 = C asked for structured logging to Sentry beyond exceptions. Taken literally together, every framework `Information` entry would become a Sentry event — an event per request, exhausting the free plan within hours and burying real errors in request noise.
Splitting the thresholds honours both: the console gets everything (Q3 = C), and Sentry gets warnings and errors — still more than exceptions, as Q16 = C requires — each carrying its informational breadcrumbs.
---
## Sentry Rules
| ID | Rule |
|---|---|
| **BR-U4-08** | An absent DSN is a normal, supported state. Sentry initialisation is skipped and console logging continues. |
| **BR-U4-09** | An unreachable Sentry never blocks or fails a request. |
| **BR-U4-10** | Every event is tagged with the environment and the release. |
| **BR-U4-11** | One Sentry project serves both backend and frontend, and both environments, distinguished by tags. |
| **BR-U4-12** | Request context is included, with credentials scrubbed per BR-U4-13. |
| **BR-U4-13** | Before any event leaves the process, these are removed: the entire `Cookie` header, the `Authorization` header, the `X-Master-Api-Key` header, and the request body. |
| **BR-U4-14** | Scrubbing happens in-process, before transmission — never relying on a server-side setting in Sentry. |
**Rationale for BR-U4-13 — this list is longer than the question implied.** Enabling `SendDefaultPii` attaches request headers, and this application carries two standing credentials in headers: the `refreshToken` cookie and the `X-Master-Api-Key` used by the master/slave protocol. The API key was not mentioned when the setting was chosen, but it is the same class of secret and would otherwise be sent to a third party on every error raised during a master/slave call.
The request body is removed because the login and password-change endpoints carry passwords in it.
**Rationale for BR-U4-14**: server-side scrubbing means the secret already left the building. Doing it in-process is the only version that actually protects anything.
**What is deliberately retained**: method, path, query string, user agent, IP address, authenticated username and correlation ID. Note that invitation tokens travel as `?token=…` on one endpoint — a single-use, time-limited token rather than a standing credential, and the diagnostic value of the path and query outweighs it. Recorded so the trade-off is visible rather than accidental.
---
## Sentry Tunnel Rules
| ID | Rule |
|---|---|
| **BR-U4-15** | The browser sends Sentry envelopes to a same-origin tunnel endpoint, not directly to Sentry. |
| **BR-U4-16** | The tunnel forwards **only** to the host derived from the configured DSN. A caller-supplied destination is never honoured. |
| **BR-U4-17** | The tunnel rejects payloads above a fixed maximum size. |
| **BR-U4-18** | With no DSN configured, the tunnel accepts nothing and does nothing. |
| **BR-U4-19** | The tunnel is anonymous — error reports must work for a user whose session just expired. |
| **BR-U4-20** | The tunnel is **not** on the availability bypass list. |
| **BR-U4-21** | The backend's own Sentry reporting bypasses the tunnel and reports directly. |
**Rationale for BR-U4-16 — the rule that keeps this endpoint from being a liability**: an anonymous endpoint that makes an outbound request on demand is a server-side request forgery primitive if the destination comes from the caller. Deriving the destination solely from configuration removes that entirely.
**Rationale for BR-U4-19 and BR-U4-20 together**: the tunnel must be anonymous, because the errors most worth capturing include authentication failures. But it need not survive the instance being switched off — if the CMS is deliberately disabled, losing admin-SPA error reports is acceptable, and keeping it off the bypass list means one less anonymous, outbound-capable endpoint reachable on a disabled instance.
---
## Frontend Configuration Rules
| ID | Rule |
|---|---|
| **BR-U4-22** | An absent or empty `VITE_API_BASE_URL` resolves to same-origin: requests use relative paths. |
| **BR-U4-23** | An explicit absolute URL is used as supplied. |
| **BR-U4-24** | Validation accepts an empty string **or** a valid absolute URL — nothing else. A malformed value is not silently accepted. |
| **BR-U4-25** | Local development against `https://localhost:7221` (master) and `:7222` (slave) must keep working unchanged. |
| **BR-U4-26** | Frontend Sentry initialisation is skipped when no DSN is configured. |
| **BR-U4-27** | The Umami script is never loaded in local development, regardless of configuration. |
| **BR-U4-28** | With no Umami website ID configured, nothing is rendered. |
| **BR-U4-29** | `Do Not Track` is not consulted. |
**Rationale for BR-U4-24**: relaxing validation to allow an empty value is not the same as removing validation. A typo such as `htp://localhost:7221` must still be caught, or the SPA silently issues requests to a nonexistent origin.
**Rationale for BR-U4-29** (Q5 = A): Umami sets no cookies and collects no personal data, and the admin SPA's audience is a known set of operators. Honouring DNT would reduce data without protecting anyone. A conscious choice rather than an omission.
---
## Error and Edge-Case Scenarios
| Scenario | Expected behaviour |
|---|---|
| No DSN, application runs normally | Console logging only. No error, no warning about the absence |
| DSN configured, Sentry unreachable | Sentry buffers and eventually drops. No request fails |
| Exception during a master/slave call | Event sent; `X-Master-Api-Key` scrubbed |
| Failed login | Event at warning level; no password, no attempted password |
| Startup migration fails | Event sent, then the process does not start. The event must be delivered before exit |
| Sentry initialisation itself throws | Logged by the already-registered console logger; the application continues without Sentry |
| Browser posts to the tunnel with no DSN configured | Rejected; nothing forwarded |
| Browser posts an oversized payload to the tunnel | Rejected |
| Instance availability-disabled, browser posts to the tunnel | `503` from the gate. Accepted loss |
| Ad blocker active | The tunnel is same-origin, so reports arrive — the reason it exists |
| `VITE_API_BASE_URL` unset in a production build | Same-origin. The intended production configuration |
| `VITE_API_BASE_URL` set to `https://localhost:7221` locally | Used as given; local development unchanged |
| `VITE_API_BASE_URL` set to `htp://typo` | Development: loud warning. Production: used as given, and the requests visibly fail |
| Umami configured but its origin missing from the CSP | Script blocked by the browser; U3's startup warning (BR-U3-22) flags the misconfiguration |
| Local development with a Umami ID configured | Script not loaded |
---
## Security Compliance for U4
| Rule | Status | Notes |
|---|---|---|
| SECURITY-03 | **Compliant** | Structured logging with a correlation ID on every entry (BR-U4-03); credentials and PII excluded by BR-U4-05 and BR-U4-13; centralised destination via Sentry |
| SECURITY-11 | Compliant | The tunnel's fixed destination (BR-U4-16) prevents it becoming a request-forgery primitive |
| SECURITY-13 | Compliant | The Umami script is external and constrained by U3's CSP; SRI applied where the provider supports it |
| SECURITY-14 | **Addressed, with DEV-01** | Six alertable event types emitted (BR-U4-01 group); alert rules configured in Operations. Retention remains the accepted deviation — Sentry's plan retains roughly 30 days against the 90 the rule asks for |
| SECURITY-15 | Compliant | Observability failures never propagate into request handling (BR-U4-09) |
**No new deviation.** DEV-01 already covers the retention shortfall; nothing here introduces another.
**One risk closed that was not in the original scope**: BR-U4-13 adds `X-Master-Api-Key` to the scrub list. Without it, enabling `SendDefaultPii` would have sent the master/slave shared secret to a third-party service on every error raised during a master/slave call.