# Logical Components — U4 Observability Integration Backend types live in `SlpModularCms.Core/Hosting/Observability/`, with the exception of `SecurityEvents`, which sits at `SlpModularCms.Core/Observability/` because it is referenced from four assemblies and is not host wiring. Everything is in `Core`, therefore everything is reachable from `SlpModularCms.Core.Tests`. Frontend additions are two components, one library module and three modified files under `frontend/src/`. --- ## Backend Component Wiring ```mermaid graph TD subgraph Config["Configuration"] Section["Observability section"] Options["ObservabilityOptions"] LogCfg["Logging:LogLevel
raised to Information"] end subgraph Logging["Logging — always active"] Activity["ActivityTrackingOptions
TraceId, SpanId, ParentId"] Console["JsonConsole or SimpleConsole
IncludeScopes = true"] end subgraph SentryWiring["Sentry — only when a DSN is present"] UseExt["UseCmsSentry"] Scrubber["ISentryEventScrubber"] Processor["SecurityEventProcessor
ISentryEventProcessor"] Sdk["Sentry SDK
MinimumEventLevel = Warning
MinimumBreadcrumbLevel = Information"] end subgraph Events["Alertable Events"] SecEvents["SecurityEvents
LoggerMessage, EventId 5001-5006"] Reason["BypassRejectionReason"] Sites["Emission sites:
Identity, Master, Availability, Core"] end subgraph Tunnel["Sentry Tunnel"] Target["SentryTunnelTarget
singleton, parsed at startup"] Endpoint["MapSentryTunnel
/sentry-tunnel"] Limiter["sentry-tunnel rate limiter"] Client["Named HttpClient
5s timeout, no retry"] end Ingest["Sentry ingest"] Section --> Options Options --> UseExt Options --> Target LogCfg --> Console Activity --> Console Activity --> Sdk UseExt --> Sdk Scrubber --> Sdk Processor --> Sdk Sites --> SecEvents SecEvents --> Reason SecEvents --> Console SecEvents --> Sdk Endpoint --> Limiter Endpoint --> Target Endpoint --> Client Client --> Ingest Sdk --> Ingest classDef cfg fill:#fbd38d,stroke:#c05621,stroke-width:1px,color:#000; classDef always fill:#9ae6b4,stroke:#2f855a,stroke-width:1px,color:#000; classDef optional fill:#90cdf4,stroke:#2b6cb0,stroke-width:1px,color:#000; classDef guard fill:#fbb6ce,stroke:#b83280,stroke-width:1px,color:#000; classDef ext fill:#e2e8f0,stroke:#4a5568,stroke-width:1px,color:#000; class Section,Options,LogCfg cfg; class Activity,Console,SecEvents,Reason,Sites always; class UseExt,Sdk,Target,Endpoint,Client,Processor optional; class Scrubber,Limiter guard; class Ingest ext; ``` Text alternative: activity tracking and console logging are always active and independent of Sentry; the Sentry SDK is registered only when a DSN is present and always behind the scrubber and the security-event tag processor; the six security events are ordinary log entries that reach both destinations; the tunnel endpoint forwards browser envelopes to a destination parsed once at startup. --- ## Frontend Component Wiring ```mermaid graph TD main["main.tsx"] sentryts["lib/sentry.ts
initSentry — NEW"] config["lib/config.ts
MODIFIED"] apiclient["lib/api-client.ts
MODIFIED — empty base"] vite["vite.config.ts
MODIFIED — define + dev proxy"] boundary["SentryErrorBoundary
NEW"] umami["UmamiAnalytics
NEW"] inner["InnerApp"] tunnelpath["POST /sentry-tunnel"] main --> sentryts main --> boundary main --> umami boundary --> inner config --> sentryts config --> apiclient config --> umami vite --> sentryts vite --> tunnelpath sentryts --> tunnelpath classDef newc fill:#9ae6b4,stroke:#2f855a,stroke-width:1px,color:#000; classDef modc fill:#fbd38d,stroke:#c05621,stroke-width:1px,color:#000; classDef exist fill:#90cdf4,stroke:#2b6cb0,stroke-width:1px,color:#000; classDef ext fill:#e2e8f0,stroke:#4a5568,stroke-width:1px,color:#000; class sentryts,boundary,umami newc; class config,apiclient,vite modc; class main,inner exist; class tunnelpath ext; ``` Text alternative: a new sentry library module is initialised first from main.tsx and posts to the same-origin tunnel; the modified config module feeds Sentry, the API client and the Umami component; vite supplies the release version and, in development, proxies the tunnel path to the local API. --- ## Backend Component Responsibilities | Component | Lifetime | Responsibility | Pattern | |---|---|---|---| | `ObservabilityOptions` | Options singleton | `SentryDsn`, `Environment`, `TracesSampleRate`, `TunnelMaxPayloadBytes` | 10 | | `LoggingExtensions.AddCmsLogging` | Static | Activity tracking, `IncludeScopes`, console provider per environment | 1, 2 | | `SentryExtensions.UseCmsSentry` | Static | No-op without a DSN; otherwise thresholds, scrubber, processor | 3, 8 | | `ISentryEventScrubber` / `SentryEventScrubber` | Singleton | Removes four credential headers and the body from events **and** transactions | 4 | | `SecurityEventProcessor` | Singleton | Maps `EventId` → `security_event` tag so alert rules filter on a tag | 6 | | `SecurityEvents` | Static partial | Six source-generated `LoggerMessage` methods, `EventId` 5001–5006 | 6 | | `BypassRejectionReason` | Enum | Why an admin bypass was refused — never the token | 6 | | `SentryTunnelTarget` | Singleton | Envelope endpoint parsed from the DSN once, at startup | 7 | | `SentryTunnelExtensions.MapSentryTunnel` | Static | The `/sentry-tunnel` endpoint: size cap, forward, `202` on failure | 7 | ### Modified existing components | File | Change | Why | |---|---|---| | `Program.cs` (both hosts) | `AddCmsLogging`, `UseCmsSentry`, `MapSentryTunnel` | Host wiring | | `appsettings.json` (both hosts) | `Observability` section; `Logging` raised to `Information`; EF command logging pinned at `Warning` | Pattern 3 — otherwise breadcrumbs arrive empty and EF logs parameter values | | `ServiceCollectionExtensions.AddCmsRateLimiting` | `OnRejected` callback; new `sentry-tunnel` limiter | Pattern 6, Pattern 7 — `429`s currently leave no trace at all | | `DatabaseMigrationExtensions` (U2) | `FlushAsync` before rethrow | Pattern 5 — otherwise the `Critical` event dies with the process | | `AdminTokenValidator` (U1) | Overload returning `BypassRejectionReason` | Pattern 6 | | `AvailabilityMiddleware` | Emit `AdminBypassRejected` | Pattern 6 | Two of these touch files U1 and U2 already committed. Both are additive and both are named in the U4 commit message. --- ## Frontend Component Responsibilities | Component | Kind | Responsibility | |---|---|---| | `lib/sentry.ts` | Module | `initSentry()`: skip without a DSN, tunnel path, no router integration | | `components/SentryErrorBoundary.tsx` | Class component | Catch render errors, report, show a recoverable fallback with no exception text | | `components/UmamiAnalytics.tsx` | Component | Inject the script once, never in development, no cleanup | | `lib/config.ts` | Modified | Empty-or-URL schema, four new fields | | `lib/api-client.ts` | Modified | Correct URL construction with an empty base | | `main.tsx` | Modified | `initSentry()` first; boundary inside `AuthProvider`; `UmamiAnalytics` alongside `InnerApp` | | `vite.config.ts` | Modified | `__APP_VERSION__` define; dev proxy for `/sentry-tunnel` → local API | | `vite-env.d.ts` | Modified | Declare the five new `VITE_` variables and `__APP_VERSION__` | --- ## Packages Added | Project | Package | Version | Note | |---|---|---|---| | `SlpModularCms.Core` | `Sentry.AspNetCore` | `6.8.0` | Latest on nuget.org as of 2026-07-28. **`net10.0` compatibility to be confirmed at code generation** — if the package resolves only to a `net9.0` asset the build still succeeds, but a warning must not be ignored silently | | `frontend` | `@sentry/react` | `^10.68.0` | Same major as the reference project | `Sentry.AspNetCore` in `Core` rather than in the two host projects, because `UseCmsSentry`, the scrubber, the processor and the tunnel all live in `Core` and both hosts consume them. It brings `Sentry` and `Sentry.Extensions.Logging` transitively. **FR-22 interaction**: this adds a dependency subtree to `Core` while U5 is due to pin two packages against known advisories. The new tree must be checked in the same pass, not assumed clean because it is new. --- ## DI Registration Order ``` builder.Logging: 1. AddCmsLogging(builder.Environment) ← ClearProviders, ActivityTracking, IncludeScopes builder.WebHost: 2. UseCmsSentry(builder.Configuration) ← no-op without a DSN builder.Services: 3. AddOptions().Bind(...).ValidateOnStart() 4. AddSingleton() 5. AddSingleton() 6. AddSingleton() ← throws at startup on an unparseable DSN 7. AddHttpClient("sentry-tunnel") 8. AddCmsRateLimiting(...) ← existing call, now also registers "sentry-tunnel" app: 9. MapSentryTunnel() ← after MapControllers, before MapCmsSpaFallbacks ``` **Steps 1 and 2 are order-critical** (BR-U4-04): a Sentry initialisation problem must be logged by an already-registered provider. **Step 9's position matters**: `MapCmsSpaFallbacks` maps `{*path:nonfile}`. A catch-all registered before the tunnel would not actually shadow it — route precedence favours the literal segment — but relying on precedence for a security-relevant endpoint is a poor trade against writing the two lines in the obvious order. --- ## NFR Coverage Traceability | NFR / Rule | Pattern | Component | |---|---|---| | SECURITY-03 — correlation ID on every entry | 1 | `ActivityTrackingOptions` + `IncludeScopes` | | SECURITY-03 — no secrets or PII in logs | 3, 4 | EF command logging pinned; `SecurityEvents` templates carry no credentials | | SECURITY-11 — no request forgery | 7 | `SentryTunnelTarget` — destination never from the caller | | SECURITY-14 — alertable security events | 6 | `SecurityEvents` + `SecurityEventProcessor` tag | | SECURITY-15 — failures never propagate | 8 | `UseCmsSentry` no-op path; tunnel returns `202` on forwarding failure | | NFR-01 — no server configuration | 7 | The tunnel is in-process, not in nginx | | NFR-07 — no secrets committed | 10 | DSN empty in `appsettings`, supplied as `Observability__SentryDsn` | | NFR-08 — graceful degradation | 8 | Three fully functional configurations | | BR-U4-01…05 | 1, 2, 3 | Logging always active, before Sentry, `Information` console | | BR-U4-06, BR-U4-07 | 3 | `MinimumEventLevel` / `MinimumBreadcrumbLevel` | | BR-U4-08…12 | 8 | DSN gate, environment and release tags | | BR-U4-13, BR-U4-14 | 4 | `SentryEventScrubber`, in-process, two layers | | BR-U4-15…21 | 7 | Tunnel endpoint and target | | BR-U4-22…25 | 9 | `config.ts` union schema, `api-client.ts` URL construction | | BR-U4-26…29 | 9 | `initSentry` DSN gate, `UmamiAnalytics` guards | --- ## Carried Forward | Item | To | Why | |---|---|---| | Sentry alert rules for `security_event:*` | **Operations — Monitoring Setup** (FR-19) | The tag exists in code; the rules are configured in Sentry | | `Observability__SentryDsn` as an environment variable per environment | **U6 Deploy Workflow** | Must not be committed | | `VITE_SENTRY_DSN`, `VITE_APP_ENV`, `VITE_UMAMI_*` per environment build | **U5 CI Workflow** | Build-time values; two artifacts (D-15) | | New dependency subtree checked against advisories | **U5** (FR-22) | `Sentry.AspNetCore` is new and must not be assumed clean | | `Sentry.AspNetCore` `net10.0` asset confirmation | **Code Generation** | A `net9.0` fallback builds but should be a conscious acceptance | | Trace ID propagation master → slave verified end to end | **Build and Test** | Needs both hosts running and a real master/slave call — the whole justification for Pattern 1 | | `apiBaseUrl === ''` request construction verified | **Build and Test** + unit test | Only fails in the production configuration, which nobody runs locally |