Records the functional design for the two remaining application units, before any of their code exists. Security headers have to come from the application, because relying on nginx or IIS configuration is exactly what this deployment model rules out. Strict applies to /admin, /api/v1 and /health; a relaxed policy applies to the public website, which this repository does not author. The strict policy needs style-src 'unsafe-inline'. That is not a shortcut: Radix positions dropdowns and dialogs with inline style attributes recalculated per click and scroll position, and CSP nonces apply only to style elements, never to style attributes. No nonce- or hash-based variant leaves the admin UI working. The exception is bounded to styles — script-src stays closed, which is where XSS actually lives. The website's policy is enforcing rather than absent, so every HTML-serving path carries a CSP and no exception has to be recorded. It still blocks external script origins, so it remains a real boundary. HSTS is skipped in development: browsers remember it per host and localhost is shared with unrelated projects. Every other header applies locally, so a CSP violation surfaces while developing. For observability, browser error reports tunnel through the API rather than going to Sentry directly. Ad blockers block Sentry domains, which loses errors precisely for the users most likely to have browser oddities. The tunnel forwards only to the host derived from the configured DSN — a caller-supplied destination would turn an anonymous endpoint into a request-forgery primitive. Two consequences of the chosen options are recorded rather than left implicit: Enabling SendDefaultPii attaches request headers, and this application carries two standing credentials in them. Besides the refreshToken cookie, X-Master-Api-Key would have been sent to a third party on every error raised during a master/slave call. The scrub list removes the whole Cookie header, Authorization, X-Master-Api-Key and the request body. Console logging at Information plus structured logging to Sentry would, taken literally, mean one Sentry event per request — exhausting the free plan within hours and burying real errors in request noise. The thresholds are split: console keeps Information, Sentry takes warnings and above as events with Information as breadcrumbs, so every event arrives carrying the trail that led to it. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01HHoJpxYXzHACSQguHrC5fw
10 KiB
Business Rules — U4 Observability Integration
Reporting Decision Logic
graph TD
entry["Log entry or exception"]
console["Write to console<br/>Information and above"]
dsn{"Sentry DSN configured ?"}
stop["Done: console only"]
level{"Level is Warning or above ?"}
crumb["Attach as breadcrumb<br/>context for a future event"]
scrub["Scrub credentials from request context"]
event["Send as Sentry event<br/>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.