digidem / digidem/comapeo-cloud-app

feat: per-server diagnostics export — capture raw archive responses with validation results for agent debugging

Open
#326 0 comments 0 reactions 0 assignees View on GitHub

Nobody has claimed this yet.

agent:ready-for-spec difficulty:hard lane:spec
Dominant language
TypeScript
Stars
0
Forks
0
Avg merge
1d 7h
Merged PRs (30d)
29

Description

Problem

When a connected remote archive server returns unexpected responses, there is no way for a user (or an agent debugging on their behalf) to inspect what the server actually sent:

  • handleResponse() (src/lib/api-client.ts:285-339) runs v.parse(schema, body) — on schema mismatch Valibot throws and the raw body is discarded.
  • On HTTP errors, ApiError keeps status/code/message but not the body.
  • Partial failures surface only as console.warn strings (src/lib/remote-archive.ts:359) and sync-result error messages — nothing persistent or exportable.
  • DevTools Network export is manual, includes credentials, and not feasible for field users.

Proposed design — on-demand per-server diagnostics export

An explicit "Run diagnostics" action (Advanced Settings / ArchiveServerDetail) that performs a bounded, one-shot capture over the existing archive transport and exports a single JSON bundle designed for agent consumption.

Design validated by dual review (Opus 5 + Codex GPT-5.6, both with repo access — consensus on all points below).

Architecture: capture at the existing seam, not a parallel client
shared archive transport (api-client)
  → bounded body reader (response.text(), capped)
  → JSON decoding
  → v.safeParse validation
  → normal client classification
  → optional diagnostic observer (module-scoped sink, armed only for one run)
  • Refactor handleResponse to await response.text()JSON.parsev.safeParse and emit to an off-by-default observer sink armed only for the duration of one diagnostics run. Diagnostics then calls the ordinary apiClient methods in order.
  • Rationale: a standalone replay client would duplicate resolveApiRequest() proxy-header logic, getAuthHeaders(), classify404(), and unsupported-endpoint fallbacks — and would diagnose a code path that isn't the failing one.
  • Going through apiClient also gives the security-startup gate for free (resolveApiRequest:136-143).
  • Do NOT attach raw bodies to ApiError globally — those errors reach logs, UI state, and telemetry.
Capture scope (V1)
  • /info, /healthcheck, /projects, then per selected project: project detail, observations, tracks, alerts, presets, fields.
  • No icons in V1 — byte-length still requires full download (proxy has no HEAD), and the icon handler does upstream SVG→PNG fallback fetches.
  • Project scope selection when the archive exceeds a small threshold, with estimated request count shown. Sequential or concurrency 1-2. Cancel support with partial export. No overlap with an active sync or another diagnostics run.
Bundle format (per-endpoint entry envelope)
{
  "endpoint": "observations",
  "projectId": "…",
  "request": { "method": "GET", "path": "/projects/…/observations", "via": "same-origin-api-proxy" },
  "durationMs": 84,
  "outcome": "response",
  "response": {
    "status": 200,
    "source": "proxy | upstream",
    "headers": {},
    "body": { "text": "…", "bytesCaptured": 102400, "truncated": true }
  },
  "schemaId": "observationsResponse",
  "validation": { "status": "schema-mismatch", "issues": [{ "path": "$.data[3].lat", "kind": "…", "expected": "number", "receivedType": "string", "message": "…" }] },
  "clientClassification": "schema-error"
}
  • Validation states: 'ok' | 'invalid-json' | 'schema-mismatch' | 'response-too-large' | 'not-applicable' | 'not-run'. Network failures/aborts/skips use the same envelope without a response.
  • source: proxy | upstream (Opus finding): functions/api/_middleware.ts synthesizes responses the server never sent (405 UNSUPPORTED_ARCHIVE_PROXY_PATH, 428 security update, 502 upstream failure). Without provenance an agent misdiagnoses the archive server. Proxy-added headers (Cache-Control, Strict-Transport-Security, …) must be labeled as the proxy's.
  • Include schemaId + exact app release SHA (VITE_APP_RELEASE; package.json reports 0.0.0). Do NOT inline schema source — it lives in src/lib/schemas/ and drifts. Label success as "accepted by app schema", not "exact match" (schemas strip unknown keys).
  • Do NOT serialize Valibot issue.path directly — entries contain full ancestor inputs (territary data + bundle blowup). Normalize to JSONPath-style strings.
  • Optional (Opus): recursive type-only skeleton of the raw body (arrays → first-element shape + length) — diagnostic without territory data.
Redaction — dedicated credential-only scrubber
  • Do NOT reuse sanitizeTelemetry: it redacts geometry, lat, lon, coordinates, tags, observations, projects (telemetry-redaction.ts:29-61) — exactly the fields a mismatch lives in — and sets saturated on arrays >100 items, which would replace every string with [REDACTED].
  • Write a narrow scrubber: exact runtime token + Bearer <token> from every captured string, then scan the final serialized JSON; if the token survives, omit the offending field or fall back to a minimal safe bundle. Reuse the credential-pattern idea + canary tests from sanitizeTelemetryString:157-166.
  • Optional UI toggle (default on): "redact coordinate values" (type-preserving replacement — schema diagnosis needs received: "string", not the actual coordinate).
Security invariants (must match the #288 contract)
  • Must go through the security-startup gate (automatic via apiClient/resolveApiRequest).
  • Same-origin /api proxy + credential-revision header; SW NetworkOnly for /api/**; cache: 'no-store' (also add it to getIcon — currently the only method missing it — if icons are ever probed).
  • Identity-bound 401 locking preserved; stop authenticated fan-out after the first 401 (don't repeatedly submit a rejected token; Codex also suggests a candidate-credential pattern so diagnostics can't lock an unlocked archive mid-run).
  • Short-circuit remaining work on 429; capture Retry-After if exposed.
  • If /projects fails or is invalid, export that evidence and mark dependent calls skipped — never fan out from untrusted data.
  • Diagnostics must not bypass reconnect/approval flows for a locked archive. An already-unlocked HTTP archive need not re-prompt.
  • Hard caps: project count, total requests, total captured bytes, total bundle bytes, response headers, issues count, per-request and whole-run duration. 100KB × endpoint × project is NOT bounded on its own.
  • Strict response-header allowlist (content-type, content-length, retry-after, rate-limit headers, x-comapeo-* capability headers) — not "serialize everything and sanitize". Never serialize request headers, RequestConfig, Request/Response objects, raw Valibot issues, or error stacks.
  • Capture state lives only in function/component memory; the explicitly downloaded file is the only durable result. Sanitize + length-limit the server label in the filename.
  • Confirmation dialog: "This export may contain territory data, precise locations, names, tags, and attachment URLs. Share carefully."
Phasing
  1. Increment 1 — last-sync report export: sync.ts already computes per-resource warnings, ReconciliationCounts, and EndpointSemantics, and throws them away; ArchiveServerDetail.tsx references none of it. A "last sync report" panel + export covers unsupported endpoints and per-project detail failures for a fraction of the work.
  2. Increment 2 — raw-body replay diagnostics (this design), for the case structured warnings can't explain: schema mismatch.
  3. Keep the bundle format close to scripts/capture-archive-responses.sh output so tests/fixtures/ entries can be extracted from a field bundle.

Related findings (handle separately)

  • Upstream comapeo-cloud has no GET /projects/:id route. The app calls it for every project (src/lib/remote-archive.ts:338), producing the recurring warning at :359. Diagnostics should capture this 404, but the known mismatch deserves its own fix rather than being normalized as mysterious server behavior.
  • docs/remote-archive-api-spec.md:41-44 says v0.5.0+ moved observations to singular /observation, but archive-proxy.ts:117 allowlists only the plural — if diagnostics should probe the singular path, the proxy allowlist needs extending first.

Alternatives considered (rejected)

  • Always-on ring-buffer recorder: memory cost on mobile, misses failures unless pre-enabled.
  • MSW/dev-mode only: field users hitting strange servers are not developers.
  • DevTools HAR export: manual, includes credentials, not feasible for users.

Contributor guide

No contributing guide indexed for this repository

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. Open a pull request that references the issue number.

Research direction

Start with handleResponse in src/lib/api-client.ts and the diagnostics entry points named in ArchiveServerDetail.tsx; then read sync.ts, src/lib/remote-archive.ts, functions/api/_middleware.ts, and archive-proxy.ts. Confirm the existing security and proxy contracts before deciding how to phase the work. Done should include an explicit, bounded export with validation results, redaction, cancellation, and the stated security invariants.

Written by the indexing model from the issue text.

Assessment

Tech stack
typescript
Domain
backend-api-design, devtools, security
Issue type
Feature
Difficulty
5/5
Estimated time
Over a week
Activity status
Active
Clarity
Mostly clear
Newbie friendliness
32/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.