Skip to content

Uploaded gpx data is ignored for heart rate #137

Closed

Description

@bartmuskala

NOOP version

8.5.0

Platform

Android

OS version

16

WHOOP strap model

WHOOP 5.0

Where the data is coming from

FIT / GPX / TCX import

Test Centre profile

Not a test-mode bug

What happens

Yesterday I had not connected yet and had no HR data during the day. I did however two rides. with data.

I exported the go data from strava and imported it. It currently is parsed and states to have imported 1885 heart rate signals.

It however does not feed the HR data and does not calculate an effort score.

What you expected instead

Partial HR data during the day and an effort score.

Steps to reproduce

1.export from strava
2.import to noop
3.notification mentions data is imported
4. Workout is added with data about HR
5. No effort score
6. No HR data

Strap log


Before you submit

  • I attached my exported NOOP .zip (not a screenshot of the Live screen). If my bug is a clearly reproducible UI issue I described exactly how to see it instead.
  • I attached or pasted a strap log above — or my bug is a clearly reproducible UI issue that doesn't need one (e.g. a typo, a button that does nothing) and I've described exactly how to see it.
  • I checked the Troubleshooting guide — especially "history syncs but no data" (most often the strap isn't banking history to flash; charge it to 100% and reconnect).
  • I checked the FAQ and — if I'm on a WHOOP 5.0 / MG — the 5.0/MG status page, and this isn't an already-known limit (e.g. SpO₂, or recovery/sleep on the 5/MG).
  • I've left out anything personal (strap serial, account email, raw export).

Activity

  1. digitalerdude commented on Jul 10, 2026

    @digitalerdude

    Dug through the import → scoring path to see exactly why an imported ride with HR produces neither day HR nor an Effort score. Sharing the full picture, because a fix here reverses a deliberate design decision and touches the scoring pipeline — probably a maintainer call on direction.

    Why it happens

    1. The importer keeps only aggregates. ActivityFileImporter parses the per-sample HR but reduces it to avgHr / maxHr / hrSampleCount on the WorkoutRow; the per-sample HR time series is discarded (the ImportedWorkout shape has no sample field). The row is stored with strain = null — and this is intentional:

      • WorkoutRow(… strain = null, // never a fabricated cardiovascular strain) (Android ActivityFileImporter)
      • // … upsert one workout row under the "activity-file" source, refresh. No HR Effort is touched. (macOS DataSourcesView)
    2. Day "Effort" is computed from the day HR stream, not from workout strains. In AnalyticsEngine.analyzeDay, Strain/Effort is "cardiovascular load over the full CALENDAR day", derived from dayHr, which the engine reads as hrSamples(deviceId: owner, …) — the resolved day owner's HR. Imported activity-file HR is not in any HR-sample stream at all, and even if it were (under the activity-file source), the day-Effort read wouldn't see it unless that source reaches dayHr.

    3. The existing under-scored-workout rescore doesn't apply. rescoreManualWorkouts (the #137-tagged path) is gated to source == "manual" and reads HR under the engine's own deviceId, so activity-file rows are skipped and their HR (under a different source) wouldn't be read anyway.

    So the imported ride is stored honestly (avg/max HR, distance, route) but is deliberately kept out of HR/Effort.

    What a fix would require

    To give the ride a day Effort + partial day HR, three things, on both platforms (byte-parity):

    • (A) Persist the real per-sample HR under the activity-file source (thread the samples through the importer → StreamStore.insert / repo.insertHr).
    • (B) Reach the day-Effort read — either let day-owner resolution treat a strap-less day as owned by activity-file, or union activity-file HR into the dayHr read the way workouts already union apple-health (realWorkouts = workouts(deviceId) + workouts("apple-health")). The union is the less-invasive option (doesn't touch the "one source per day" invariant).
    • (C, optional) Also score the workout row's own strain from the persisted samples (generalize rescoreManualWorkouts to activity-file, reading HR under that source).

    The judgement call

    This reverses the explicit "No HR Effort is touched" decision. The nuance in its favour: the GPX/FIT HR is real measured data, not a fabricated estimate — so surfacing it as day HR + Effort isn't the "fabricated strain" the current guard avoids. But it's a scoring-pipeline change and would need on-device validation (import a GPX with HR on a strap-less day, confirm the Effort ring lights and the day HR chart fills).

    Happy to put up a PR for whichever direction you prefer — I'd lean toward (A) + the (B) dayHr-union, as the smallest change that lights Effort without reworking owner resolution. Would just want your OK on reversing the "No HR Effort is touched" stance first.

  2. bartmuskala commented on Jul 10, 2026

    @bartmuskala
    Author

    Thanks @digitalerdude for the thorough feedback. Really appreciated.

    I understand the current behaviour and when wearing the strap at all times, this is not even necessary. Given I did not have an all day reading, I hoped to fix it this way. I therefore believe that the A+B (union) makes for a good suggestion.

    In terms of priorities I can live without :).

  3. added a commit that references this issue on Jul 11, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't working

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions