fix(desktop): classify authorization, launchd, cache-path, 5xx and staging updater failures as environmental
Problem
Six updater failures that happen on the user's machine reach Sentry.captureException from the autoUpdater.on("error") handler because isEnvironmentUpdateError does not recognise them. Together they are the largest source of desktop events: DESKTOP-7N alone is ~4,400 events (2,700 of them -60006, 1,500 -60005) and refires every 4-hour check on each affected machine.
Reality check
- User-visible harm: none that code here can remove. The user sees the updates pill in its error state with the raw message and a retry. What these events cost is us: quota on the uncapped desktop DSN, and real updater defects (the signature failures in DESKTOP-14Y / DESKTOP-15M) buried under thousands of non-actionable entries.
- Reach: the events come from stable 1.21.0–1.26.0, all through
apps/desktop/src/main/lib/auto-updater.ts→isEnvironmentUpdateError. This ships in the next desktop release and stops the capture on every machine that adopts it; machines stuck on an older build (some of these users cannot install updates at all) keep reporting until they do. - Why code: archiving does not stop ingestion or quota, and a Sentry-side filter is forbidden by the PR #6164 contract (classify at the capture site only). The classifier module exists precisely for this. No in-flight work restructures this path; #7273 (merged 2026-09-08) only touched the
net::ERR_branch and is not retouched.
Root cause
None of the six is a defect in the artifact or feed, and we do not own any of the producers:
- -60006 / -60005 (DESKTOP-7N, DESKTOP-FZ): Squirrel.Mac launches ShipIt privileged when the app bundle or its parent is not writable by the user, and
AuthorizationCreatefails witherrAuthorizationCanceled(user dismissed the admin prompt) orerrAuthorizationDenied. Foundation localises the sentence; theOSStatusdomain name and number do not change. - "The command is disabled and cannot be executed" (DESKTOP-8Z): launchd refusing to start the ShipIt job because the user or an MDM disabled it. Only the sentence reaches the message.
- ENOTDIR on the updater cache path (DESKTOP-159): a file sits where electron-updater's cache directory must be.
Cannot download …, status 504(DESKTOP-45): a server error from the release CDN.ditto: … app.asar: No such file or directoryunder ShipIt (DESKTOP-14Z): a staged copy lost its parent mid-unpack. I checked whether the error path already discards it:clearCachedUpdateruns on every non-network error (auto-updater.ts:405) and empties electron-updater's pending download, and Squirrel stages each attempt into a freshupdate.<id>directory, so nothing reuses the half-staged copy. Classification alone is the correct fix here; no cache-clearing change was needed.
Fix
apps/desktop/src/main/lib/update-error-classification.ts, matched on the most structural signal each failure offers, in the order the task set:
/OSStatus\D*-6000[56](?!\d)/— the domain's name and the number token, never the English or German prose. Any other OSStatus keeps reporting (a signing failure,-67062, wears the same sentence;-60008errAuthorizationInternal, 18 events, also still reports).ENOTDIRadded toENVIRONMENT_ERRNO_CODES, behind the existing-updater/shipitpath-marker guard./^Cannot download ".*", status 5\d\d(?!\d)/— 5xx on the release download only; 404 stays reported because a missing asset is ours.ditto:+shipit+no such file or directory— ditto prints no errno; Darwin's strerror is not localised."the command is disabled and cannot be executed"— wording, because no code is available in the message.
The defect-first ordering is untouched: UPDATER_DEFECT_PATTERNS still short-circuits before any of these, so a code-signature failure whose message also carries -60006 still reports (pinned by a test). Electron does attach code/domain from the NSError to the error object, but our Sentry SDK does not serialise those properties, so they cannot be verified against production; the message token was verified against every event message in DESKTOP-7N and DESKTOP-FZ.
What still reports after this change: every UPDATER_DEFECT_PATTERNS entry (checksum, code signature, codesign, cannot parse update info, cannot find channel, no files provided), including DESKTOP-14Y / DESKTOP-15M; any other OSStatus code; 4xx on the download; errno failures off updater paths; ditto failures other than a missing file under ShipIt; HttpError 5xx on the feed yml fetch (DESKTOP-QX, not in scope, noted below).
No typed cause added: nothing reads one here, the handler only branches on the boolean.
Verification
Tests were written first. Against the unchanged classifier the five positive cases fail and every negative case passes:
9 pass
5 fail
39 expect() calls
Ran 14 tests across 1 file. [117.00ms]
After the change:
$ bunx biome check apps/desktop/src/main/lib/update-error-classification.ts apps/desktop/src/main/lib/update-error-classification.test.ts
Checked 2 files in 6ms. No fixes applied.
$ cd apps/desktop && bun run typecheck
$ tsc --noEmit
(clean)
$ cd apps/desktop && bun test src/main/lib/update-error-classification.test.ts
14 pass
0 fail
48 expect() calls
Ran 14 tests across 1 file. [113.00ms]
$ cd apps/desktop && bun test src/main/lib
664 pass
0 fail
1337 expect() calls
Ran 664 tests across 46 files. [1.90s]
No desktop git code touched, so the main-thread git ratchet test was not in the affected set.
Adjacent, left alone
- One native Squirrel failure is emitted twice on our
autoUpdater:MacUpdaterforwards the nativeerrorevent directly and also rejects the download promise, whoseerrorHandlercallsdispatchErroron the same object. Every Squirrel-side failure (including DESKTOP-14Y) is captured twice. Upstream behaviour, not changed here. - The affected machines re-download the archive and re-prompt for an admin password every 4 hours because
clearCachedUpdateruns on the authorization failure too. Stopping that is a product decision (prompt once, or stop auto-download when the install target is not writable), not a classification change. - DESKTOP-QX:
HttpError5xx/618 from the feed yml fetch, ~20 events in 90 days. Same class, different message shape and carriesstatusCode; not in this task's issue list. - The comment in
update-error-redaction.tssaying Sentry serialises non-standard error properties is not what the raw events show (extrais null); nothing depends on it.
Refs DESKTOP-7N
Refs DESKTOP-FZ
Refs DESKTOP-8Z
Refs DESKTOP-159
Refs DESKTOP-45
Refs DESKTOP-14Z
https://claude.ai/code/session_016FJrpE7y2akoSBfbnGXxZc
Summary by cubic
Classifies six user-machine updater failures as environmental so they stop reaching Sentry, cutting the largest source of desktop error events.
- The failures are a cancelled or refused admin authorization (-60006/-60005), launchd disabling the ShipIt job, ENOTDIR on the updater cache path, a 5xx release download, and ditto losing a file during ShipIt staging.
- DESKTOP-7N alone accounts for ~4,400 events, refiring every 4 hours on each affected machine.
- Signature failures, 404s, feed-fetch 5xxs, and other OSStatus codes still report because the defect-first ordering is unchanged.
Summary by CodeRabbit
- Bug Fixes
- Improved desktop update error handling for authorization cancellations, disabled updater services, unavailable download servers, missing staged files, and invalid updater paths.
- More accurately distinguishes environment-related update failures from updater defects, reducing incorrect error classifications.
- Added coverage for signature validation failures, authorization status codes, and file-system conditions to ensure consistent update diagnostics.