Skip to content

feat(backup): record the app build in exports + on version change (#1410 tiers 1-2) - #1422

Merged
ryanbr merged 3 commits into
mainfrom
backup-build-provenance-1410
Aug 17, 2026
Merged

feat(backup): record the app build in exports + on version change (#1410 tiers 1-2)#1422
ryanbr merged 3 commits into
mainfrom
backup-build-provenance-1410

Conversation

@ryanbr

@ryanbr ryanbr commented Aug 17, 2026

Copy link
Copy Markdown
Owner

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.json in every .noopbak. Keys appVersion, 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_CHANGED event. kind + {from,to,schemaVersion} appended on the first launch after an update, onto the existing event table 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 BackupProvenance builders (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.schemaVersion corrected from a drifted 18 to the real migration count (37); Android surfaces WhoopDatabase.SCHEMA_VERSION (= the Room version).

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 + BackupProvenanceTest green locally. Swift package (BackupProvenance + recordEvent + tests) covered by swift-packages; app-target Swift (DataBackup.swift, AppModel.swift) by the dispatched app-build.

ryanbr added 3 commits August 18, 2026 11:37
 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
ryanbr merged commit d9aae8c into main Aug 17, 2026
14 checks passed
@ryanbr
ryanbr deleted the backup-build-provenance-1410 branch August 17, 2026 23:58
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).
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant