Files
pulse/internal/updates/check_observation.go
T
courtmanr@gmail.com 81e92bff95 Report update channel and last update check outcome in telemetry (#2285)
A failed update check never reaches the update history, because nothing
was applied, so telemetry could not tell an install whose check is broken
from one that was offered an update and ignored it. The 6.4.3-rc.1 installs
looked healthy while their check failed on every call (#2282).

The update manager now records a closed outcome for every check on the
install's effective channel (up_to_date, available, no_release,
rate_limited, network_error, metadata_error, skipped, error), tagging
lookup failures without changing their error text, and ignores previews of
another channel from Settings. Telemetry schema v18 exports update_channel,
update_check_outcome and update_available. No version, URL or error text
leaves the install. The Settings preview type, privacy disclosure, adoption
report and subsystem contracts are updated to match; the Pulse Pro receiver
change lands separately and the parity check passes against it.
2026-09-27 23:50:23 +01:00

97 lines
2.9 KiB
Go

package updates
import (
"errors"
"time"
)
// Update check outcomes are a closed vocabulary shared with usage telemetry.
// A failed check never reaches the update history (nothing was applied), so
// without this record an install whose check is broken is indistinguishable
// from one that was offered an update and ignored it (#2285).
const (
UpdateCheckOutcomeNotChecked = "not_checked"
UpdateCheckOutcomeUpToDate = "up_to_date"
UpdateCheckOutcomeAvailable = "available"
UpdateCheckOutcomeNoRelease = "no_release"
UpdateCheckOutcomeRateLimited = "rate_limited"
UpdateCheckOutcomeNetworkError = "network_error"
UpdateCheckOutcomeMetadataError = "metadata_error"
UpdateCheckOutcomeSkipped = "skipped"
UpdateCheckOutcomeError = "error"
)
// UpdateCheckObservation is the content-free result of the most recent update
// check on the install's effective channel. It carries no version strings,
// URLs, or error text.
type UpdateCheckObservation struct {
Channel string
Outcome string
Available bool
CheckedAt time.Time
}
// updateCheckError tags a release lookup failure with its outcome category
// while leaving the wrapped error message unchanged.
type updateCheckError struct {
outcome string
err error
}
func (e *updateCheckError) Error() string { return e.err.Error() }
func (e *updateCheckError) Unwrap() error { return e.err }
func withUpdateCheckOutcome(outcome string, err error) error {
if err == nil {
return nil
}
return &updateCheckError{outcome: outcome, err: err}
}
func updateCheckErrorOutcome(err error) string {
if errors.Is(err, errGitHubRateLimited) {
return UpdateCheckOutcomeRateLimited
}
var tagged *updateCheckError
if errors.As(err, &tagged) {
return tagged.outcome
}
return UpdateCheckOutcomeError
}
// recordUpdateCheck stores the outcome of a check on the effective channel.
// Checks that preview another channel (an explicit UI override) are ignored so
// the observation always describes what the install itself would be offered.
func (m *Manager) recordUpdateCheck(channel string, effective bool, outcome string, available bool) {
if !effective {
return
}
m.statusMu.Lock()
m.lastCheck = UpdateCheckObservation{
Channel: channel,
Outcome: outcome,
Available: available,
CheckedAt: time.Now().UTC(),
}
m.statusMu.Unlock()
}
// LastUpdateCheck returns the most recent effective-channel check result, with
// the currently effective channel and a not_checked outcome before any check.
func (m *Manager) LastUpdateCheck() UpdateCheckObservation {
if m == nil {
return UpdateCheckObservation{Outcome: UpdateCheckOutcomeNotChecked}
}
currentInfo, _ := GetCurrentVersion()
channel := m.resolveChannel("", currentInfo)
m.statusMu.RLock()
observation := m.lastCheck
m.statusMu.RUnlock()
if observation.Outcome == "" || observation.Channel != channel {
return UpdateCheckObservation{Channel: channel, Outcome: UpdateCheckOutcomeNotChecked}
}
return observation
}