Skip to content

macOS updater: 0.12 clients report appcast retrieval failures after successful update #8941

Description

@Git-on-my-level

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:

  1. root-causing/reducing the Sparkle appcast retrieval failures; and
  2. 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:

  1. Client on 0.11.x emits Update Available.
  2. Same client emits Update Installed while still reporting the old $app_version / $app_build.
  3. Same user relaunches and emits App Launched as 0.12.0 / 12000.
  4. 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

  1. 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.
  2. 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.
  3. 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.
  4. 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

Metadata

Metadata

Assignees

No one assigned

    Labels

    analyticsbugSomething isn't workingdesktopp2Priority: Important (score 14-21)

    Type

    No type

    Fields

    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions