Summary
After the 0.12.0 stable rollout, PostHog shows a sizeable Update Check Failed bucket emitted by clients already running 0.12.0 / build 12000:
Update Check Failed from source app 0.12.0 / 12000, update_channel=stable, update_failure_reason=appcast_retrieval
- 24h sample during release audit: ~82 users / 123 events for stable appcast retrieval failures
- These are post-update checks from 0.12.0 clients, not primarily pre-0.12 clients failing to update to 0.12.
This issue tracks both:
- root-causing/reducing the Sparkle appcast retrieval failures; and
- improving updater instrumentation so future release checks can distinguish source build, target build, channel, network/server/parsing causes, and nested Sparkle errors without inference.
Evidence from production telemetry
Source vs target interpretation
$app_version / $app_build on updater events are the source client emitting the event, not the target release.
For users who emitted the 0.12 appcast retrieval failure:
| Signal |
Count |
| Users with a 0.12 update failure |
98 |
Had prior older-build Update Available |
96 |
Had prior older-build Update Installed |
93 |
Failure happened after an observed 0.12.0 App Launched |
96 |
Failure after a 0.12.0 Update Installed event |
0 (expected: install event is logged by the old app before relaunch) |
Representative sequence shape, sanitized:
- Client on
0.11.x emits Update Available.
- Same client emits
Update Installed while still reporting the old $app_version / $app_build.
- Same user relaunches and emits
App Launched as 0.12.0 / 12000.
- Hours later, same user emits
Update Check Failed as 0.12.0 / 12000.
So the largest bucket is users already on 0.12.0 checking the appcast, not users blocked while trying to retrieve 0.12.0.
Failure breakdown
Recent 24h diagnostic counts for source app 0.12.0 / build 12000:
| update_channel |
reason |
domain/code |
underlying |
launch location |
users / events |
| stable |
appcast_retrieval |
SUSparkleErrorDomain / 2001 |
SUSparkleErrorDomain / 2001 |
applications_system |
~81 / 122 |
| beta |
download |
SUSparkleErrorDomain / 2001 |
NSURLErrorDomain / -1005 |
applications_system |
~6 / 7 |
| beta |
appcast_retrieval |
SUSparkleErrorDomain / 2001 |
SUSparkleErrorDomain / 2001 |
applications_system |
~4 / 8 |
| stable |
read_only_location |
SUSparkleErrorDomain / 1003 |
none |
dmg_mounted |
~2 / 6 |
| beta |
download |
SUSparkleErrorDomain / 2001 |
NSURLErrorDomain / -1001 |
applications_system |
~2 / 2 |
| stable |
unknown |
SUSparkleErrorDomain / 1000 |
NSXMLParserErrorDomain / 68 |
applications_system |
~1 / 1 |
User retry distribution for source 0.12.0 appcast retrieval failures:
| failures per user |
users |
| 1 |
62 |
| 2 |
10 |
| 3 |
8 |
| 4 |
5 |
| 5 |
1 |
Server-side checks did not show appcast 5xx
Read-only Cloud Logging checks for /v2/desktop/appcast.xml over the same release window showed only 200 responses in returned samples:
- last 2h direct Cloud Run/appcast logs: 1,298 entries, all
200
- last 6h direct Cloud Run/appcast logs: 3,747 entries, all
200
- broader appcast HTTP log samples were also all
200 until query truncation
Live appcast is XML-parseable and currently contains:
- stable:
0.12.0+12000
- beta:
0.12.2+12002 at time of probe (previously 0.12.1+12001 during the audit)
This points away from a broad server outage. The current best hypothesis is a client/Sparkle-side appcast download failure path or transient network/background fetch issue, but instrumentation is not deep enough to confirm the exact cause.
Code pointers
Desktop/Sources/UpdaterViewModel.swift
UpdateFailureDiagnostics.analyticsProperties currently records:
update_failure_reason
update_failure_domain
update_failure_code
update_channel
launch_location_bucket
- first-level
underlying_domain / underlying_code
UpdaterDelegate.didAbortWithError classifies errors and emits Update Check Failed.
allowedChannels(for:) uses UserDefaults.update_channel: stable returns an empty set, beta returns Set(["beta"]).
Desktop/Sources/PostHogManager.swift
updateAvailable(version:) and updateInstalled(version:) only emit version; they do not emit target build/channel/source context.
- Sparkle source in
.build/checkouts/Sparkle/Sparkle/SUAppcastDriver.m
- appcast download failures are wrapped as
SUSparkleErrorDomain / SUDownloadError (2001) with a localized message: “An error occurred in retrieving update information.”
- nested underlying error information can be more informative than the first-level wrapper.
Likely contributing factors / hypotheses to test
-
Transient client network/background download failures during automatic checks.
- Server logs show appcast
200s, and most affected users only have one failure.
- Need deeper nested
NSURLErrorDomain / failing URL / HTTP response data to confirm.
-
Sparkle wrapper hides the real underlying error.
- Stable appcast failures often show first-level underlying as another Sparkle
2001, so telemetry loses the lower-level cause.
- The beta download path does expose nested
NSURLErrorDomain values (-1005, -1001, -1004), suggesting deeper traversal would help.
-
Post-0.12 check cadence increased exposure.
- 0.12 initializes Sparkle early and triggers an immediate background check after launch, then checks every 10 minutes for release builds.
- This may surface expected transient appcast fetch failures more prominently after a mass update.
-
Channel/target ambiguity makes release-readiness interpretation harder.
Update Available / Update Installed do not include update_channel, target_version, or target_build.
Update Check Failed includes update_channel but not target version/build or appcast URL.
Proposed fixes / follow-up
A. Improve updater instrumentation
Add properties to all updater events (Update Available, Update Installed, Update Check Failed, and ideally Update Check Started):
- source app context:
source_app_version
source_app_build
- existing
$app_version / $app_build still ok, but explicit names reduce ambiguity
- target/update context when available:
target_version / to_version
target_build
item_channel / update_channel
appcast_url or appcast_url_host + path (avoid secrets; appcast URL is public)
appcast_item_count
latest_stable_version, latest_stable_build
latest_beta_version, latest_beta_build
- Sparkle/client diagnostics on failure:
- full nested error chain:
error_chain_domains, error_chain_codes
localized_failure_reason, sanitized/truncated
failing_url_host, failing_url_path
nsurl_error_code if any nested NSURLErrorDomain
- HTTP status if Sparkle exposes one
background_check vs manual check if available
can_check_for_updates, session_in_progress, automatically_checks_for_updates, automatically_downloads_updates
launch_location_bucket (already present; keep)
B. Classify failures more precisely
- Traverse nested
NSUnderlyingErrorKey recursively before classifying.
- If any nested error is
NSURLErrorDomain, classify as network or a more specific network_connection_lost, timed_out, cannot_connect, etc., instead of generic appcast_retrieval.
- Consider a separate reason for
sparkle_appcast_download_wrapper vs XML parse/signature failure.
C. Product behavior / mitigation
- Avoid surfacing one-off automatic background appcast retrieval failures as alarming user-visible errors unless repeated or manual.
- For automatic checks, consider retry/backoff before setting
lastUpdateFailure / prompting manual download.
- Track consecutive failures per install/session locally and only show UI after threshold.
D. Server observability
- Add/update a dashboard tile comparing appcast request 2xx/4xx/5xx vs client
Update Check Failed counts by source build/channel.
- If feasible, stamp response cache status / region in server logs for
/v2/desktop/appcast.xml.
Acceptance criteria
- Release dashboards can answer “is this error happening before or after users update?” without person-level sequence inference.
Update Available / Update Installed events include enough target metadata to identify the destination release/channel.
Update Check Failed includes nested error chain and failing URL/HTTP status where available.
- Automatic one-off appcast fetch failures are either retried silently or differentiated from actionable update failures.
- A follow-up release-readiness query can separate:
- old clients failing to retrieve/install the stable update;
- current stable clients checking stable and failing;
- current stable clients trying beta and failing;
- local filesystem/location failures.
Suggested labels
bug, desktop, analytics, p2
Summary
After the 0.12.0 stable rollout, PostHog shows a sizeable
Update Check Failedbucket emitted by clients already running0.12.0/ build12000:Update Check Failedfrom source app0.12.0/12000,update_channel=stable,update_failure_reason=appcast_retrievalThis issue tracks both:
Evidence from production telemetry
Source vs target interpretation
$app_version/$app_buildon updater events are the source client emitting the event, not the target release.For users who emitted the 0.12 appcast retrieval failure:
Update AvailableUpdate Installed0.12.0App Launched0.12.0Update InstalledeventRepresentative sequence shape, sanitized:
0.11.xemitsUpdate Available.Update Installedwhile still reporting the old$app_version/$app_build.App Launchedas0.12.0/12000.Update Check Failedas0.12.0/12000.So the largest bucket is users already on 0.12.0 checking the appcast, not users blocked while trying to retrieve 0.12.0.
Failure breakdown
Recent 24h diagnostic counts for source app
0.12.0/ build12000:appcast_retrievalSUSparkleErrorDomain/2001SUSparkleErrorDomain/2001applications_systemdownloadSUSparkleErrorDomain/2001NSURLErrorDomain/-1005applications_systemappcast_retrievalSUSparkleErrorDomain/2001SUSparkleErrorDomain/2001applications_systemread_only_locationSUSparkleErrorDomain/1003dmg_mounteddownloadSUSparkleErrorDomain/2001NSURLErrorDomain/-1001applications_systemunknownSUSparkleErrorDomain/1000NSXMLParserErrorDomain/68applications_systemUser retry distribution for source
0.12.0appcast retrieval failures:Server-side checks did not show appcast 5xx
Read-only Cloud Logging checks for
/v2/desktop/appcast.xmlover the same release window showed only200responses in returned samples:200200200until query truncationLive appcast is XML-parseable and currently contains:
0.12.0+120000.12.2+12002at time of probe (previously 0.12.1+12001 during the audit)This points away from a broad server outage. The current best hypothesis is a client/Sparkle-side appcast download failure path or transient network/background fetch issue, but instrumentation is not deep enough to confirm the exact cause.
Code pointers
Desktop/Sources/UpdaterViewModel.swiftUpdateFailureDiagnostics.analyticsPropertiescurrently records:update_failure_reasonupdate_failure_domainupdate_failure_codeupdate_channellaunch_location_bucketunderlying_domain/underlying_codeUpdaterDelegate.didAbortWithErrorclassifies errors and emitsUpdate Check Failed.allowedChannels(for:)usesUserDefaults.update_channel: stable returns an empty set, beta returnsSet(["beta"]).Desktop/Sources/PostHogManager.swiftupdateAvailable(version:)andupdateInstalled(version:)only emitversion; they do not emit target build/channel/source context..build/checkouts/Sparkle/Sparkle/SUAppcastDriver.mSUSparkleErrorDomain/SUDownloadError(2001) with a localized message: “An error occurred in retrieving update information.”Likely contributing factors / hypotheses to test
Transient client network/background download failures during automatic checks.
200s, and most affected users only have one failure.NSURLErrorDomain/ failing URL / HTTP response data to confirm.Sparkle wrapper hides the real underlying error.
2001, so telemetry loses the lower-level cause.NSURLErrorDomainvalues (-1005,-1001,-1004), suggesting deeper traversal would help.Post-0.12 check cadence increased exposure.
Channel/target ambiguity makes release-readiness interpretation harder.
Update Available/Update Installeddo not includeupdate_channel,target_version, ortarget_build.Update Check Failedincludesupdate_channelbut not target version/build or appcast URL.Proposed fixes / follow-up
A. Improve updater instrumentation
Add properties to all updater events (
Update Available,Update Installed,Update Check Failed, and ideallyUpdate Check Started):source_app_versionsource_app_build$app_version/$app_buildstill ok, but explicit names reduce ambiguitytarget_version/to_versiontarget_builditem_channel/update_channelappcast_urlorappcast_url_host+ path (avoid secrets; appcast URL is public)appcast_item_countlatest_stable_version,latest_stable_buildlatest_beta_version,latest_beta_builderror_chain_domains,error_chain_codeslocalized_failure_reason, sanitized/truncatedfailing_url_host,failing_url_pathnsurl_error_codeif any nestedNSURLErrorDomainbackground_checkvs manual check if availablecan_check_for_updates,session_in_progress,automatically_checks_for_updates,automatically_downloads_updateslaunch_location_bucket(already present; keep)B. Classify failures more precisely
NSUnderlyingErrorKeyrecursively before classifying.NSURLErrorDomain, classify asnetworkor a more specificnetwork_connection_lost,timed_out,cannot_connect, etc., instead of genericappcast_retrieval.sparkle_appcast_download_wrappervs XML parse/signature failure.C. Product behavior / mitigation
lastUpdateFailure/ prompting manual download.D. Server observability
Update Check Failedcounts by source build/channel./v2/desktop/appcast.xml.Acceptance criteria
Update Available/Update Installedevents include enough target metadata to identify the destination release/channel.Update Check Failedincludes nested error chain and failing URL/HTTP status where available.Suggested labels
bug,desktop,analytics,p2