Skip to content

Sentry: engine transport failures fragment into duplicate issues (per-operation × ephemeral port) #473

Description

@cravenceiling

Summary

When the engine sidecar is briefly unreachable, the frontend fetch to 127.0.0.1:<port> rejects with WebKit's TypeError: Load failed (127.0.0.1:<port>). A single underlying failure (engine momentarily unreachable) is currently reported to Sentry as many distinct issues, fragmented along three axes:

  1. Per operationread_agent_file, list_skills, list_project_files, check_provider_status, list_all_conversations, … each become their own Sentry issue.
  2. Per ephemeral port — the engine's port changes every launch and WebKit bakes it into the error message, so the issue title is unique per run.
  3. Per release / sourcemap state — older releases without resolved sourcemaps group by the minified frame (zA), newer ones by the real frame (HoustonClient.request).

Net effect: one bug currently spawns ~20+ separate Sentry issues and keeps creating new ones, which buries the signal and breaks escalation tracking.

Concrete example (the two issues this came from): the same read_agent_file failure exists as HOUSTON-APP-65 (7532375190, source-mapped, houston-app@0.4.19) and HOUSTON-APP-2 (7510343192, minified, houston-app@0.4.15) — identical exc.type: read_agent_file and Load failed, differing only by ephemeral port and minified-vs-source stack. (APP-65 kept as canonical; APP-2 muted as a dup.) The same split exists for list_all_conversations, check_provider_status, list_project_files, list_routine_runs, and more.

Root cause

app/src/lib/sentry-report-error.ts:

const error = new Error(message);   // message = "Load failed (127.0.0.1:52119)" — raw WebKit text, includes the ephemeral port
error.name = command;               // → Sentry exception type = operation name → per-operation split
error.stack = originalError.stack;  // real engine-client stack → per-method-frame split
  • error.name = commandmetadata.type becomes the operation label, so each op is a distinct Sentry exception type.
  • The preserved real stack differs per engine-client method (readAgentFile vs listSkills …), reinforcing the per-operation split.
  • message carries the raw WebKit TypeError text, which includes the ephemeral 127.0.0.1:<port> → volatile titles per app launch. (Confirmed: nothing in Houston appends the port — it comes straight from the WKWebView fetch rejection.)
  • (Historical, already fixed — not an action item.) Sourcemap upload was gated on an empty SENTRY_AUTH_TOKEN and skipped on every build (see the comment in .github/workflows/release.yml, ~L259–264); fixed by hoisting the token to job scope. That is why 0.4.15 frames are minified while 0.4.19 resolves. Sourcemaps are uploaded today (sentry-cli sourcemaps upload for both macOS and Windows in release.yml).

Note: the per-operation split was a deliberate earlier fix — engine errors used to all collapse into a single issue, so a 404 was indistinguishable from a network failure (see app/src/lib/tauri.ts, ~L84–86). The fix overcorrected; the goal now is the middle ground.

Proposed durable fix

Group by the kind of failure, not by operation or ephemeral port. Keep genuinely-different failures distinct (the original goal) while collapsing the engine-unreachable family into one tracked issue.

  1. Type transport failures in engine-client. In ui/engine-client/src/client.ts, wrap the fetch in request / rawRequest and throw a typed HoustonTransportError (carrying the original cause) when the fetch itself rejects (engine unreachable) — distinct from HoustonEngineError (a real HTTP response with a status). Do not rely on string-sniffing "Load failed": it is WebKit-specific and becomes "Failed to fetch" on the Windows WebView2 / Chromium webview.

  2. Stable fingerprint at capture. Extend captureException in app/src/lib/sentry.ts to accept a fingerprint, and in the report path set:

    • transport failure → fingerprint: ['engine-transport-failure']one "Engine unreachable" issue regardless of operation / port / release.
    • HoustonEngineErrorfingerprint: ['engine-http', String(status)] (optionally + a route group) → meaningful per-status issues.
    • keep the operation as the existing source tag for triage (already set in error-toast.ts), not as the group key.
  3. Sanitize the volatile port out of the title. Before building the Sentry error, normalize 127.0.0.1:<port>127.0.0.1 (or <engine>) in the message; stash the real port / base URL in Sentry extra/context, not the title.

  4. (Optional stopgap.) A Sentry server-side fingerprint/merge rule collapsing message:"Load failed*" would dedupe existing noise immediately, but the code fix above is the real cure and lives in-repo.

Acceptance criteria

  • A burst of engine-unreachable failures across N operations produces one Sentry issue, not N.
  • That issue's title is stable across app launches (no ephemeral port).
  • HTTP-status and domain errors stay distinct — no regression to the old "everything is one issue" behavior.
  • Unit tests cover the transport-vs-HTTP classification with verbatim WebKit (Load failed) and Chromium (Failed to fetch) fetch-rejection fixtures.

References

  • app/src/lib/sentry-report-error.tserror.name = command; message carries the ephemeral port
  • app/src/lib/tauri.ts (call / surfaceError, ~L50–92) — source of the operation label
  • app/src/lib/error-toast.ts (reportError / showErrorToast) — capture path + source / error_kind tags
  • app/src/lib/sentry.ts (captureException, ~L132–150) — extend to accept fingerprint
  • ui/engine-client/src/client.ts (request / rawRequest / toError, ~L103–164) — add the typed transport error
  • .github/workflows/release.yml — sourcemap upload (already wired); gate-bug context ~L259–264
  • Sentry: HOUSTON-APP-65 (7532375190, canonical) · HOUSTON-APP-2 (7510343192, muted dup)

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't working

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions