fix: classify backend reload diagnostics (#3414)

This commit is contained in:
Carlos Florêncio
2026-09-06 11:35:51 +01:00
committed by GitHub
parent 698ef63e67
commit b086f41179
12 changed files with 416 additions and 34 deletions
@@ -1,5 +1,5 @@
// Package frontenderrors accepts bounded, best-effort reports for error toasts
// already displayed by the browser.
// Package frontenderrors accepts bounded, best-effort browser reports for
// displayed error toasts and backend-reload recovery.
package frontenderrors
import (
@@ -21,13 +21,20 @@ import (
)
const (
maxRequestBodyBytes = 64 * 1024
titleByteLimit = 8 * 1024
descriptionByteLimit = 8 * 1024
urlByteLimit = 8 * 1024
taskIDByteLimit = 128
browserFieldByteLimit = 2 * 1024
stackByteLimit = 16 * 1024
maxRequestBodyBytes = 64 * 1024
titleByteLimit = 8 * 1024
descriptionByteLimit = 8 * 1024
urlByteLimit = 8 * 1024
taskIDByteLimit = 128
browserFieldByteLimit = 2 * 1024
stackByteLimit = 16 * 1024
sourceSonner = "sonner"
sourceToastProvider = "toast-provider"
sourceBackendReload = "backend-reload"
reloadBootIDChanged = "boot_id_changed"
reloadInterlockRejected = "settings_interlock_rejected"
frontendErrorMessage = "frontend error toast"
backendReloadMessage = "frontend backend reload required"
)
type Viewport struct {
@@ -128,12 +135,21 @@ func decodeRequest(c *gin.Context) (Request, int, int, error) {
}
func normalize(request Request) (normalizedRequest, error) {
if request.Source != "sonner" && request.Source != "toast-provider" {
return normalizedRequest{}, errors.New("unsupported toast source")
if request.Source != sourceSonner && request.Source != sourceToastProvider &&
request.Source != sourceBackendReload {
return normalizedRequest{}, errors.New("unsupported report source")
}
if strings.TrimSpace(request.Title) == "" && strings.TrimSpace(request.Description) == "" {
return normalizedRequest{}, errors.New("title or description is required")
}
if request.Source == sourceBackendReload {
if request.Title != reloadBootIDChanged && request.Title != reloadInterlockRejected {
return normalizedRequest{}, errors.New("unsupported backend reload signal")
}
if request.Description != "" || request.Stack != "" || request.Error != nil {
return normalizedRequest{}, errors.New("backend reload report contains error data")
}
}
if request.ClientTimestamp != "" {
if _, err := time.Parse(time.RFC3339Nano, request.ClientTimestamp); err != nil {
return normalizedRequest{}, errors.New("client_timestamp must be RFC3339")
@@ -208,5 +224,9 @@ func (s *Service) logRequest(userID string, request normalizedRequest) {
zap.String("error_stack", request.Error.Stack),
)
}
s.log.Error("frontend error toast", fields...)
if request.Source == sourceBackendReload {
s.log.Info(backendReloadMessage, fields...)
return
}
s.log.Error(frontendErrorMessage, fields...)
}
@@ -17,6 +17,7 @@ import (
"go.uber.org/zap/zaptest/observer"
)
// @covers AC-PLATFORM-EXPECTED-RUNTIME-LOG-SEVERITY-001.18
func TestHandlerLogsBoundedStructuredFrontendError(t *testing.T) {
router, observed := newHandlerTestRouter(t, "user-1")
body := `{
@@ -55,6 +56,65 @@ func TestHandlerLogsBoundedStructuredFrontendError(t *testing.T) {
}
}
// @covers AC-PLATFORM-EXPECTED-RUNTIME-LOG-SEVERITY-001.17
func TestHandlerLogsBackendReloadDiagnosticAtInfo(t *testing.T) {
for _, signal := range []string{"boot_id_changed", "settings_interlock_rejected"} {
t.Run(signal, func(t *testing.T) {
router, observed := newHandlerTestRouter(t, "user-1")
body := `{
"client_timestamp":"2026-09-05T15:52:11.255Z",
"source":"backend-reload",
"task_id":"task-123",
"title":"` + signal + `",
"url":"http://localhost/t/task-123"
}`
response := performFrontendErrorRequest(router, body)
if response.Code != http.StatusNoContent {
t.Fatalf("status = %d, body = %s", response.Code, response.Body.String())
}
entries := observed.All()
if len(entries) != 1 {
t.Fatalf("logged entries = %d, want 1", len(entries))
}
entry := entries[0]
if entry.Level != zapcore.InfoLevel || entry.Message != "frontend backend reload required" {
t.Fatalf("entry = %s %q", entry.Level, entry.Message)
}
fields := entry.ContextMap()
if fields["source"] != "backend-reload" || fields["title"] != signal {
t.Fatalf("structured fields = %#v", fields)
}
})
}
}
func TestHandlerRejectsInvalidBackendReloadReports(t *testing.T) {
tests := []struct {
name string
body string
}{
{name: "unknown signal", body: `{"source":"backend-reload","title":"other"}`},
{name: "error data", body: `{"source":"backend-reload","title":"boot_id_changed","stack":"toast stack"}`},
{name: "description data", body: `{"source":"backend-reload","title":"boot_id_changed","description":"should be rejected"}`},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
router, observed := newHandlerTestRouter(t, "user-1")
response := performFrontendErrorRequest(router, test.body)
if response.Code != http.StatusBadRequest {
t.Fatalf("status = %d, want %d, body = %s",
response.Code, http.StatusBadRequest, response.Body.String())
}
if observed.Len() != 0 {
t.Fatalf("invalid request produced %d log entries", observed.Len())
}
})
}
}
func TestHandlerRejectsInvalidAndOversizedRequests(t *testing.T) {
router, observed := newHandlerTestRouter(t, "user-1")
tests := []struct {
@@ -43,6 +43,35 @@ describe("frontend error log API", () => {
expect(JSON.stringify(report)).not.toContain("must-not-be-walked");
});
it("builds backend-reload reports without a synthetic toast stack", () => {
const report = buildFrontendErrorReport({
source: "backend-reload",
title: "boot_id_changed",
});
expect(report.stack).toBeUndefined();
});
it("omits error objects from backend-reload reports", () => {
const report = buildFrontendErrorReport({
source: "backend-reload",
title: "boot_id_changed",
error: new Error("not a toast error"),
});
expect(report.error).toBeUndefined();
});
it("omits descriptions from backend-reload reports", () => {
const report = buildFrontendErrorReport({
source: "backend-reload",
title: "boot_id_changed",
description: "should be suppressed",
});
expect(report.description).toBeUndefined();
});
it("uses an authenticated two-second request and silently accepts 204", async () => {
const fetchMock = vi
.spyOn(globalThis, "fetch")
@@ -8,7 +8,7 @@ const BROWSER_FIELD_LIMIT = 2 * 1024;
const STACK_LIMIT = 16 * 1024;
const MAX_REACT_TEXT_NODES = 32;
export type FrontendErrorSource = "sonner" | "toast-provider";
export type FrontendErrorSource = "sonner" | "toast-provider" | "backend-reload";
export type FrontendErrorReportInput = {
source: FrontendErrorSource;
@@ -49,16 +49,20 @@ export function buildFrontendErrorReport(input: FrontendErrorReportInput): Front
client_timestamp: new Date().toISOString(),
source: input.source,
title: visibleText(input.title),
description: visibleText(input.description),
stack: bounded(new Error("error toast emitted").stack, STACK_LIMIT),
...(input.source === "backend-reload" ? {} : { description: visibleText(input.description) }),
};
if (input.source !== "backend-reload") {
report.stack = bounded(new Error("error toast emitted").stack, STACK_LIMIT);
}
if (currentURL) {
report.task_id = deriveTaskID(currentURL);
report.url = bounded(`${currentURL.origin}${currentURL.pathname}`, TEXT_LIMIT);
}
addBrowserContext(report);
const error = errorDetails(input.error);
if (error) report.error = error;
if (input.source !== "backend-reload") {
const error = errorDetails(input.error);
if (error) report.error = error;
}
return report;
}
+1 -1
View File
@@ -23,7 +23,7 @@ installVitePreloadRecovery();
installBfcacheRestoreReload();
markRenderingEngine(document.documentElement);
setBackendReloadDiagnosticReporter((source) => {
scheduleFrontendErrorReport({ source: "toast-provider", title: source });
scheduleFrontendErrorReport({ source: "backend-reload", title: source });
});
const AUTH_ROUTE_PATHS = new Set(["/login", "/setup", "/invite"]);
@@ -0,0 +1,103 @@
---
created: 2026-09-05
status: done
requirements:
- REQ-PLATFORM-BACKEND-RESTART-PAGE-RECOVERY-001
- REQ-PLATFORM-DIAGNOSTIC-LOGGING-001
- REQ-PLATFORM-EXPECTED-RUNTIME-LOG-SEVERITY-001
system_design:
- ../../specs/platform/system-design/backend-restart-page-recovery.md
- ../../specs/platform/system-design/diagnostic-logging-01.md
- ../../specs/platform/system-design/diagnostic-logging-02.md
- ../../specs/platform/system-design/expected-runtime-log-severity.md
legacy_specs: []
---
# Implementation Plan: Backend Reload Diagnostic Severity
## Overview
This fix keeps the reload-required behavior and records its report at info
level. One vertical work order changes the browser report contract and the
backend severity mapping together.
## Root cause
The backend-reload coordinator sends its expected lifecycle signal through the
error-toast report source. The backend treats every accepted report as an error
and uses the fixed message `frontend error toast`.
## Scope
### In scope
- Give backend-reload reports a distinct, allow-listed source.
- Record valid backend-reload reports at info level with a fixed message.
- Keep actual Sonner and legacy error-toast reports at error level.
- Remove synthetic error-toast data from backend-reload reports.
### Out of scope
- Backend restart detection and boot ID generation.
- The reload-required state, alert, copy, and action.
- The report endpoint path, rate limits, field limits, or storage policy.
- General log-severity changes for other frontend events.
## Technical approach
### Browser report contract
`apps/web/lib/api/domains/frontend-error-log-api.ts` adds a dedicated
backend-reload report path. The path uses `backend-reload` as its source and
does not create an error-toast stack or error object.
`apps/web/src/main.tsx` uses the dedicated path. The title remains the
stable recovery signal from `BackendReloadSignal`.
### Backend classification
`apps/backend/internal/system/frontenderrors/handler.go` accepts
`backend-reload` only with `boot_id_changed` or
`settings_interlock_rejected`. It records these reports at info level with
the fixed message `frontend backend reload required`.
The existing `sonner` and `toast-provider` sources keep the fixed
`frontend error toast` error entry. Client fields remain structured fields
and cannot select the severity.
## Tests
- `AC-PLATFORM-EXPECTED-RUNTIME-LOG-SEVERITY-001.17` maps to a new observer
test in `apps/backend/internal/system/frontenderrors/handler_test.go`.
- `AC-PLATFORM-EXPECTED-RUNTIME-LOG-SEVERITY-001.18` maps to the existing
error-toast observer test and an explicit unchanged-severity assertion.
- Frontend report tests cover the new source and the absence of synthetic
toast-error data.
- Existing generation-guard and coordinator tests confirm that restart
detection remains unchanged.
The backend observer test is the regression test. Before the correction, a
backend-reload report is rejected or represented as an error-toast entry.
## E2E tests
No E2E change is required. The existing restart-recovery E2E behavior and user
interface remain unchanged.
## Work orders
- [x] [Task 01: Classify backend-reload reports](task-01-classify-backend-reload-reports.md)
## Verification results
- `go test ./internal/system/frontenderrors -count=1`: 17 tests passed.
- Focused Vitest command: 3 files and 20 tests passed.
- `pnpm run typecheck`: passed.
- Specification lint and `git diff --check`: passed.
## Risks
- An untrusted browser can try to use the new source to reduce severity. The
backend accepts only the two stable recovery titles.
- Info entries do not appear on default warning-only stdout. They remain in the
retained backend log and diagnostic bundles.
@@ -0,0 +1,104 @@
---
id: "01-classify-backend-reload-reports"
title: "Classify backend-reload reports"
status: done
wave: 1
depends_on: []
plan: "plan.md"
requirements:
- REQ-PLATFORM-BACKEND-RESTART-PAGE-RECOVERY-001
- REQ-PLATFORM-DIAGNOSTIC-LOGGING-001
- REQ-PLATFORM-EXPECTED-RUNTIME-LOG-SEVERITY-001
acceptance_criteria:
- AC-PLATFORM-BACKEND-RESTART-PAGE-RECOVERY-001.3
- AC-PLATFORM-BACKEND-RESTART-PAGE-RECOVERY-001.5
- AC-PLATFORM-EXPECTED-RUNTIME-LOG-SEVERITY-001.17
- AC-PLATFORM-EXPECTED-RUNTIME-LOG-SEVERITY-001.18
system_design:
- ../../specs/platform/system-design/backend-restart-page-recovery.md
- ../../specs/platform/system-design/diagnostic-logging-01.md
- ../../specs/platform/system-design/diagnostic-logging-02.md
- ../../specs/platform/system-design/expected-runtime-log-severity.md
---
# Task 01: Classify Backend-Reload Reports
## Summary
Add a distinct source for expected backend-reload reports. Record this source
at info level while actual error-toast reports remain errors.
## In scope
- Add the `backend-reload` source to the frontend and backend contracts.
- Restrict the source to the two stable reload signals.
- Omit synthetic error-toast stacks and error objects from reload reports.
- Add backend observer tests and frontend payload tests.
## Out of scope
- Change the restart detector, reload coordinator, or reload alert.
- Rename the existing endpoint.
- Reclassify any other frontend or backend event.
## Acceptance
- A valid backend-reload report creates one info entry named
`frontend backend reload required` and no error entry.
- Sonner and legacy error-toast reports keep their current error entry.
- The frontend sends one reload report with browser context and no synthetic
toast-error data.
## Verification
```bash
cd apps/backend
go test ./internal/system/frontenderrors -count=1
```
```bash
cd apps/web
pnpm exec vitest run lib/api/domains/frontend-error-log-api.test.ts lib/platform/backend-reload-coordinator.test.ts hooks/domains/system/use-backend-generation-guard.test.ts
```
```bash
cd apps/web
pnpm run typecheck
```
## Files likely touched
- `apps/backend/internal/system/frontenderrors/handler.go`
- `apps/backend/internal/system/frontenderrors/handler_test.go`
- `apps/web/lib/api/domains/frontend-error-log-api.ts`
- `apps/web/lib/api/domains/frontend-error-log-api.test.ts`
- `apps/web/src/main.tsx`
## Dependencies
None.
## Risks
- The backend must validate the source and signal before it selects info level.
- Existing toast reports must keep their message, fields, and error severity.
## Parallelism
`sequential`
## Inputs
- The requirements and system designs in the frontmatter.
- The observer-backed frontend-error handler tests.
- The backend-reload coordinator and generation-guard tests.
## Results
- RED: the backend rejected `backend-reload` with HTTP 400.
- RED: the frontend added the synthetic toast stack and error object.
- RED: the incomplete allow-list accepted invalid recovery reports and rejected
`settings_interlock_rejected`.
- GREEN: the backend package passed 17 tests.
- GREEN: the three focused frontend files passed 20 tests.
- GREEN: the web typecheck passed.
@@ -2,7 +2,7 @@
status: active
system: platform
created: 2026-08-23
updated: 2026-08-28
updated: 2026-09-05
owners:
- kandev
---
@@ -37,6 +37,8 @@ This behavior obscures actionable errors and makes diagnostic bundles harder to
- **AC-PLATFORM-EXPECTED-RUNTIME-LOG-SEVERITY-001.14:** **GIVEN** a child emits unstructured stderr, **WHEN** the launcher forwards the line, **THEN** the parent records it at warning level.
- **AC-PLATFORM-EXPECTED-RUNTIME-LOG-SEVERITY-001.15:** **GIVEN** a session references a missing task environment, **WHEN** workspace information is loaded, **THEN** the task fallback succeeds without a warning entry.
- **AC-PLATFORM-EXPECTED-RUNTIME-LOG-SEVERITY-001.16:** **GIVEN** a task-environment lookup returns another error, **WHEN** workspace information is loaded, **THEN** the existing fallback and warning behavior remains unchanged.
- **AC-PLATFORM-EXPECTED-RUNTIME-LOG-SEVERITY-001.17:** **GIVEN** an open page detects a changed backend `boot_id`, **WHEN** its recovery report is accepted, **THEN** the backend records one info-level reload entry. It records no `frontend error toast` error entry.
- **AC-PLATFORM-EXPECTED-RUNTIME-LOG-SEVERITY-001.18:** **GIVEN** Sonner or the legacy toast provider displays an error toast, **WHEN** its report is accepted, **THEN** the backend records one `frontend error toast` error entry.
## Migrated source detail
@@ -91,6 +93,9 @@ status and message path.
The task fallback remains authoritative for workspace information.
- Other task-environment lookup errors can indicate a storage problem and
remain warnings.
- A changed backend generation is an expected lifecycle condition. Its browser
report remains diagnostic evidence, but it is not an error-toast report.
- Actual error toasts keep their error-level records.
## Persistence guarantees
@@ -125,6 +130,11 @@ persistence, or cleanup behavior across backend restarts.
- **GIVEN** a task-environment lookup returns another error, **WHEN** workspace
information is loaded, **THEN** the existing fallback and warning behavior
remains unchanged.
- **GIVEN** an open page detects a changed backend `boot_id`, **WHEN** its
recovery report is accepted, **THEN** one info entry records the event and no
`frontend error toast` error entry is emitted.
- **GIVEN** an actual error toast is displayed, **WHEN** its report is accepted,
**THEN** the existing `frontend error toast` error entry remains unchanged.
## Out of scope
@@ -133,3 +143,5 @@ persistence, or cleanup behavior across backend restarts.
or startup reconciliation.
- Reclassifying integration failures, raw ACP frames, host-utility idle repair,
network disconnects, or authentication warnings.
- Changing backend-restart detection, the reload-required state, or the reload
alert.
@@ -4,7 +4,7 @@ system: platform
requirements:
- REQ-PLATFORM-BACKEND-RESTART-PAGE-RECOVERY-001
created: 2026-09-03
updated: 2026-09-03
updated: 2026-09-05
owners:
- kandev
---
@@ -167,9 +167,15 @@ not expose the token.
## Observability
The coordinator sends one frontend diagnostic report when a document first
enters reload-required state. The report includes the signal source and no
token value. Valid sources are `boot_id_changed` and
`settings_interlock_rejected`.
enters reload-required state. The report uses the `backend-reload` report
source. Its title is `boot_id_changed` or `settings_interlock_rejected`.
The report includes browser context and no token value. It does not include a
synthetic error-toast stack or error object. The backend records the report at
info level with the fixed message `frontend backend reload required`.
Actual error-toast reports keep their error severity and fixed message. The
client cannot supply a log level or message template.
Backend logs keep the current HTTP request and restart evidence. This design
does not add a metric or durable event.
@@ -80,6 +80,8 @@ history or returning an unbounded log export.
- Every error toast displayed through either supported frontend toast system
is reported immediately to the backend and written as an `error` entry named
`frontend error toast`.
- A backend-reload alert is not an error toast. Its allow-listed recovery report
uses the info-level contract in part 2 of this design.
- Toast reporting includes the visible toast text plus rich browser context:
the browser URL origin and pathname with query and fragment removed, browser
identity fields, viewport dimensions, a client timestamp, the toast
@@ -51,7 +51,10 @@ The authenticated browser submits:
}
```
- `source` is `sonner` or `toast-provider`.
- `source` is `sonner`, `toast-provider`, or `backend-reload`.
- For `backend-reload`, `title` is `boot_id_changed` or
`settings_interlock_rejected`. The report omits `description`, `stack`, and
`error`.
- `title` and `description` are optional individually, but at least one must
contain visible text.
- `client_timestamp` is RFC 3339 when supplied. The backend log timestamp
@@ -73,8 +76,12 @@ The authenticated browser submits:
- An exhausted identity or process-wide report bucket returns
`429 Too Many Requests` with `Retry-After`.
The endpoint logs one structured `error` entry. Client fields remain fields;
they cannot replace the fixed `frontend error toast` message.
For `sonner` and `toast-provider`, the endpoint logs one structured error entry
with the fixed message `frontend error toast`.
For `backend-reload`, the endpoint logs one structured info entry with the
fixed message `frontend backend reload required`. Client fields cannot replace
the fixed message or select a log level.
### Diagnostic bundle jobs
@@ -307,8 +314,8 @@ with the same task/session rules and excludes message bodies and user identity.
the caller's equivalent active job. It does not create unbounded concurrent
archive or capture work.
- If the browser report endpoint receives a network, authentication,
validation, or server failure, the original error toast remains unchanged
and the reporting promise is discarded without retry.
validation, or server failure, the original UI state remains unchanged. The
reporting promise is discarded without retry.
- Unsupported or non-text toast content is omitted from the corresponding text
field; reporting proceeds only when visible text can be extracted.
- Daily rollover uses UTC calendar boundaries and never uses client timestamps
@@ -339,7 +346,8 @@ with the same task/session rules and excludes message bodies and user identity.
after becoming downloadable. They are not durable product data.
- Improve Kandev's task-context ZIP is a separate internal lease on the shared
archive builder and remains available for up to 24 hours.
- No toast report is stored in SQLite or queued in browser storage for retry.
- No toast or backend-reload report is stored in SQLite or queued in browser
storage for retry.
- Concurrent backend processes sharing one Kandev home are unsupported.
## Scenarios
@@ -367,6 +375,9 @@ with the same task/session rules and excludes message bodies and user identity.
- **GIVEN** an error toast on a recognized task route, **WHEN** its report is
accepted, **THEN** one backend error entry includes its visible text,
browser context, and `task_id`.
- **GIVEN** a page detects a changed backend generation, **WHEN** its report is
accepted, **THEN** one backend info entry includes the recovery signal and
browser context. No error-toast entry is emitted.
- **GIVEN** console activity over three days, **WHEN** browser retention runs,
**THEN** expired and oldest-over-cap entries are removed without uploading
retained entries.
@@ -4,6 +4,7 @@ system: platform
requirements:
- REQ-PLATFORM-EXPECTED-RUNTIME-LOG-SEVERITY-001
created: 2026-08-28
updated: 2026-09-05
owners:
- kandev
---
@@ -11,17 +12,18 @@ owners:
## Purpose and boundaries
This design preserves the severity of recognized child logs. It also removes
warnings from one expected task-environment fallback.
This design preserves the severity of recognized child logs. It also assigns
non-error levels to expected task-environment and backend-restart conditions.
The design does not change process control, workspace selection, or returned
errors. Unknown child stderr remains visible at warning level.
The design does not change process control, workspace selection, restart
detection, or returned errors. Unknown child stderr remains visible at warning
level.
## Requirement mapping
| Requirement | Design section |
| --- | --- |
| `REQ-PLATFORM-EXPECTED-RUNTIME-LOG-SEVERITY-001` | [Child log forwarding](#child-log-forwarding) and [Task-environment fallback](#task-environment-fallback) |
| `REQ-PLATFORM-EXPECTED-RUNTIME-LOG-SEVERITY-001` | [Child log forwarding](#child-log-forwarding), [Task-environment fallback](#task-environment-fallback), and [Frontend restart recovery](#frontend-restart-recovery) |
## Child log forwarding
@@ -52,6 +54,24 @@ lookup error keeps the current warning and fallback behavior.
The fallback result, workspace metadata, and caller response do not change.
## Frontend restart recovery
An open application page compares its boot ID with the boot ID of a reconnected
backend. A mismatch is an expected process-lifecycle condition.
The frontend sends this condition with the allow-listed `backend-reload`
report source. The title contains `boot_id_changed` or
`settings_interlock_rejected`. The report does not contain an error-toast stack
or error object.
The backend accepts only these two titles for the `backend-reload` source. It
records the report at info level with the fixed message
`frontend backend reload required`.
The `sonner` and `toast-provider` sources keep the fixed
`frontend error toast` message at error level. A client cannot select an
arbitrary severity or log message.
## Failure and recovery
An unknown child record remains a warning. This rule prevents a parser change
@@ -61,6 +81,10 @@ An unexpected task-environment lookup error remains a warning. The task-based
fallback can still return usable workspace information, but the storage error
remains diagnostic evidence.
An invalid frontend report remains a bad request. A valid reload report cannot
reduce the severity of an actual toast report because each source has a fixed
severity.
## Observability
The parent log keeps the original child line and `stream` field. Only the parent
@@ -69,7 +93,14 @@ severity changes when the parser recognizes the child severity.
The stale task-environment path keeps the session and environment identifiers
at debug level. Unexpected lookup errors keep the existing warning fields.
The reload entry keeps the browser context and stable recovery signal. It does
not use the error-toast message or a synthetic error stack.
## Test strategy
Table tests cover Zap, Go `slog`, malformed, and unstructured child lines.
Observer-backed service tests cover typed not-found and unexpected lookup errors.
Observer-backed frontend-report tests cover the reload info entry and the
unchanged error-toast entry. Frontend tests cover the distinct report source
and the absence of a synthetic toast stack.