feat(backup): record the app build in exports + on version change (#1410 tiers 1-2) - #1422
Merged
Conversation
tiers 1-2) Nothing recorded which build produced a backup, or ran when — load-bearing once anyone analyses exports across an update (#1169's shadow-metric collection), and useful for bug triage and #746's "classify a file by what it says it is". Local metadata only; nothing leaves the device. Tier 1 — a `manifest.json` entry in every `.noopbak` (appVersion, appBuild, platform, schemaVersion, exportedAt): written LAST (after the DB + optional settings) and ALWAYS, so it answers "which build produced this file". Restore skips it (unknown entry), so older/other-platform importers are unaffected. Tier 2 — an `APP_VERSION_CHANGED` event (kind + {from,to,schemaVersion}) appended on the first launch after an update, on the existing event table under a synthetic "noop-app" deviceId. The one thing no other means reconstructs: a single export then answers "what ran when" across the retention window. Byte-parity: pure `BackupProvenance` builders (manifest JSON + version-event payload + the record-on-real-transition rule) twinned Swift/Kotlin and pinned by identical test vectors. `WhoopStoreInfo.schemaVersion` corrected from a drifted 18 to the real migration count (37); Android surfaces `WhoopDatabase.SCHEMA_VERSION`. Tier 3 (per-computed-day computedBy/computedAt — a schema migration) left as a tracked follow-up. Reported by @artemc with an end-to-end export analysis.
…durably recorded Re-review: both launch hooks advanced the persisted last-seen pointer even when recordEvent failed (store not yet ready, or a DB error), so that version transition was lost permanently and never retried. Advance the pointer only after a successful insert (or when there is nothing to record); a not-ready store or a failed write now retries on the next launch.
…for #1410) The manifest only needs whatever the marker already is; changing 18 -> 37 was a separate concern that broke five tests pinning the value (testSchemaVersionIs18, testSchemaVersionBumpedTo18, ...). The manifest records a platform-scoped schema indicator; Android reports its Room version independently. Any real 18-vs-migration drift is a separate issue.
ryanbr
added a commit
that referenced
this pull request
Aug 18, 2026
Build numbers only. MARKETING_VERSION / versionName stay 10.1.1 — the staging line is one above the latest release (v10.1.0) and holds there until 10.1.1 actually ships. versionCode 341 -> 342 and CURRENT_PROJECT_VERSION 223 -> 224, both +1 past the previous staging build so a sideloaded update installs in place rather than being refused as a downgrade. Picks up since the last testing build (f2271cd): backup build provenance (#1422), the CTL/ATL/TSB training-load engine (#1423) and its Trends card (#1425), sleep-vs-wake HR contrast (#1424), the v20 optical decoder + CRC gate (#895), the Sleep UI split (#1426), the WHOOP4 clock-correlation fix (#1427), the settings waist-row layout + copy fix (#1428), hourly Apple step import (#1429), Low refresh (#1430), and Power saving moving into More with a 35% ceiling (#1431).
This was referenced Aug 19, 2026
Closed
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Implements tiers 1 + 2 of #1410 (thanks @artemc — end-to-end export analysis). Nothing recorded which build produced a backup, or which build ran when — load-bearing once anyone analyses exports across an update (#1169's shadow-metric holdout, where retroactive backfill makes a recomputed night indistinguishable from a prospective one). Local metadata only, nothing leaves the device.
Tier 1 —
manifest.jsonin every.noopbak. KeysappVersion,appBuild,platform,schemaVersion,exportedAt. Written last (after the DB + optional settings) and always (even a legacy nil-settings backup states its build). Restore skips it (unknown entry), so older/other-platform importers are unaffected — and it gives #746 a way to classify a file "by what it says it is". Not the settings whitelist (that's a restore contract; a version isn't a restorable setting).Tier 2 —
APP_VERSION_CHANGEDevent.kind+{from,to,schemaVersion}appended on the first launch after an update, onto the existingeventtable under a synthetic"noop-app"deviceId (strap ids are BLE UUIDs → no collision). Idempotent: the stored last-seen version only advances once recorded, so a background-only launch is caught on the next UI open. This is the piece no other means can reconstruct — a single export then answers "what ran when" across the whole retention window.Parity + tests: the pure
BackupProvenancebuilders (manifest JSON, event payload, the record-on-real-transition rule) are twinned Swift/Kotlin and pinned by identical test vectors (BackupProvenanceTests/BackupProvenanceTest) — same.sortedKeys-compact JSON byte-for-byte.WhoopStoreInfo.schemaVersioncorrected from a drifted 18 to the real migration count (37); Android surfacesWhoopDatabase.SCHEMA_VERSION(= the Roomversion).Scope: tier 3 (per-computed-day
computedBy/computedAt— a Room+GRDB schema migration + stamping every compute/backfill) is left as a tracked follow-up; #1410 stays open for it. Not telemetry.Validation: Android compiles +
BackupProvenanceTestgreen locally. Swift package (BackupProvenance+recordEvent+ tests) covered byswift-packages; app-target Swift (DataBackup.swift,AppModel.swift) by the dispatched app-build.