fix(desktop): classify authorization, launchd, cache-path, 5xx and staging updater failures as environmental

Merged
SA
2mo ago
+175-1

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

  1. 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.
  2. 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.
  3. 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 AuthorizationCreate fails with errAuthorizationCanceled (user dismissed the admin prompt) or errAuthorizationDenied. Foundation localises the sentence; the OSStatus domain 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 directory under ShipIt (DESKTOP-14Z): a staged copy lost its parent mid-unpack. I checked whether the error path already discards it: clearCachedUpdate runs on every non-network error (auto-updater.ts:405) and empties electron-updater's pending download, and Squirrel stages each attempt into a fresh update.<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; -60008 errAuthorizationInternal, 18 events, also still reports).
  • ENOTDIR added to ENVIRONMENT_ERRNO_CODES, behind the existing -updater / shipit path-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: MacUpdater forwards the native error event directly and also rejects the download promise, whose errorHandler calls dispatchError on 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 clearCachedUpdate runs 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: HttpError 5xx/618 from the feed yml fetch, ~20 events in 90 days. Same class, different message shape and carries statusCode; not in this task's issue list.
  • The comment in update-error-redaction.ts saying Sentry serialises non-standard error properties is not what the raw events show (extra is 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.