# 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.