fix(desktop): report a missing macOS app as not installed, not a 500

SU
6h ago
+65-0

Problem

On macOS, "Open in " for an editor that is not installed is reported to Sentry as an unhandled INTERNAL_SERVER_ERROR. It is a user-environment condition, not a bug. This family has produced thousands of events (DESKTOP-15, archived but still firing), and each new build groups as a new issue (DESKTOP-1MZ).

Root cause

spawnAsync already classifies "app not installed" as PRECONDITION_FAILED / APP_NOT_INSTALLED, but only for the Linux shape: the spawn ENOENT when the editor CLI is not on PATH. On macOS the command is always open, which exists. open -a <name> reports an unresolvable app with exit code 1 and the stderr line Unable to find application named '<name>'. That went to the generic non-zero-exit branch as a plain Error. The tRPC Sentry middleware reports every INTERNAL_SERVER_ERROR by contract, so the event went to Sentry.

Fix

In the exit handler, classify exactly that case (command === "open" and stderr starting with Unable to find application named ) as the same PRECONDITION_FAILED / APP_NOT_INSTALLED error the ENOENT branch produces. The message is still the stderr text, so the toast the user sees does not change. Other open failures (for example a missing file) and other commands are not matched.

The fix is at the throw site, as sentryMiddleware's contract requires. No filter is added.

Verification

Tests written first. The positive case failed before the fix; the two negative cases (another open failure; the same stderr from a command other than open) pass before and after.

$ bun test src/lib/trpc/routers/external/   # before fix
(fail) spawnAsync > macOS open with an uninstalled app name is PRECONDITION_FAILED
 121 pass
 1 fail

$ bun test src/lib/trpc/routers/external/   # after fix
 122 pass
 0 fail
 131 expect() calls
Ran 122 tests across 1 file.

$ bunx biome check apps/desktop/src/lib/trpc/routers/external/helpers.ts apps/desktop/src/lib/trpc/routers/external/helpers.test.ts
Checked 2 files in 42ms. No fixes applied.

$ cd apps/desktop && bun run typecheck
$ tsc --noEmit        (no errors)

Review note: an automated reviewer suggested settling on close instead of exit, because exit can fire before stderr is drained. That change was tried and reverted. close also waits for any process still holding the stderr pipe, so a successful launch could stay pending. Every recorded event already carries the full stderr text captured on exit. If the race does happen, the error stays today's unclassified 500; nothing is wrongly classified. The final tree is identical to the first commit.

Not covered here: apps launched by bundle id (open -b: Zed, IntelliJ, PyCharm) print a different message, and Sentry has no events of that form to confirm its text against, so the matcher does not include it.

Refs DESKTOP-1MZ