fix: classify backend reload diagnostics (#3414)
This commit is contained in:
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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.
|
||||
+104
@@ -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.
|
||||
|
||||
Reference in New Issue
Block a user