Skip to content

Raw sensor export reads the active strap, not the legacy id (#175) - #924

Merged
ryanbr merged 2 commits into
mainfrom
fix/raw-export-active-strap
Jul 28, 2026
Merged

ryanbr merged 2 commits into
mainfrom
fix/raw-export-active-strap

Conversation

@ryanbr

@ryanbr ryanbr commented Jul 28, 2026 •

Copy link
Copy Markdown
Owner

The raw sensor CSV — the file people attach to bug reports — could describe a strap the user
isn't wearing. Fixed on both platforms.

Android. RawSensorExport.export took deviceId: String = "my-whoop" and its one caller
(SettingsScreen.kt:2082) took the default. After a remove+re-add the strap gets a fresh
whoop-<uuid> that the collector writes today's raw under, so the export read the legacy id's
streams instead. The sibling export four hundred lines up already threads vm.activeStrapId.

The default is removed rather than corrected, so the compiler asks at every future call site
instead of the next author having to remember. Verified there was exactly one caller first.

iOS/macOS. Same defect, worse form: exportRawSensorCSV passed model.deviceId, which is a
hardcoded let "my-whoop", where ~20 other strap reads use repo.deviceId — seeded with that id
and then re-pointed to the registry's active strap by adoptActiveDeviceId. Read on the MainActor
before the Task hop, matching LiveSessionRunner.

This is #814 on the diagnostic path, and the #172/#175 shape that ResolvedSeriesCallSiteAuditTest
exists to catch.

Verification. Android compiles and its unit tests pass in CI. The Swift side is app-target
(Strand/), which nothing compiles while app-build.yml is disabled — so it is stated plainly: it
is uncompiled. What supports it is that LiveSessionRunner.persist() is the same four lines already
shipping — capture repo.deviceId, Task {, await repo.storeHandle(), pass the captured id — and
the enclosing function mutates @State synchronously, so it is MainActor-isolated. Same isolation
shape as working code, but a local xcodebuild before merge is still the thing that proves it.

Not changed, deliberately. Two other model.deviceId readers exist — WatchSessionBridge.swift:150
and StrandiOSApp.swift:58 — and WhoopRepository still defaults strapDeviceId on four Android
reads. Each needs its own caller audit; bundling them here would hide this one.

Android half ported from tanarchytan/noop e4f508f, which fixed the caller; the removed default
and the Apple twin are additional.

RawSensorExport.export defaulted deviceId to "my-whoop" and its single caller in
SettingsScreen took the default. After a strap re-add the active id is no longer
the canonical one, so the export wrote the OLD strap's streams — silently, into
the CSV people attach to bug reports. The diagnostic most likely to be read by
someone debugging a re-added strap was the one describing a different strap.

This is the #172/#175 shape verbatim: a defaulted strap id that no caller
overrides, which compiles, passes every test, and only misbehaves after a
re-add. ResolvedSeriesCallSiteAuditTest exists because eight surfaces had it.
The sibling export twelve hundred lines up (WhoopCsvExporter.exportZip at
SettingsScreen:603) already threads vm.activeStrapId, so the codebase knew the
rule and this call site missed it.

Fixed by removing the default rather than only passing the argument: with
`deviceId: String` required, a future caller cannot silently inherit the wrong
strap, and the compiler enforces what an audit test would otherwise have to
notice. Checked first that export() has exactly one caller, so requiring it
breaks nothing.

Ported from tanarchytan/noop e4f508f, which fixed the same call site by passing
the argument; credited in the code comment.

Not swept further here: WhoopRepository still defaults strapDeviceId on several
reads (dismissedDetected, dismissedSleeps, relabelDetected, freshness). Those are
a wider audit than this one-line fix and deserve their own pass, since each needs
its callers checked individually.
@ryanbr
ryanbr force-pushed the fix/raw-export-active-strap branch from 344b9c6 to cd4cc53 Compare July 28, 2026 05:42
The Apple twin of the same defect. exportRawSensorCSV passed model.deviceId,
a hardcoded let "my-whoop", where every other strap read in the app uses
repo.deviceId — seeded with that id and then re-pointed to the registry's
active strap once the store opens. After a remove+re-add the two diverge and
the CSV described the wrong strap.

Read on the MainActor before the Task hop, matching LiveSessionRunner.
@ryanbr
ryanbr merged commit ff5a28d into main Jul 28, 2026
3 checks passed
@ryanbr
ryanbr deleted the fix/raw-export-active-strap branch July 28, 2026 06:01
simoncad7 pushed a commit to simoncad7/noop that referenced this pull request Aug 17, 2026
…) (ryanbr#924)

* Raw sensor export reads the active strap, not the legacy id (ryanbr#175)

RawSensorExport.export defaulted deviceId to "my-whoop" and its single caller in
SettingsScreen took the default. After a strap re-add the active id is no longer
the canonical one, so the export wrote the OLD strap's streams — silently, into
the CSV people attach to bug reports. The diagnostic most likely to be read by
someone debugging a re-added strap was the one describing a different strap.

This is the ryanbr#172/ryanbr#175 shape verbatim: a defaulted strap id that no caller
overrides, which compiles, passes every test, and only misbehaves after a
re-add. ResolvedSeriesCallSiteAuditTest exists because eight surfaces had it.
The sibling export twelve hundred lines up (WhoopCsvExporter.exportZip at
SettingsScreen:603) already threads vm.activeStrapId, so the codebase knew the
rule and this call site missed it.

Fixed by removing the default rather than only passing the argument: with
`deviceId: String` required, a future caller cannot silently inherit the wrong
strap, and the compiler enforces what an audit test would otherwise have to
notice. Checked first that export() has exactly one caller, so requiring it
breaks nothing.

Ported from tanarchytan/noop e4f508f, which fixed the same call site by passing
the argument; credited in the code comment.

Not swept further here: WhoopRepository still defaults strapDeviceId on several
reads (dismissedDetected, dismissedSleeps, relabelDetected, freshness). Those are
a wider audit than this one-line fix and deserve their own pass, since each needs
its callers checked individually.

* Raw sensor CSV export: use the active strap on iOS too

The Apple twin of the same defect. exportRawSensorCSV passed model.deviceId,
a hardcoded let "my-whoop", where every other strap read in the app uses
repo.deviceId — seeded with that id and then re-pointed to the registry's
active strap once the store opens. After a remove+re-add the two diverge and
the CSV described the wrong strap.

Read on the MainActor before the Task hop, matching LiveSessionRunner.
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