Skip to content

Observability: frontend error reporting — ErrorBoundary → API + traceparent + sourcemaps (#217)#232

Open
mforce wants to merge 3 commits into
mainfrom
feat/217-frontend-errors
Open

Observability: frontend error reporting — ErrorBoundary → API + traceparent + sourcemaps (#217)#232
mforce wants to merge 3 commits into
mainfrom
feat/217-frontend-errors

Conversation

@mforce

@mforce mforce commented Jul 26, 2026

Copy link
Copy Markdown
Owner

Closes #217 — the last slice of the observability program (#214 traces, #215 metrics, #216 handler logs).

What

API — POST /api/v1/client-errors (anonymous, no storage)

SPA

  • web/src/lib/traceparent.ts — every API request (JSON + blob paths) now carries a W3C traceparent minted client-side (crypto-random, all-zero ids guarded), so a browser action correlates with the API request log and spans from Observability: activate the telemetry pipeline — OTLP trace export, request logging, TraceId↔log correlation #214. The client remembers the last trace id — a failed request's id is exactly the one worth having.
  • web/src/api/errorReport.ts — best-effort by contract: plain fetch (no bearer, no refresh churn, no idempotency key), keepalive: true so the report survives the fallback's Reload, client-side truncation mirroring the server, and the returned promise always resolves.
  • ErrorBoundary.componentDidCatch posts the report (message, stacks, scope, location.pathname) fire-and-forget; the fallback UI cannot be broken or delayed by it.
  • Production build emits hidden sourcemaps (.map files, no sourceMappingURL comment) so reported minified stacks resolve to source lines; verified 115 sources with embedded content in the build output.

Tests — TDD, RED verified before implementation:

  • 8 API integration tests (ClientErrorReportTests, dedicated factory: sink tap + fake remote IP + tight limit): structured-fields log, app scope, 413 + not-logged, blank message 400, unknown scope 400, per-IP 429 + independent buckets, truncation.
  • 16 SPA tests: traceparent shape/uniqueness/zero-guard, header on JSON + blob requests, last-trace-id capture incl. failed requests, report payload/truncation/trace-id join, network-failure + 429 swallowing, boundary report + scope + fallback-survives-report-failure.
  • FakeRemoteIpStartupFilter promoted from RateLimitingTests to shared infrastructure (auth rate-limit tests unchanged, still green).

Acceptance criteria

  • Render error in either scope → one structured Error log line with message, stacks, scope, route
  • Endpoint rejects oversized payloads (413) and is rate-limited per IP (429 + Retry-After)
  • Reporting failure never breaks or delays the fallback UI (contract + test)
  • Vitest tests for the reporting path (standing web/ rule)
  • SPA requests carry traceparent; the id becomes the request log's TraceId (Observability: activate the telemetry pipeline — OTLP trace export, request logging, TraceId↔log correlation #214's Incoming_traceparent_becomes_the_logged_traceid covers the server half)
  • Production build emits sourcemaps; stacks resolve (hidden variant, sources embedded)
  • Build clean; SPA coverage gate green (lib/** 100 % lock holds for the new traceparent module)

Verification

  • .NET: 765 tests green ×2 (parallel-flake check)
  • SPA: 633 tests green; coverage thresholds pass
  • dotnet build: 0 warnings, 0 errors

No GLOSSARY/Help update: nothing user-visible changed (the fallback UI is untouched; reporting is invisible to the user).

mforce added 3 commits July 25, 2026 23:39
Anonymous, per-IP rate-limited (new client-errors fixed-window policy on the
#143 infrastructure), 16 KB byte cap enforced by a capped manual read (the
#123 logo-upload pattern), per-field truncation, one structured Error-level
log line per report — no storage. FakeRemoteIpStartupFilter moves to shared
test infrastructure so the new tests and the auth rate-limit tests use the
same remote-IP shim.
…aceparent (#217)

Every API request now carries a W3C traceparent minted client-side, so a
browser action correlates with the API request log and spans (#214); the
client remembers the last trace id and the ErrorBoundary attaches it to a
best-effort, keepalive crash report that can never break the fallback UI.
Production builds emit hidden sourcemaps so reported stacks resolve to
source lines.
- Server strips control characters from the two console-rendered fields
  (message, route) — CR/LF or ANSI escapes from the anonymous caller could
  forge extra plain-text log lines; stacks stay verbatim in structured
  properties (codex/pi).
- Client budget is now the SERIALIZED body in bytes (15 KB, shrink-by-halves):
  the per-field caps summed past the server's 16 KB cap, so the deepest
  legitimate crash reports would have been silently 413'd (codex + reviewer).
- Whitespace-only messages get the placeholder before the server can 400 them
  (cavecrew).
- The crash-report POST carries its own traceparent like every SPA request;
  the payload traceId still points at the crashed screen's last request (codex).
- Sourcemaps leave wwwroot: the Dockerfile parks every .map under
  /app/sourcemaps (docker cp to resolve stacks) instead of serving full
  original sources at predictable public URLs; workbox's own maps (which
  carried sourceMappingURL comments) are no longer emitted (codex + reviewer).
- New integration tests: control-char stripping, missing message, non-JSON
  body, body exactly at / one byte over the cap, chunked no-Content-Length
  oversize (the capped-read-loop path); new web tests: byte budget incl.
  multibyte, whitespace message, report traceparent.
@mforce

mforce commented Jul 26, 2026

Copy link
Copy Markdown
Owner Author

Review round outcome (codex + pi + 2 Claude agents)

19 findings across 4 reviewers → 6 accepted (fixed in c89fc2e), 13 rejected with evidence. Several findings were duplicates across reviewers; grouped below.

Accepted → fixed

# Finding (reviewers) Fix
1 CR/LF/ANSI in anonymous message/route can forge extra plain-text log lines under {Message:lj} (codex #2, pi #1's valid half) Control characters stripped from the two console-rendered args; stacks stay verbatim in structured properties the console template never renders. Regression test asserts \n/\r/ESC gone from Message/Route, preserved in Stack.
2 Client per-field caps sum to ~18.5 KB > server 16 KB cap → deepest legitimate reports silently 413'd (codex #3, Claude-agent Important) Budget is now the serialized body in bytes: 15 KB, all sized fields shrink by halves until it fits. Tests pin worst-case ASCII and all-multibyte (💥) payloads ≤ 15 000 bytes.
3 .map files publicly served from wwwroot at predictable URLs → full original sources disclosed; workbox maps also carried sourceMappingURL comments (codex #4, Claude-agent Critical) Dockerfile parks every .map under /app/sourcemaps (operator: docker cp to resolve stacks — AC still met); workbox no longer emits maps at all. Verified: 0 sourceMappingURL comments, only the app map in dist, moved out of the served tree.
4 Crash-report POST carries no traceparent, inconsistent with "every SPA request" (codex #6) Report POST mints its own traceparent; payload traceId still points at the crashed screen's last request (the causal one). Test added.
5 Oversize coverage only exercised the declared-Content-Length early exit (codex #7 + pi #4) New integration tests: chunked body with no Content-Length (the capped-read-loop path), body exactly at the cap (202), one byte over (413), missing message, non-JSON body.
6 Whitespace-only message passes the client, server 400s it, report lost (cavecrew #4) Client trims before the emptiness check → "(no message)" placeholder. Test added.

Rejected — with evidence

Verification after fixes

  • .NET: 770 green ×2 (417 integration incl. 18 client-error tests)
  • Web: 637 green, coverage gate + lib/** 100 % lock hold
  • Build: 0 warnings / 0 errors; SPA build verified (app map emitted, nothing map-shaped served)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Observability: frontend error reporting — ErrorBoundary → API + production sourcemaps

1 participant