shoc-backend/docs/adr/0002-sentry-observability.md
2026-09-03 17:12:49 -03:00

4 KiB

ADR 0002 — Sentry observability: release identity and redaction-safe telemetry fields

  • Status: Accepted
  • Date: 2026-09-03
  • Scope: Api.SeaHavenIndustries (observability layer only; no behavior changes)

Context

The backend reports to Sentry (SH-298). Events arrived without a stable release identity, without service/commit attribution, and background jobs each hand-rolled their own transaction setup. HTTP request metadata risked leaking PII through route values, query strings, or identity claims.

Decision

  1. Release identity. SentryObservability.ResolveRelease(Assembly) accepts an informational version shaped shoc-backend@<40 hex commit sha> and otherwise resolves to local-development. scripts/package-elastic-beanstalk.sh resolves the commit from APP_COMMIT_SHA → GITHUB_SHA → git rev-parse HEAD, fails when CI=true cannot produce a valid 40-hex sha, and publishes with -p:SourceRevisionId, -p:InformationalVersion=shoc-backend@<sha>, -p:DebugType=portable, -p:DebugSymbols=true.
  2. Default tags. service=shoc-backend and app.commit=<sha | local-development> on SentryOptions.DefaultTags. All events use the same commit-addressed shoc-backend@<sha> release.
  3. HTTP requests. SentryRequestMetadataMiddleware runs after routing/authentication and before authorization. It sets on the current scope only the fields below. Non-controller endpoints omit code.function.
  4. Background jobs. All six worker transaction blocks (five files) use SentryObservability.BeginBackgroundTransaction, which pushes a scope, starts the transaction, sets the scope transaction, and returns a handle with FinishOk / FinishCancelled / FinishError(Exception) that cannot double-finish. Existing status/error behavior is preserved.
  5. Last-mile privacy. Request-body extraction is disabled. Global before-send processors clear request data, query strings, cookies, headers, environment values, and miscellaneous request values from both error events and transactions, retain only an opaque user ID, and attach native trace and transaction IDs as searchable transaction tags. Root and child span data use an allowlist for function, operation, actor, route template, method/status, protocol, database operation, and service host; raw URLs, query values, network addresses, and arbitrary extras are removed.

Field / redaction contract

Allowed request fields (template values only — never resolved route values):

Field Source Rule
actor.type auth state authenticated / anonymous / system
operation.type static http.server / background_job
code.function ControllerActionDescriptor ControllerName.ActionName only
http.method request HTTP verb
http.route RouteEndpoint.RoutePattern.RawText route template; omitted when unresolved
trace_id / transaction_id active Sentry span/transaction native Sentry ids
scope User.Id ClaimTypes.NameIdentifier only set only when authenticated

Never intentionally sent: query strings, headers, bodies, cookies, usernames, emails, IP addresses, route values, credentials, or token values. The application retains stack traces, exception types/messages, safe tags, operation status, and route templates because those are required to identify the failing function and operation.

Consequences

  • Release health maps 1:1 to a commit sha; local builds are clearly labeled.
  • One redaction contract to audit, enforced by SentryObservabilityTests/SentryPipelineContractTests.
  • ASP.NET Core request transactions and the SDK's registered outgoing HTTP instrumentation cover inbound API calls and the platform's typed procurement client; explicit root transactions cover every hosted worker loop.

Alternatives considered

  • Per-worker transaction setup (rejected: duplication drifted across five files).
  • Sentry's default request payload with PII scrubbing (rejected: default-deny is safer than default-capture-then-scrub).