Skip to content

Mapping — Fitbit

Source: a Fitbit Google Takeout export — a folder of JSON files (Takeout/Fitbit/Global Export Data/ in current exports), not a single file. Pillars: A + B. Mapper: mapFitbitTakeoutmapFitbitTakeout(files, opts) → { records, warnings }, where files is a list of { name, text } pairs (any subset); files are classified by basename, never by directory. Unknown, empty, or corrupt files are ignored gracefully.

Structural correspondence

Takeout fileOpenBody
exercise-<N>.json (activity logs)Session + a continuous WorkUnit (time/distance/energy); logIdclientRecordId; per-workout averageHeartRate and steps → linked aggregate Measurements; heartRateZones/activityLevel/elevationGainextension.fitbit
activityNamedisciplines (Runrunning, Weightsstrength, …); unknown names round-trip as fitbit:<name>
steps-<date>.json (per-minute buckets)one step_count sampleArray per day
heart_rate-<date>.json (intraday samples)one heart_rate sampleArray per day
sleep-<date>.json (levels.data + shortData)sleep_stage category Measurements over adjacent intervals (§4.3), plus per-log sleep_duration and per-stage summary totals
weight-<date>.jsonbody_mass in [lb_av] (Takeout exports pounds — kept exact, no float conversion), plus bmi and body_fat_percentage; source device → provenance.device
resting_heart_rate-<date>.jsonone resting_heart_rate quantity per day

Input (excerpt of examples/fitbit/exercise-0.json)

[
{
"logId": 21092332392,
"activityName": "Run",
"averageHeartRate": 149,
"calories": 306,
"distance": 5.28,
"distanceUnit": "Kilometer",
"duration": 1843000,
"startTime": "01/06/24 07:08:57",
"steps": 4510,
"heartRateZones": [ { "name": "Cardio", "min": 140, "max": 170, "minutes": 24 } ]
}
]

Output (selected records)

An exercise log → a Session; the workout-level HR average and step count become linked Measurements rather than inline fields:

{
"id": "fitbit-ex-21092332392",
"recordType": "Session",
"subject": "subj-001",
"clientRecordId": "21092332392",
"disciplines": ["running"],
"intent": "train",
"startTime": "2024-01-06T07:08:57-08:00",
"endTime": "2024-01-06T07:39:40-08:00",
"provenance": { "method": "sensor", "sourceApp": "fitbit" },
"extension": { "fitbit": { "heartRateZones": [""], "activityLevel": [""], "elevationGain": 67.056 } },
"workUnits": [
{
"id": "fitbit-ex-21092332392-wu",
"recordType": "WorkUnit",
"scoring": "continuous",
"performance": {
"time": { "absolute": { "value": 1843, "unit": "s" } },
"distance": { "absolute": { "value": 5.28, "unit": "km" } },
"energy": { "absolute": { "value": 306, "unit": "kcal" } }
},
"links": [
{ "type": "measuredBy", "ref": "fitbit-ex-21092332392-hr-mean" },
{ "type": "measuredBy", "ref": "fitbit-ex-21092332392-steps" }
]
}
]
}

A weight log → an exact fixed-point body_mass in the source’s own unit:

{
"id": "fitbit-weight-1704526260000",
"recordType": "Measurement",
"subject": "subj-001",
"clientRecordId": "1704526260000",
"type": "body_mass",
"quantity": { "coefficient": 1755, "exponent": -1 },
"unit": "[lb_av]",
"startTime": "2024-01-06T07:31:00-08:00",
"endTime": "2024-01-06T07:31:00-08:00",
"provenance": { "method": "sensor", "sourceApp": "fitbit", "device": { "manufacturer": "fitbit", "model": "Aria" } }
}

A sleep stage → a category Measurement; the 30-second shortData wake episodes are spliced into the surrounding stage segment, so the intervals stay adjacent:

{
"id": "fitbit-sleep-44940631937-s4",
"recordType": "Measurement",
"subject": "subj-001",
"type": "sleep_stage",
"category": "deep",
"startTime": "2024-01-06T00:42:00-08:00",
"endTime": "2024-01-06T01:10:00-08:00",
"provenance": { "method": "sensor", "sourceApp": "fitbit" }
}

Notes

  • 175.5 lb stays 175.5 lb. Takeout exports pounds; the mapper keeps the exact decimal as a fixed-point quantity in [lb_av] rather than converting to kg with float rounding — units convert losslessly downstream, on demand.
  • Known deliberate loss: the per-sample HR confidence (0–3 quality flag) is dropped from the heart_rate sampleArray.
  • Built against the publicly documented Takeout structure (importer sources, Fitbit Web API docs) and cross-checked fixtures — verify against your own real Takeout.