Merge branch 'release/v1.46.0' into feat/v146-server

# Conflicts:
#	src/__tests__/baselines/source-function-lines.json
#	src/__tests__/baselines/version-stamp-comments.json
This commit is contained in:
Marc-André Bombeck committed 2026-10-23 14:35:19 +02:00
commit c40d728b47
442 files changed
+33533 -13503

No files matched your search

+3 -3
View File
@@ -53,8 +53,8 @@
"mcp:stdio": "npx tsx src/cli/mcp-stdio.ts"
},
"dependencies": {
"@aws-sdk/client-s3": "^3.1152.0",
"@aws-sdk/lib-storage": "3.1152.0",
"@aws-sdk/client-s3": "^3.1153.0",
"@aws-sdk/lib-storage": "3.1153.0",
"@dnd-kit/core": "^6.3.1",
"@dnd-kit/sortable": "^10.0.0",
"@dnd-kit/utilities": "^3.2.2",
@@ -66,7 +66,7 @@
"@prisma/client": "^7.10.1",
"@simplewebauthn/browser": "^14.0.0",
"@simplewebauthn/server": "^14.0.3",
"@tanstack/react-query": "5.104.4",
"@tanstack/react-query": "5.104.5",
"@tanstack/react-virtual": "^3.14.21",
"@types/sax": "^1.2.7",
"class-variance-authority": "^0.7.1",
+43 -52
View File
@@ -46,11 +46,11 @@ importers:
.:
dependencies:
'@aws-sdk/client-s3':
specifier: ^3.1152.0
version: 3.1152.0
specifier: ^3.1153.0
version: 3.1153.0
'@aws-sdk/lib-storage':
specifier: 3.1152.0
version: 3.1152.0(@aws-sdk/client-s3@3.1152.0)
specifier: 3.1153.0
version: 3.1153.0(@aws-sdk/client-s3@3.1153.0)
'@dnd-kit/core':
specifier: ^6.3.1
version: 6.3.1(react-dom@19.3.1(react@19.3.1))(react@19.3.1)
@@ -85,8 +85,8 @@ importers:
specifier: ^14.0.3
version: 14.0.3
'@tanstack/react-query':
specifier: 5.104.4
version: 5.104.4(@types/react@19.3.0)(react@19.3.1)
specifier: 5.104.5
version: 5.104.5(@types/react@19.3.0)(react@19.3.1)
'@tanstack/react-virtual':
specifier: ^3.14.21
version: 3.14.21(react-dom@19.3.1(react@19.3.1))(react@19.3.1)
@@ -303,8 +303,8 @@ packages:
resolution: {integrity: sha512-H8BiG/2DnfwydVxD1uDPLa1s1sfhRnVTbD2OwzfTr3WlJzgbIzICDXxBTlTMjbWNNCe4VS9X55fco+JI34L9kw==}
engines: {node: '>=20.0.0'}
'@aws-sdk/client-s3@3.1152.0':
resolution: {integrity: sha512-oI1+ZaM6pSOwZazu4nAb7rL8ug7LSjg31tDoJqRB9MooRqqWCf/qvkIZqne1mRA3xK3slg6K0RNWXaJYHbWLvg==}
'@aws-sdk/client-s3@3.1153.0':
resolution: {integrity: sha512-h0VScQZPfBApg1Qi4AedsOUSMZ7m758kZIg0JO0B5HtwSzQhB7Tgh6h1JoGEXELgrWADPusYMpMBDmrRAEx2yw==}
engines: {node: '>=20.0.0'}
'@aws-sdk/core@3.978.4':
@@ -343,11 +343,11 @@ packages:
resolution: {integrity: sha512-nHkgkMIC6NHgqfI6UAQq5cCS4bUkNxkT+r9fXCq/X4IKfX/DHGZ8UM3T4b78OXpWK5OlzEzN4kxwyax0W/MUCA==}
engines: {node: '>=20.0.0'}
'@aws-sdk/lib-storage@3.1152.0':
resolution: {integrity: sha512-UU1fDLKBd8az+4AmNx30ggnA4D4HolUd1MxT/J6ka76qKcckI0IxjAy6FpX/vwiUf2KDNxxN2YX0cfwjMqzWoQ==}
'@aws-sdk/lib-storage@3.1153.0':
resolution: {integrity: sha512-zshL4tYVwTXfEOQ0iqNsXUPq/0aoZwOHrAsqlBmGcAij1+8qc79rm9LpzBuEV7KWiSMetZ+89n7mSYgcJKFgyA==}
engines: {node: '>=20.0.0'}
peerDependencies:
'@aws-sdk/client-s3': ^3.1152.0
'@aws-sdk/client-s3': ^3.1153.0
'@aws-sdk/middleware-sdk-s3@3.972.80':
resolution: {integrity: sha512-zX3lXN/9XOUAuGpxgt3wXwZrXhTjQYEgRsqa856PbZWpOtleUWmCMbhlkLfusPuhW24cOC/8JYR8hnRalrrIkQ==}
@@ -2856,12 +2856,8 @@ packages:
resolution: {integrity: sha512-tlqY9xq5ukxTUZBmoOp+m61cqwQD5pHJtFY3Mn8CA8ps6yghLH/Hw8UPdqg4OLmFW3IFlcXnQNmo/dh8HzXYIQ==}
engines: {node: '>=18'}
'@smithy/core@3.35.2':
resolution: {integrity: sha512-ejTukBuP/Pg3t9N8Ux+0zKsXeHWm7p6W3HbXFOyltKasSxZwCFmvTut7kBdZSCF3m9y1bXXyVu6p2xuMFcsTyQ==}
engines: {node: '>=18.0.0'}
'@smithy/core@3.36.1':
resolution: {integrity: sha512-WC5W1Jrsfxu1y6LeqXMie7N14qmcdafij/Haye4feVlyMMXLGpYxBeOX6Ee1iRJbDzGG5LVman8U1rrt9MYFHA==}
'@smithy/core@3.36.0':
resolution: {integrity: sha512-3hJiwG1F5t1SoXqp/yOCmYXziyx7ZVqF63S5W6YU201e/qwVaHPex+E/3vvyY9a3ORq/b+BydeNZpP2saQ9X2g==}
engines: {node: '>=18.0.0'}
'@smithy/credential-provider-imds@4.5.2':
@@ -3008,11 +3004,11 @@ packages:
'@tailwindcss/postcss@4.3.3':
resolution: {integrity: sha512-JTSZZGQi1AyKirbLN3azmjVzef92tcX7h+iSqPdaeStyFpGpDlKvvpxeOE8njhbUanbRwr3z8DyzhICWnMtQeg==}
'@tanstack/query-core@5.104.4':
resolution: {integrity: sha512-nU4Yrb8Sjn0DeVb7PKG9uiepNavys3yoxy1FX0hViY4/P6Ng/mBje2SMl7i9812rtyHKtHyqiaC/td6GkI8StA==}
'@tanstack/query-core@5.104.5':
resolution: {integrity: sha512-Q94gI8Opd+CbdVNOdj8ZCyV8WqLYitrDq1t96Xc8WZT6lY0r5iRnJLXYDxMex7EEf2v+mXvn4FcjaBKSxXZQ9A==}
'@tanstack/react-query@5.104.4':
resolution: {integrity: sha512-YD5QkvUNy8Ey3LbOy2vwL97JjoYycwDv++cWAibTe+9BLYvl3c09XpYp5dZ2VcxcguwoDhGF+utYJYb79RB36w==}
'@tanstack/react-query@5.104.5':
resolution: {integrity: sha512-RORgFe052U+93weaNuqg7qHq7vNVNzXN/VrXqD4bbAuQCWUqOl2lV5Ui9flWk5CVvMyL/Bh27QWhzwH0aaOx9g==}
peerDependencies:
'@types/react': ^18 || ^19
react: ^18 || ^19
@@ -7293,11 +7289,11 @@ snapshots:
dependencies:
'@aws-sdk/core': 3.978.4
'@aws-sdk/types': 3.974.6
'@smithy/core': 3.36.1
'@smithy/core': 3.36.0
'@smithy/types': 4.19.0
tslib: 2.8.1
'@aws-sdk/client-s3@3.1152.0':
'@aws-sdk/client-s3@3.1153.0':
dependencies:
'@aws-sdk/checksums': 3.1001.4
'@aws-sdk/core': 3.978.4
@@ -7305,7 +7301,7 @@ snapshots:
'@aws-sdk/middleware-sdk-s3': 3.972.80
'@aws-sdk/signature-v4-multi-region': 3.996.48
'@aws-sdk/types': 3.974.6
'@smithy/core': 3.36.1
'@smithy/core': 3.36.0
'@smithy/fetch-http-handler': 5.8.0
'@smithy/node-http-handler': 4.12.1
'@smithy/types': 4.19.0
@@ -7316,7 +7312,7 @@ snapshots:
'@aws-sdk/types': 3.974.6
'@aws-sdk/xml-builder': 3.972.41
'@aws/lambda-invoke-store': 0.3.0
'@smithy/core': 3.36.1
'@smithy/core': 3.36.0
'@smithy/signature-v4': 5.7.4
'@smithy/types': 4.19.0
bowser: 2.14.1
@@ -7326,7 +7322,7 @@ snapshots:
dependencies:
'@aws-sdk/core': 3.978.4
'@aws-sdk/types': 3.974.6
'@smithy/core': 3.36.1
'@smithy/core': 3.36.0
'@smithy/types': 4.19.0
tslib: 2.8.1
@@ -7334,7 +7330,7 @@ snapshots:
dependencies:
'@aws-sdk/core': 3.978.4
'@aws-sdk/types': 3.974.6
'@smithy/core': 3.36.1
'@smithy/core': 3.36.0
'@smithy/fetch-http-handler': 5.8.0
'@smithy/node-http-handler': 4.12.1
'@smithy/types': 4.19.0
@@ -7351,7 +7347,7 @@ snapshots:
'@aws-sdk/credential-provider-web-identity': 3.972.81
'@aws-sdk/nested-clients': 3.997.49
'@aws-sdk/types': 3.974.6
'@smithy/core': 3.36.1
'@smithy/core': 3.36.0
'@smithy/credential-provider-imds': 4.5.2
'@smithy/types': 4.19.0
tslib: 2.8.1
@@ -7361,7 +7357,7 @@ snapshots:
'@aws-sdk/core': 3.978.4
'@aws-sdk/nested-clients': 3.997.49
'@aws-sdk/types': 3.974.6
'@smithy/core': 3.36.1
'@smithy/core': 3.36.0
'@smithy/types': 4.19.0
tslib: 2.8.1
@@ -7374,7 +7370,7 @@ snapshots:
'@aws-sdk/credential-provider-sso': 3.973.19
'@aws-sdk/credential-provider-web-identity': 3.972.81
'@aws-sdk/types': 3.974.6
'@smithy/core': 3.36.1
'@smithy/core': 3.36.0
'@smithy/credential-provider-imds': 4.5.2
'@smithy/types': 4.19.0
tslib: 2.8.1
@@ -7383,7 +7379,7 @@ snapshots:
dependencies:
'@aws-sdk/core': 3.978.4
'@aws-sdk/types': 3.974.6
'@smithy/core': 3.36.1
'@smithy/core': 3.36.0
'@smithy/types': 4.19.0
tslib: 2.8.1
@@ -7393,7 +7389,7 @@ snapshots:
'@aws-sdk/nested-clients': 3.997.49
'@aws-sdk/token-providers': 3.1153.0
'@aws-sdk/types': 3.974.6
'@smithy/core': 3.36.1
'@smithy/core': 3.36.0
'@smithy/types': 4.19.0
tslib: 2.8.1
@@ -7402,14 +7398,14 @@ snapshots:
'@aws-sdk/core': 3.978.4
'@aws-sdk/nested-clients': 3.997.49
'@aws-sdk/types': 3.974.6
'@smithy/core': 3.36.1
'@smithy/core': 3.36.0
'@smithy/types': 4.19.0
tslib: 2.8.1
'@aws-sdk/lib-storage@3.1152.0(@aws-sdk/client-s3@3.1152.0)':
'@aws-sdk/lib-storage@3.1153.0(@aws-sdk/client-s3@3.1153.0)':
dependencies:
'@aws-sdk/client-s3': 3.1152.0
'@smithy/core': 3.35.2
'@aws-sdk/client-s3': 3.1153.0
'@smithy/core': 3.36.0
'@smithy/types': 4.19.0
buffer: 5.6.0
events: 3.3.0
@@ -7421,7 +7417,7 @@ snapshots:
'@aws-sdk/core': 3.978.4
'@aws-sdk/signature-v4-multi-region': 3.996.48
'@aws-sdk/types': 3.974.6
'@smithy/core': 3.36.1
'@smithy/core': 3.36.0
'@smithy/types': 4.19.0
tslib: 2.8.1
@@ -7430,7 +7426,7 @@ snapshots:
'@aws-sdk/core': 3.978.4
'@aws-sdk/signature-v4-multi-region': 3.996.48
'@aws-sdk/types': 3.974.6
'@smithy/core': 3.36.1
'@smithy/core': 3.36.0
'@smithy/fetch-http-handler': 5.8.0
'@smithy/node-http-handler': 4.12.1
'@smithy/types': 4.19.0
@@ -7448,7 +7444,7 @@ snapshots:
'@aws-sdk/core': 3.978.4
'@aws-sdk/nested-clients': 3.997.49
'@aws-sdk/types': 3.974.6
'@smithy/core': 3.36.1
'@smithy/core': 3.36.0
'@smithy/types': 4.19.0
tslib: 2.8.1
@@ -9949,37 +9945,32 @@ snapshots:
'@sindresorhus/merge-streams@4.0.0': {}
'@smithy/core@3.35.2':
dependencies:
'@smithy/types': 4.19.0
tslib: 2.8.1
'@smithy/core@3.36.1':
'@smithy/core@3.36.0':
dependencies:
'@smithy/types': 4.19.0
tslib: 2.8.1
'@smithy/credential-provider-imds@4.5.2':
dependencies:
'@smithy/core': 3.36.1
'@smithy/core': 3.36.0
'@smithy/types': 4.19.0
tslib: 2.8.1
'@smithy/fetch-http-handler@5.8.0':
dependencies:
'@smithy/core': 3.36.1
'@smithy/core': 3.36.0
'@smithy/types': 4.19.0
tslib: 2.8.1
'@smithy/node-http-handler@4.12.1':
dependencies:
'@smithy/core': 3.36.1
'@smithy/core': 3.36.0
'@smithy/types': 4.19.0
tslib: 2.8.1
'@smithy/signature-v4@5.7.4':
dependencies:
'@smithy/core': 3.36.1
'@smithy/core': 3.36.0
'@smithy/types': 4.19.0
tslib: 2.8.1
@@ -10130,11 +10121,11 @@ snapshots:
postcss: 8.5.26
tailwindcss: 4.3.3
'@tanstack/query-core@5.104.4': {}
'@tanstack/query-core@5.104.5': {}
'@tanstack/react-query@5.104.4(@types/react@19.3.0)(react@19.3.1)':
'@tanstack/react-query@5.104.5(@types/react@19.3.0)(react@19.3.1)':
dependencies:
'@tanstack/query-core': 5.104.4
'@tanstack/query-core': 5.104.5
react: 19.3.1
optionalDependencies:
'@types/react': 19.3.0
@@ -25,7 +25,7 @@
* true, only that the set did not change without someone editing this file.
*/
import { describe, it, expect } from "vitest";
import { readFileSync, readdirSync } from "node:fs";
import { existsSync, readFileSync, readdirSync } from "node:fs";
import { join } from "node:path";
import { walkSourceFiles } from "./helpers/source-files";
@@ -84,17 +84,25 @@ function backfillFiles(): string[] {
const jobs = readdirSync(join(SRC, "lib/jobs"))
.filter((f) => /-backfill\.ts$/.test(f))
.map((f) => `src/lib/jobs/${f}`);
// A provider that carries its own jobs folder (`lib/<provider>/jobs/`).
const providerJobs = readdirSync(join(SRC, "lib")).flatMap((dir) => {
const jobsDir = join(SRC, "lib", dir, "jobs");
if (!existsSync(jobsDir)) return [];
return readdirSync(jobsDir)
.filter((f) => /(?:^|-)backfill\.ts$/.test(f))
.map((f) => `src/lib/${dir}/jobs/${f}`);
});
const scripts = readdirSync(join(ROOT, "scripts"))
.filter((f) => /^backfill-.*\.ts$/.test(f))
.map((f) => `scripts/${f}`);
return [...jobs, ...scripts].sort();
return [...jobs, ...providerJobs, ...scripts].sort();
}
/**
* Backfill writes that stamp `updatedAt` on purpose, keyed `file > model`.
*/
const BACKFILL_STAMPS: Record<string, string> = {
"src/lib/jobs/fitbit-backfill.ts > fitbitConnection":
"src/lib/fitbit/jobs/backfill.ts > fitbitConnection":
"the job's own progress on the connection row; not a listed record",
"src/lib/jobs/google-health-backfill.ts > googleHealthConnection":
"the job's own progress on the connection row; not a listed record",
+13 -22
View File
@@ -1,7 +1,7 @@
{
"$comment": "Production files over 800 lines and their length. Maintained by src/__tests__/source-size-ratchet.test.ts.",
"count": 115,
"total": 133899,
"count": 106,
"total": 122271,
"entries": {
"app/api/dashboard/summary/route.ts": 1032,
"app/api/documents/inbound/route.ts": 811,
@@ -64,13 +64,12 @@
"lib/export/backup-plan.ts": 853,
"lib/export/coach-backup.ts": 1070,
"lib/export/profile-backup.ts": 1019,
"lib/fitbit/client.ts": 1529,
"lib/fitbit/sync-core.ts": 968,
"lib/fitbit/client.ts": 1262,
"lib/fitbit/sync-core.ts": 951,
"lib/gamification/achievements-result.ts": 1094,
"lib/gamification/achievements.ts": 913,
"lib/google-health/client.ts": 1875,
"lib/google-health/mappers.ts": 1295,
"lib/google-health/sync-core.ts": 1634,
"lib/google-health/sync-core.ts": 1621,
"lib/idempotency.ts": 879,
"lib/illness/correlation.ts": 1059,
"lib/import/health-connect/import.ts": 1689,
@@ -84,18 +83,13 @@
"lib/insights/mood-aggregates.ts": 1053,
"lib/insights/status-cache.ts": 812,
"lib/integrations/status.ts": 1445,
"lib/jobs/apple-health-import-worker.ts": 801,
"lib/jobs/backup-restore.ts": 888,
"lib/jobs/coach-nudge.ts": 1089,
"lib/jobs/insight-pregenerate.ts": 1266,
"lib/jobs/medication-intake-import.ts": 822,
"lib/jobs/offhost-backup.ts": 1168,
"lib/jobs/backup-restore.ts": 887,
"lib/jobs/coach-nudge.ts": 1084,
"lib/jobs/insight-pregenerate.ts": 1259,
"lib/jobs/medication-intake-import.ts": 819,
"lib/jobs/offhost-backup.ts": 1147,
"lib/jobs/reaction-line.ts": 824,
"lib/jobs/reminder/medication-reminder-check.ts": 818,
"lib/jobs/reminder/register-integration-sync.ts": 1051,
"lib/jobs/reminder/register-maintenance.ts": 2197,
"lib/jobs/reminder/register-rollup.ts": 1156,
"lib/jobs/reminder/register-status.ts": 878,
"lib/managed-profiles/handover.ts": 986,
"lib/mcp/record-search.ts": 1467,
"lib/mcp/rich-reads.ts": 1467,
@@ -105,18 +99,15 @@
"lib/medications/scheduling/recurrence.ts": 1108,
"lib/medications/scheduling/slot-upsert.ts": 926,
"lib/notifications/senders/apns.ts": 1100,
"lib/oura/client.ts": 1360,
"lib/oura/sync.ts": 902,
"lib/polar/client.ts": 1010,
"lib/oura/sync.ts": 900,
"lib/rollups/measurement-rollups.ts": 1496,
"lib/search/search-records.ts": 996,
"lib/sharing/grants.ts": 807,
"lib/support/report.ts": 905,
"lib/support/report.ts": 903,
"lib/telegram-webhook-handlers.ts": 1341,
"lib/validations/inbound-documents.ts": 1130,
"lib/whoop/client.ts": 917,
"lib/withings/sync-sleep.ts": 884,
"lib/withings/sync.ts": 978,
"lib/withings/sync.ts": 933,
"proxy.ts": 870
}
}
@@ -1,7 +1,7 @@
{
"$comment": "Production functions over 200 lines, keyed \"file :: name\", and their length. Maintained by src/__tests__/source-size-ratchet.test.ts.",
"count": 388,
"total": 137367,
"count": 380,
"total": 133935,
"entries": {
"app/achievements/page.tsx :: AchievementsPage": 206,
"app/api/admin/backups/upload/route.ts :: apiHandler(arg)": 314,
@@ -313,12 +313,11 @@
"lib/export/vaccinations-backup.ts :: restoreVaccinationsData": 216,
"lib/export/visits-backup.ts :: restoreVisitsData": 261,
"lib/fhir/build-bundle.ts :: buildFhirDocumentBundle": 363,
"lib/fitbit/sync-core.ts :: upsertFitbitMeasurements": 370,
"lib/fitbit/sync-core.ts :: upsertFitbitMeasurements": 356,
"lib/gamification/achievements-result.ts :: buildAchievementsResult": 586,
"lib/google-health/client.ts :: forEachDataPointPage": 288,
"lib/google-health/client.ts :: forEachDataPointPage > walk": 223,
"lib/google-health/sync-core.ts :: upsertGoogleHealthMeasurements": 463,
"lib/google-health/sync.ts :: runGoogleHealthSyncCycle": 203,
"lib/google-health/data-points.ts :: forEachDataPointPage": 288,
"lib/google-health/data-points.ts :: forEachDataPointPage > walk": 223,
"lib/google-health/sync-core.ts :: upsertGoogleHealthMeasurements": 453,
"lib/illness/correlation.ts :: computeIllnessCorrelation": 233,
"lib/import/labs-csv.ts :: importLabsCsv": 309,
"lib/import/labs-csv.ts :: importLabsCsv > flush": 231,
@@ -348,13 +347,6 @@
"lib/jobs/reaction-line.ts :: runReactionLine": 337,
"lib/jobs/reminder/medication-reminder-check.ts :: handleReminderCheck": 619,
"lib/jobs/reminder/medication-reminder-check.ts :: handleReminderCheck > withBackgroundEvent(arg)": 614,
"lib/jobs/reminder/register-integration-sync.ts :: enqueueIntegrationSyncBootDiscovery": 224,
"lib/jobs/reminder/register-integration-sync.ts :: registerIntegrationSyncQueues": 320,
"lib/jobs/reminder/register-maintenance.ts :: enqueueMaintenanceBootDiscovery": 232,
"lib/jobs/reminder/register-maintenance.ts :: registerMaintenanceQueues": 997,
"lib/jobs/reminder/register-rollup.ts :: enqueueRollupBootDiscovery": 238,
"lib/jobs/reminder/register-rollup.ts :: registerRollupQueues": 616,
"lib/jobs/reminder/register-status.ts :: registerStatusQueues": 542,
"lib/jobs/workout-insight-generate.ts :: runWorkoutInsightGenerate": 324,
"lib/labs/biomarker-merge.ts :: runMerge": 254,
"lib/labs/ocr-extract.ts :: runOcrExtraction": 207,
@@ -389,7 +381,7 @@
"lib/withings/sync-activity.ts :: syncUserActivity": 377,
"lib/withings/sync-ecg.ts :: syncUserEcg": 202,
"lib/withings/sync-sleep.ts :: syncUserSleep": 414,
"lib/withings/sync.ts :: syncUserMeasurements": 373,
"lib/withings/sync.ts :: syncUserMeasurements": 337,
"proxy.ts :: proxy": 499
}
}
+3 -21
View File
@@ -1,7 +1,7 @@
{
"$comment": "tests that read a source file by path, per test file under src/. Maintained by src/__tests__/test-realism-ratchet.test.ts.",
"count": 366,
"total": 366,
"count": 348,
"total": 348,
"entries": {
"__tests__/account-payload-consumer-guard.test.ts": 1,
"__tests__/admin-data-wipe-completeness.test.ts": 1,
@@ -302,6 +302,7 @@
"lib/export/__tests__/intraday-profile-backup.test.ts": 1,
"lib/feature-flags/__tests__/no-dead-assistant-flag.test.ts": 1,
"lib/fitbit/__tests__/sync-module-graph.test.ts": 1,
"lib/fitbit/jobs/__tests__/queues.test.ts": 1,
"lib/format/__tests__/bytes.test.ts": 1,
"lib/google-health/__tests__/sync-module-graph.test.ts": 1,
"lib/http/__tests__/deprecation.test.ts": 1,
@@ -313,44 +314,25 @@
"lib/jobs/__tests__/ai-job-capability-inventory.test.ts": 1,
"lib/jobs/__tests__/apple-health-import-ecg.test.ts": 1,
"lib/jobs/__tests__/apple-health-import-export-date.test.ts": 1,
"lib/jobs/__tests__/apple-health-import-reconcile.test.ts": 1,
"lib/jobs/__tests__/apple-health-import-send-policy.test.ts": 1,
"lib/jobs/__tests__/coach-memory-refresh-queue.test.ts": 1,
"lib/jobs/__tests__/coach-message-cleanup-queue.test.ts": 1,
"lib/jobs/__tests__/cumulative-pr-rederive-queue.test.ts": 1,
"lib/jobs/__tests__/cycle-reminder-queue.test.ts": 1,
"lib/jobs/__tests__/daily-briefing.test.ts": 1,
"lib/jobs/__tests__/data-arrival-provider-isolation.test.ts": 1,
"lib/jobs/__tests__/data-arrival-queue.test.ts": 1,
"lib/jobs/__tests__/data-backup-send-policy.test.ts": 1,
"lib/jobs/__tests__/drain-cumulative-queue.test.ts": 1,
"lib/jobs/__tests__/fitbit-queues.test.ts": 1,
"lib/jobs/__tests__/geo-backfill.test.ts": 1,
"lib/jobs/__tests__/geolite2-fetch-queue.test.ts": 1,
"lib/jobs/__tests__/google-health-queues.test.ts": 1,
"lib/jobs/__tests__/insight-pregenerate.test.ts": 1,
"lib/jobs/__tests__/insight-status-generate.test.ts": 1,
"lib/jobs/__tests__/intake-slot-dedup-queue.test.ts": 1,
"lib/jobs/__tests__/integration-backfill-admission.test.ts": 1,
"lib/jobs/__tests__/job-failures-retention.test.ts": 1,
"lib/jobs/__tests__/job-outcome-source-inventory.test.ts": 1,
"lib/jobs/__tests__/mcp-token-cleanup-queue.test.ts": 1,
"lib/jobs/__tests__/mean-consolidation-queue.test.ts": 1,
"lib/jobs/__tests__/measurement-reminder-queue.test.ts": 1,
"lib/jobs/__tests__/measurement-tombstone-cleanup.test.ts": 1,
"lib/jobs/__tests__/nightscout-queues.test.ts": 1,
"lib/jobs/__tests__/period-narrative-warm.test.ts": 1,
"lib/jobs/__tests__/queue-policy-tables.test.ts": 1,
"lib/jobs/__tests__/read-side-effect-queues.test.ts": 1,
"lib/jobs/__tests__/recovery-score-queue.test.ts": 1,
"lib/jobs/__tests__/sleep-timeline-queues.test.ts": 1,
"lib/jobs/__tests__/status-cron-candidates.test.ts": 1,
"lib/jobs/__tests__/step-consolidation-repair-queue.test.ts": 1,
"lib/jobs/__tests__/stress-strain-retention-queue.test.ts": 1,
"lib/jobs/__tests__/subsystem-surface.test.ts": 1,
"lib/jobs/__tests__/tls-pin-monitor-queue.test.ts": 1,
"lib/jobs/__tests__/whoop-queues.test.ts": 1,
"lib/jobs/__tests__/withings-ecg-worker.test.ts": 1,
"lib/jobs/__tests__/withings-queues.test.ts": 1,
"lib/jobs/__tests__/workout-insight-queue.test.ts": 1,
"lib/jobs/reminder/__tests__/integration-cohort-outcomes.test.ts": 1,
@@ -1,7 +1,7 @@
{
"$comment": "Comment lines that open with a release version, per file under src/. Maintained by src/__tests__/version-stamp-ratchet.test.ts.",
"count": 2950,
"total": 10187,
"count": 2915,
"total": 9853,
"entries": {
"__tests__/account-payload-consumer-guard.test.ts": 2,
"__tests__/acting-account-boundary-guard.test.ts": 7,
@@ -2062,6 +2062,10 @@
"lib/fitbit/__tests__/poll-cohort.test.ts": 1,
"lib/fitbit/__tests__/sync-user-fitbit.test.ts": 1,
"lib/fitbit/client.ts": 1,
"lib/fitbit/jobs/__tests__/backfill.test.ts": 1,
"lib/fitbit/jobs/backfill.ts": 1,
"lib/fitbit/jobs/oauth-state-cleanup.ts": 1,
"lib/fitbit/jobs/sync-handlers.ts": 2,
"lib/fitbit/sync.ts": 1,
"lib/format-locale.ts": 1,
"lib/format.ts": 4,
@@ -2077,7 +2081,6 @@
"lib/google-health/__tests__/sync-intraday-daily-wide.test.ts": 1,
"lib/google-health/__tests__/sync-intraday-overlap.test.ts": 1,
"lib/google-health/__tests__/sync-sleep-morning-refresh.test.ts": 1,
"lib/google-health/client.ts": 1,
"lib/google-health/sync-core.ts": 2,
"lib/google-health/sync-metrics.ts": 1,
"lib/i18n/__tests__/relative-time-updated.test.ts": 2,
@@ -2285,59 +2288,33 @@
"lib/integrations/metric-freshness.ts": 1,
"lib/integrations/status.ts": 19,
"lib/intl/__tests__/formatter-cache.test.ts": 1,
"lib/jobs/__tests__/apple-health-import-reconcile.test.ts": 2,
"lib/jobs/__tests__/apple-health-import-send-policy.test.ts": 1,
"lib/jobs/__tests__/coach-memory-refresh-queue.test.ts": 2,
"lib/jobs/__tests__/coach-message-cleanup-queue.test.ts": 1,
"lib/jobs/__tests__/apple-health-import-reconcile.test.ts": 1,
"lib/jobs/__tests__/coach-nudge.test.ts": 3,
"lib/jobs/__tests__/coach-reminder-sweep.test.ts": 1,
"lib/jobs/__tests__/cumulative-pr-rederive-queue.test.ts": 1,
"lib/jobs/__tests__/cycle-reminder-queue.test.ts": 2,
"lib/jobs/__tests__/cycle-reminder.test.ts": 2,
"lib/jobs/__tests__/data-arrival-queue.test.ts": 1,
"lib/jobs/__tests__/drain-cumulative-queue.test.ts": 4,
"lib/jobs/__tests__/feedback-aggregator.test.ts": 2,
"lib/jobs/__tests__/fitbit-backfill.test.ts": 1,
"lib/jobs/__tests__/fitbit-queues.test.ts": 2,
"lib/jobs/__tests__/geo-backfill.test.ts": 3,
"lib/jobs/__tests__/google-health-queues.test.ts": 1,
"lib/jobs/__tests__/geo-backfill.test.ts": 1,
"lib/jobs/__tests__/google-health-sleep-repair.test.ts": 1,
"lib/jobs/__tests__/hero-line-json-envelope.test.ts": 1,
"lib/jobs/__tests__/insight-pregenerate-retry-enqueue.test.ts": 1,
"lib/jobs/__tests__/insight-pregenerate.test.ts": 16,
"lib/jobs/__tests__/insight-status-generate.test.ts": 2,
"lib/jobs/__tests__/insight-pregenerate.test.ts": 14,
"lib/jobs/__tests__/intake-auto-skip.test.ts": 3,
"lib/jobs/__tests__/intake-auto-take.test.ts": 1,
"lib/jobs/__tests__/intake-slot-dedup-queue.test.ts": 6,
"lib/jobs/__tests__/job-run-report.test.ts": 1,
"lib/jobs/__tests__/mean-consolidation-queue.test.ts": 2,
"lib/jobs/__tests__/measurement-reminder-queue.test.ts": 2,
"lib/jobs/__tests__/measurement-reminder.test.ts": 6,
"lib/jobs/__tests__/measurement-tombstone-cleanup.test.ts": 2,
"lib/jobs/__tests__/medication-inventory-expire.test.ts": 1,
"lib/jobs/__tests__/medication-low-stock.test.ts": 2,
"lib/jobs/__tests__/medication-reminder-check.test.ts": 2,
"lib/jobs/__tests__/mood-reminder.test.ts": 2,
"lib/jobs/__tests__/nightscout-queues.test.ts": 3,
"lib/jobs/__tests__/period-narrative-warm.test.ts": 1,
"lib/jobs/__tests__/reaction-line-degradation.test.ts": 2,
"lib/jobs/__tests__/recovery-score-queue.test.ts": 2,
"lib/jobs/__tests__/recovery-score.test.ts": 1,
"lib/jobs/__tests__/reminder-phases.test.ts": 1,
"lib/jobs/__tests__/reminder-satisfy.test.ts": 3,
"lib/jobs/__tests__/report-worker-error.test.ts": 1,
"lib/jobs/__tests__/run-job.test.ts": 1,
"lib/jobs/__tests__/sleep-timeline-backfill.test.ts": 1,
"lib/jobs/__tests__/sleep-timeline-queues.test.ts": 2,
"lib/jobs/__tests__/status-cron-candidates.test.ts": 2,
"lib/jobs/__tests__/step-consolidation-repair-queue.test.ts": 1,
"lib/jobs/__tests__/stress-strain-retention-queue.test.ts": 2,
"lib/jobs/__tests__/tls-pin-monitor-queue.test.ts": 2,
"lib/jobs/__tests__/whoop-backfill.test.ts": 1,
"lib/jobs/__tests__/whoop-queues.test.ts": 2,
"lib/jobs/__tests__/whoop-resource-sync.test.ts": 1,
"lib/jobs/__tests__/withings-queues.test.ts": 2,
"lib/jobs/__tests__/workout-insight-queue.test.ts": 1,
"lib/jobs/apple-health-import-worker.ts": 4,
"lib/jobs/coach-message-cleanup.ts": 1,
"lib/jobs/coach-nudge-ai.ts": 3,
@@ -2359,8 +2336,6 @@
"lib/jobs/environment-air-quality-history.ts": 1,
"lib/jobs/environment-fetch.ts": 2,
"lib/jobs/feedback-aggregator.ts": 6,
"lib/jobs/fitbit-backfill.ts": 1,
"lib/jobs/fitbit-oauth-state-cleanup.ts": 1,
"lib/jobs/free-text-encryption-backfill.ts": 8,
"lib/jobs/geo-backfill.ts": 4,
"lib/jobs/google-health-backfill.ts": 1,
@@ -2393,11 +2368,10 @@
"lib/jobs/recovery-score.ts": 1,
"lib/jobs/reminder-phases.ts": 1,
"lib/jobs/reminder-satisfy.ts": 1,
"lib/jobs/reminder-worker.ts": 4,
"lib/jobs/reminder-worker.ts": 3,
"lib/jobs/reminder/__tests__/cleanup-handlers-visibility.test.ts": 1,
"lib/jobs/reminder/__tests__/queue-retention.test.ts": 1,
"lib/jobs/reminder/cleanup-handlers.ts": 7,
"lib/jobs/reminder/fitbit-sync.ts": 2,
"lib/jobs/reminder/google-health-sync.ts": 1,
"lib/jobs/reminder/insights-handlers.ts": 3,
"lib/jobs/reminder/medication-maintenance.ts": 3,
@@ -2407,11 +2381,6 @@
"lib/jobs/reminder/oura-sync.ts": 1,
"lib/jobs/reminder/polar-sync.ts": 2,
"lib/jobs/reminder/poll-cohort.ts": 2,
"lib/jobs/reminder/register-integration-sync.ts": 63,
"lib/jobs/reminder/register-maintenance.ts": 114,
"lib/jobs/reminder/register-reminders.ts": 15,
"lib/jobs/reminder/register-rollup.ts": 37,
"lib/jobs/reminder/register-status.ts": 38,
"lib/jobs/reminder/registrar-shared.ts": 2,
"lib/jobs/reminder/strava-sync.ts": 1,
"lib/jobs/reminder/whoop-sync.ts": 3,
@@ -2712,7 +2681,6 @@
"lib/openapi/routes/workouts.ts": 4,
"lib/optimistic-lock.ts": 1,
"lib/oura/__tests__/sync.test.ts": 1,
"lib/oura/client.ts": 1,
"lib/oura/credentials.ts": 3,
"lib/oura/cycle-sync.ts": 1,
"lib/oura/response-classifier.ts": 1,
@@ -2724,7 +2692,6 @@
"lib/personal-records/pr-detection-worker.ts": 6,
"lib/personal-records/pr-direction.ts": 26,
"lib/polar/__tests__/dedup.test.ts": 1,
"lib/polar/client.ts": 1,
"lib/polar/credentials.ts": 3,
"lib/polar/response-classifier.ts": 1,
"lib/polar/sync-workouts.ts": 1,
@@ -2826,7 +2793,6 @@
"lib/sse/__tests__/create-stream.test.ts": 1,
"lib/sse/create-stream.ts": 3,
"lib/strava/__tests__/dedup.test.ts": 1,
"lib/strava/client.ts": 1,
"lib/strava/credentials.ts": 1,
"lib/strava/response-classifier.ts": 1,
"lib/strava/sync.ts": 1,
@@ -2928,7 +2894,6 @@
"lib/whoop/__tests__/sync-workout-overlap.test.ts": 1,
"lib/whoop/__tests__/sync.test.ts": 1,
"lib/whoop/__tests__/webhook-notification.test.ts": 1,
"lib/whoop/client.ts": 3,
"lib/whoop/connect-ticket.ts": 1,
"lib/whoop/oauth-state.ts": 1,
"lib/whoop/return-scheme.ts": 1,
@@ -2941,13 +2906,13 @@
"lib/withings/__tests__/sync-sleep.test.ts": 4,
"lib/withings/__tests__/sync-typed-classification.test.ts": 1,
"lib/withings/__tests__/sync.test.ts": 1,
"lib/withings/client.ts": 5,
"lib/withings/client.ts": 3,
"lib/withings/oauth-state.ts": 1,
"lib/withings/response-classifier.ts": 1,
"lib/withings/sync-activity.ts": 7,
"lib/withings/sync-ecg.ts": 4,
"lib/withings/sync-sleep.ts": 9,
"lib/withings/sync.ts": 9,
"lib/withings/sync.ts": 6,
"lib/withings/webhook-handler.ts": 3,
"lib/workouts/list-read.ts": 1,
"proxy.ts": 29,
@@ -88,15 +88,18 @@ describe("an empty environment variable counts as unset", () => {
it("T3 — the sweep read the tree and the matcher fires on real source", () => {
// An empty offender list proves nothing if the narrowed walk found no
// files, or if the matcher no longer fires on what the tree actually
// holds. Floor the narrowed set, then put the pre-fix read back into a
// real client and require the matcher to find it there.
// holds. Floor the narrowed set, then put the pre-fix read back into the
// redirect-URI helper every client uses and require the matcher to find it there.
const files = sourceFiles();
expect(files.length).toBeGreaterThan(2400);
expect(files).toContain("lib/withings/client.ts");
const real = readFileSync(join(SRC, "lib/withings/client.ts"), "utf8");
expect(files).toContain("lib/integrations/oauth-kit/redirect-uri.ts");
const real = readFileSync(
join(SRC, "lib/integrations/oauth-kit/redirect-uri.ts"),
"utf8",
);
expect([...real.matchAll(NULLISH_ENV_FALLBACK)]).toEqual([]);
const reverted = real.replace(
/envOr\(\s*"WITHINGS_REDIRECT_URI",/,
/envOr\(envVar,/,
"process.env.WITHINGS_REDIRECT_URI ?? (",
);
expect(reverted).not.toBe(real);
-2
View File
@@ -63,8 +63,6 @@ const NO_FLOOR_NEEDED: Readonly<Record<string, string>> = {
"named walks, each must positively match the keyset cursor",
"components/charts/__tests__/touch-action-guard.test.ts":
"named chart wrappers, each must positively declare touch-pan-y",
"__tests__/moodlog-removal-guard.test.ts":
"absence checks over named paths, columns and keys; no matcher to drift",
// Unit tests of runtime code whose name ends in "guard". They read no
// source and sweep nothing.
+41 -1
View File
@@ -197,6 +197,8 @@ export interface ModuleImports {
value: string[];
/** True when every statement is an `export … from` (or an import). */
reexportOnly: boolean;
/** The value imports evaluated when the module loads: `value` without `import("…")`. */
staticValue: string[];
}
/**
@@ -240,6 +242,7 @@ export function moduleImports(sf: ts.SourceFile): ModuleImports {
}
other += 1;
}
const staticValue = [...value];
const visit = (node: ts.Node): void => {
if (
ts.isCallExpression(node) &&
@@ -252,7 +255,7 @@ export function moduleImports(sf: ts.SourceFile): ModuleImports {
ts.forEachChild(node, visit);
};
visit(sf);
return { value, reexportOnly: reexports > 0 && other === 0 };
return { value, staticValue, reexportOnly: reexports > 0 && other === 0 };
}
export interface ImportGraph {
@@ -374,3 +377,40 @@ export function backEdgeCycles(edges: Map<string, string[]>): string[][] {
for (const v of [...edges.keys()].sort()) walk(v);
return cycles;
}
/**
* Every module under `src/` that loading `entries` evaluates: static value
* imports and re-exports, transitively. A dynamic `import("…")` is not
* followed, since it loads only when it runs. Paths are relative to `src/`.
*/
export function staticImportClosure(entries: readonly string[]): Set<string> {
const cache = ts.createModuleResolutionCache(
process.cwd(),
(x) => x,
COMPILER_OPTIONS,
);
const seen = new Set<string>();
const stack = [...entries];
while (stack.length > 0) {
const rel = stack.pop()!;
if (seen.has(rel)) continue;
seen.add(rel);
const abs = join(SRC, rel);
for (const spec of moduleImports(parse(rel, readFileSync(abs, "utf8")))
.staticValue) {
const hit = ts.resolveModuleName(
spec,
abs,
COMPILER_OPTIONS,
ts.sys,
cache,
).resolvedModule;
if (!hit || hit.isExternalLibraryImport) continue;
if (!hit.resolvedFileName.startsWith(`${SRC}/`)) continue;
const target = hit.resolvedFileName.slice(SRC.length + 1);
if (target.startsWith("generated/") || target.endsWith(".d.ts")) continue;
stack.push(target);
}
}
return seen;
}
+117 -188
View File
@@ -11,23 +11,23 @@
* `src/lib/jobs/queue-runtime.ts` declares every queue `short`, `budgeted` or
* `long`. This guard holds the tree to it:
*
* - every queue bound with `createAndWork` is in the table, and every table
* entry is bound;
* - every `boss.send` and every schedule tuple names a queue in the table;
* - every queue the worker registry declares (`jobs/registry/`) is in the
* table, and every table entry is declared;
* - every `boss.send` and every scheduled queue names a queue in the table;
* - a `long` queue has an expiry between the default and pg-boss's 24-hour
* ceiling, every send and schedule for it carries `expireInSeconds` (or an
* identifier the table names as its carrier), it stops on the job's budget
* where the table says (`jobBudget` / `jobDeadline`, or a declared
* one-shot), and it is exclusive per identity where the table says
* (`lockedPass` at the binding, or the named marker);
* ceiling, every send for it carries `expireInSeconds` (or an identifier
* the table names as its carrier) and its schedule's send options do, it
* stops on the job's budget where the table says (`jobBudget` /
* `jobDeadline`, or a declared one-shot), and it is exclusive per identity
* where the table says (the definition's `lock`, or the named marker);
* - a `budgeted` queue stops on the budget and relies on the default expiry;
* - a `short` queue's sends set no expiry of their own, which would mean the
* job is longer than the table claims.
*
* Floors fail the guard if a matcher stops matching, and the "plants" cases
* run the checks over a planted long queue that lacks each protection in turn
* (the mutation proof kept in the suite). Removing `lockedPass` from the
* pr-detection binding, or the expiry from its cron tuple, fails the real
* (the mutation proof kept in the suite). Removing the `lock` from the
* pr-detection definition, or the expiry from its schedule, fails the real
* checks below.
*/
import { describe, expect, it } from "vitest";
@@ -39,19 +39,14 @@ import {
DEFAULT_JOB_EXPIRE_SECONDS,
QUEUE_RUNTIME,
type QueueRuntime,
type SourceSite,
} from "@/lib/jobs/queue-runtime";
import { SCHEDULED_AND_SENT_QUEUES } from "@/lib/jobs/reminder/registrar-shared";
import { HEAVY_QUEUES } from "@/lib/jobs/heavy-lane";
import type { JobDefinition } from "@/lib/jobs/registry/define-job";
import { allJobDefinitions } from "@/lib/jobs/registry/index";
const SRC = join(process.cwd(), "src");
const REGISTRARS = [
"lib/jobs/reminder/register-integration-sync.ts",
"lib/jobs/reminder/register-maintenance.ts",
"lib/jobs/reminder/register-reminders.ts",
"lib/jobs/reminder/register-rollup.ts",
"lib/jobs/reminder/register-status.ts",
];
const DEFINITIONS = allJobDefinitions();
/** pg-boss refuses an expiry of 24 hours or more. */
const MAX_EXPIRE_SECONDS = 24 * 60 * 60;
@@ -120,27 +115,6 @@ interface Site {
text: string;
}
/** Every `createAndWork(boss, QUEUE, …)` in the registrars. */
function bindings(
texts: ReadonlyMap<string, string>,
definitions: Definitions,
): Site[] {
const out: Site[] = [];
for (const file of REGISTRARS) {
const text = code(texts.get(file) ?? "");
for (const m of text.matchAll(
/createAndWork\s*(?:<[^(]*?>)?\(\s*boss\s*,\s*([A-Z][A-Z0-9_]*)/g,
)) {
const open = text.indexOf("(", m.index);
const queue = resolve(definitions, file, m[1]);
if (queue) {
out.push({ file, queue, text: text.slice(open, closeOf(text, open)) });
}
}
}
return out;
}
/** Every `boss.send(QUEUE, …)` / `getGlobalBoss()?.send(QUEUE, …)`. */
function sends(
texts: ReadonlyMap<string, string>,
@@ -162,31 +136,6 @@ function sends(
return out;
}
/** Every `[QUEUE, CRON, options?]` tuple in a registrar's `schedules`. */
function schedules(
texts: ReadonlyMap<string, string>,
definitions: Definitions,
): Site[] {
const out: Site[] = [];
for (const file of REGISTRARS) {
const text = code(texts.get(file) ?? "");
const m = /const\s+schedules\s*:\s*ScheduleEntry\[\]\s*=\s*\[/.exec(text);
if (!m) continue;
const open = m.index + m[0].length - 1;
const body = text.slice(open + 1, closeOf(text, open) - 1);
for (let i = 0; i < body.length; i += 1) {
if (body[i] !== "[") continue;
const end = closeOf(body, i);
const tuple = body.slice(i, end);
const ident = /^\[\s*([A-Z][A-Z0-9_]*)/.exec(tuple)?.[1];
const queue = ident ? resolve(definitions, file, ident) : null;
if (queue) out.push({ file, queue, text: tuple });
i = end - 1;
}
}
return out;
}
/** The text of `fn` (a function or a const) in `file`. */
function bodyOf(
texts: ReadonlyMap<string, string>,
@@ -201,14 +150,6 @@ function bodyOf(
return text.slice(m.index, end < 0 ? undefined : end + 2);
}
function siteText(
texts: ReadonlyMap<string, string>,
site: SourceSite,
binding: Site | undefined,
): string {
return site === "binding" ? (binding?.text ?? "") : bodyOf(texts, site);
}
const STOPS = /\bjob(?:Budget|Deadline)\s*\(/;
/** Every finding for one queue's declaration against the tree. */
@@ -216,13 +157,12 @@ function check(
queue: string,
entry: QueueRuntime,
texts: ReadonlyMap<string, string>,
all: { bindings: Site[]; sends: Site[]; schedules: Site[] },
all: { definitions: readonly JobDefinition[]; sends: Site[] },
): string[] {
const problems: string[] = [];
const binding = all.bindings.find((b) => b.queue === queue);
const carriers = [...all.sends, ...all.schedules].filter(
(s) => s.queue === queue,
);
const definition = all.definitions.find((d) => d.queue === queue);
const sends = all.sends.filter((s) => s.queue === queue);
const scheduledExpiry = definition?.scheduleOptions?.expireInSeconds;
if (entry.runtime === "long") {
if (
!(entry.expireInSeconds > DEFAULT_JOB_EXPIRE_SECONDS) ||
@@ -230,85 +170,86 @@ function check(
) {
problems.push(`${queue}: expiry ${entry.expireInSeconds}s out of range`);
}
for (const site of carriers) {
for (const site of sends) {
const carried =
/\bexpireInSeconds\b/.test(site.text) ||
entry.expiryVia.some((v) => new RegExp(`\\b${v}\\b`).test(site.text));
if (!carried) {
problems.push(
`${queue}: ${site.file} sends or schedules it without an expiry`,
);
problems.push(`${queue}: ${site.file} sends it without an expiry`);
}
}
if (definition?.cron && typeof scheduledExpiry !== "number") {
problems.push(`${queue}: its schedule carries no expiry`);
}
if (typeof entry.stop === "object" && "oneShot" in entry.stop) {
if (entry.stop.oneShot.length < 20) {
problems.push(`${queue}: a one-shot needs its reason`);
}
} else if (!STOPS.test(siteText(texts, entry.stop, binding))) {
} else if (!STOPS.test(bodyOf(texts, entry.stop))) {
problems.push(`${queue}: does not stop on the job's budget`);
}
if (entry.exclusive === "lockedPass") {
if (!/\blockedPass\s*\(/.test(binding?.text ?? "")) {
problems.push(`${queue}: binding holds no lockedPass`);
if (!definition?.lock) {
problems.push(`${queue}: definition holds no lock`);
}
} else if (
!siteText(texts, entry.exclusive.at, binding).includes(
entry.exclusive.marker,
)
!bodyOf(texts, entry.exclusive.at).includes(entry.exclusive.marker)
) {
problems.push(`${queue}: no "${entry.exclusive.marker}" where declared`);
}
} else {
if (entry.runtime === "budgeted") {
if (!STOPS.test(siteText(texts, entry.budget, binding))) {
if (!STOPS.test(bodyOf(texts, entry.budget))) {
problems.push(`${queue}: does not stop on the job's budget`);
}
}
for (const site of carriers) {
for (const site of sends) {
if (/\bexpireInSeconds\b/.test(site.text)) {
problems.push(
`${queue}: ${site.file} sets an expiry on a queue declared ${entry.runtime}`,
);
}
}
if (scheduledExpiry !== undefined) {
problems.push(
`${queue}: its schedule sets an expiry on a queue declared ${entry.runtime}`,
);
}
}
return problems;
}
function scan(texts: ReadonlyMap<string, string>) {
const definitions = definitionsOf(texts);
return {
bindings: bindings(texts, definitions),
sends: sends(texts, definitions),
schedules: schedules(texts, definitions),
};
return { sends: sends(texts, definitionsOf(texts)) };
}
describe("every queue declares its runtime", () => {
const all = scan(source);
const bound = new Set(all.bindings.map((b) => b.queue));
const all = { definitions: DEFINITIONS, ...scan(source) };
const declared = new Set(DEFINITIONS.map((d) => d.queue));
it("finds the bindings, sends and schedules it is meant to police", () => {
// Pinned below the counts on 2026-10-01 (112 / 64 / 74).
expect(all.bindings.length).toBeGreaterThanOrEqual(105);
it("finds the declarations, sends and schedules it is meant to police", () => {
// Pinned below the counts on 2026-10-23 (122 / 64 / 78).
expect(DEFINITIONS.length).toBeGreaterThanOrEqual(115);
expect(all.sends.length).toBeGreaterThanOrEqual(50);
expect(all.schedules.length).toBeGreaterThanOrEqual(70);
expect(DEFINITIONS.filter((d) => d.cron).length).toBeGreaterThanOrEqual(70);
expect(
Object.values(QUEUE_RUNTIME).filter((e) => e.runtime === "long").length,
).toBeGreaterThanOrEqual(15);
});
it("every bound queue is in the table, and every entry is bound", () => {
expect([...bound].filter((q) => !(q in QUEUE_RUNTIME)).sort()).toEqual([]);
it("every declared queue is in the table, and every entry is declared", () => {
expect([...declared].filter((q) => !(q in QUEUE_RUNTIME)).sort()).toEqual(
[],
);
expect(
Object.keys(QUEUE_RUNTIME)
.filter((q) => !bound.has(q))
.filter((q) => !declared.has(q))
.sort(),
).toEqual([]);
});
it("every send and schedule names a queue in the table", () => {
const unknown = [...all.sends, ...all.schedules]
it("every send names a queue in the table", () => {
const unknown = all.sends
.filter((s) => !(s.queue in QUEUE_RUNTIME))
.map((s) => `${s.file} → ${s.queue}`);
expect(unknown).toEqual([]);
@@ -322,51 +263,56 @@ describe("every queue declares its runtime", () => {
});
it("plants: a long queue missing any protection is caught", () => {
const registrar = REGISTRARS[1];
const planted = (binding: string, schedule: string) =>
const file = "lib/jobs/planted.ts";
const texts = (handler: string) =>
new Map([
...source,
[
registrar,
`const PLANT_QUEUE = "planted-long";\n` +
`const schedules: ScheduleEntry[] = [\n ${schedule},\n];\n` +
`async function register(boss) {\n await createAndWork(boss, PLANT_QUEUE, { localConcurrency: 1 }, ${binding});\n}\n`,
],
[file, `export async function plant(jobs) {\n ${handler}\n}\n`],
]);
const entry: QueueRuntime = {
runtime: "long",
expireInSeconds: 3600,
expiryVia: [],
stop: "binding",
stop: { file, fn: "plant" },
exclusive: "lockedPass",
why: "planted",
};
const findings = (binding: string, schedule: string) => {
const texts = planted(binding, schedule);
return check("planted-long", entry, texts, scan(texts));
const planted = (overrides: Partial<JobDefinition>): JobDefinition => ({
queue: "planted-long",
cron: "0 3 * * *",
scheduleOptions: { expireInSeconds: 3600 },
work: { localConcurrency: 1 },
lock: () => "pass",
load: async () => async () => ({ ok: true, did: {} }),
...overrides,
});
const findings = (
handler: string,
overrides: Partial<JobDefinition> = {},
) => {
const t = texts(handler);
return check("planted-long", entry, t, {
definitions: [planted(overrides)],
...scan(t),
});
};
const safe = [
"lockedPass(PLANT_QUEUE, () => WHOLE_PASS, async (jobs) => run(jobBudget(jobs)))",
"[PLANT_QUEUE, CRON, { expireInSeconds: 3600 }]",
] as const;
expect(findings(...safe)).toEqual([]);
expect(findings(safe[0], "[PLANT_QUEUE, CRON]")).toEqual([
`planted-long: ${registrar} sends or schedules it without an expiry`,
const budgeted = "return run(jobBudget(jobs));";
expect(findings(budgeted)).toEqual([]);
expect(findings(budgeted, { scheduleOptions: { retryLimit: 0 } })).toEqual([
"planted-long: its schedule carries no expiry",
]);
expect(
findings("lockedPass(PLANT_QUEUE, () => WHOLE_PASS, run)", safe[1]),
).toEqual(["planted-long: does not stop on the job's budget"]);
expect(findings("async (jobs) => run(jobBudget(jobs))", safe[1])).toEqual([
"planted-long: binding holds no lockedPass",
expect(findings("return run(jobs);")).toEqual([
"planted-long: does not stop on the job's budget",
]);
expect(findings(budgeted, { lock: undefined })).toEqual([
"planted-long: definition holds no lock",
]);
expect(
check(
"planted-long",
{ ...entry, expireInSeconds: 900 },
...((): [ReadonlyMap<string, string>, ReturnType<typeof scan>] => {
const texts = planted(...safe);
return [texts, scan(texts)];
})(),
texts(budgeted),
{ definitions: [planted({})], ...scan(texts(budgeted)) },
),
).toEqual(["planted-long: expiry 900s out of range"]);
});
@@ -374,12 +320,13 @@ describe("every queue declares its runtime", () => {
/**
* Queues a file sends to through a variable (`boss.send(queueName, …)`): every
* known queue name the file spells out, as a constant or a map value, counts
* as sent, because the matcher above cannot tell which one the variable holds.
* known queue the file names, as a literal, a map value or an imported
* `*_QUEUE` constant, counts as sent, because the matcher above cannot tell which one the variable holds.
*/
function dynamicallySent(texts: ReadonlyMap<string, string>): Set<string> {
const definitions = definitionsOf(texts);
const out = new Set<string>();
for (const [, raw] of texts) {
for (const [file, raw] of texts) {
const text = code(raw);
if (
!/\b(?:boss|getGlobalBoss\(\)\??)\s*\.\s*send\s*(?:<[^(]*?>)?\(\s*[a-z]/.test(
@@ -391,13 +338,20 @@ function dynamicallySent(texts: ReadonlyMap<string, string>): Set<string> {
for (const m of text.matchAll(/"([a-z0-9-]+)"/g)) {
if (m[1] in QUEUE_RUNTIME) out.add(m[1]);
}
// A queue named through an imported constant counts the same.
for (const m of text.matchAll(/\b([A-Z][A-Z0-9_]*_QUEUE)\b/g)) {
const queue = resolve(definitions, file, m[1]);
if (queue && queue in QUEUE_RUNTIME) out.add(queue);
}
}
return out;
}
describe("a cron-fed queue polls slowly, a sent one does not", () => {
const all = scan(source);
const scheduled = new Set(all.schedules.map((s) => s.queue));
const scheduled = new Set(
DEFINITIONS.filter((d) => d.cron).map((d) => d.queue),
);
const sent = new Set([
...all.sends.map((s) => s.queue),
...dynamicallySent(source),
@@ -418,26 +372,14 @@ describe("a cron-fed queue polls slowly, a sent one does not", () => {
});
describe("heavy jobs share the lane", () => {
const all = scan(source);
const laned = all.bindings.filter((b) => /\bheavyLane\s*\(/.test(b.text));
const laned = DEFINITIONS.filter((d) => d.lane);
it("binds every heavy queue in the lane, and nothing else", () => {
expect(laned.map((b) => b.queue).sort()).toEqual(
it("puts every heavy queue in the lane, and nothing else", () => {
expect(laned.map((d) => d.queue).sort()).toEqual(
Object.keys(HEAVY_QUEUES).sort(),
);
});
it("holds a lockedPass outside the lane, so an expiry retry finds the run going", () => {
// Outside, a retry pg-boss starts while the expired run still holds the
// lane finds the lock and settles; inside, the lane would defer it and
// run the job a second time once the first is done.
const inverted = laned
.filter((b) => /\blockedPass\s*\(/.test(b.text))
.filter((b) => b.text.indexOf("lockedPass") > b.text.indexOf("heavyLane"))
.map((b) => b.queue);
expect(inverted).toEqual([]);
});
it("declares every heavy queue long, so its expiry, stop and exclusivity are proven", () => {
// A heavy job is the one most likely to run past pg-boss's default
// expiry, and the retry that follows an expiry runs it a second time.
@@ -464,41 +406,28 @@ describe("heavy jobs share the lane", () => {
// A deferred job returns from active to created. Under `short` (one
// queued job per key) and `stately` (one per state) a twin already
// queued would refuse it, and the job would fail instead of waiting.
const policies = policiesOf(source);
expect(policies.size).toBeGreaterThanOrEqual(40);
const refused = Object.keys(HEAVY_QUEUES)
.filter((queue) => /^(?:short|stately)$/.test(policies.get(queue) ?? ""))
.sort();
expect(refused).toEqual([]);
// The planted case: the same check over a table that declares one.
const planted = new Map(source);
const file = REGISTRARS[2];
planted.set(
file,
`const DATA_BACKUP_QUEUE = "data-backup";\nconst queuePolicies: QueuePolicyTable = {\n [DATA_BACKUP_QUEUE]: { policy: "short", reason: "x" },\n};\n`,
const refusing = (definitions: readonly JobDefinition[]) =>
definitions
.filter(
(d) => d.lane && /^(?:short|stately)$/.test(d.policy?.policy ?? ""),
)
.map((d) => d.queue)
.sort();
expect(DEFINITIONS.filter((d) => d.policy).length).toBeGreaterThanOrEqual(
40,
);
expect(policiesOf(planted).get("data-backup")).toBe("short");
expect(refusing(DEFINITIONS)).toEqual([]);
// The planted case: the same check over a heavy queue declared `short`.
expect(
refusing([
{
queue: "data-backup",
policy: { policy: "short", reason: "x" },
work: { localConcurrency: 1 },
lane: true,
load: async () => async () => ({ ok: true, did: {} }),
},
]),
).toEqual(["data-backup"]);
});
});
/** Every `[QUEUE]: { policy: "…" }` in a registrar's `queuePolicies`. */
function policiesOf(texts: ReadonlyMap<string, string>): Map<string, string> {
const definitions = definitionsOf(texts);
const out = new Map<string, string>();
for (const file of REGISTRARS) {
const text = code(texts.get(file) ?? "");
const m = /const\s+queuePolicies\s*:\s*QueuePolicyTable\s*=\s*\{/.exec(
text,
);
if (!m) continue;
const open = m.index + m[0].length - 1;
const body = text.slice(open, closeOf(text, open));
for (const entry of body.matchAll(
/\[\s*([A-Z][A-Z0-9_]*)\s*\]\s*:\s*\{\s*policy\s*:\s*"([a-z]+)"/g,
)) {
const queue = resolve(definitions, file, entry[1]);
if (queue) out.set(queue, entry[2]);
}
}
return out;
}
@@ -39,6 +39,13 @@ const TAIL = "runs afterMeasurementMutation";
const TAIL_VIA_PREFIX = "runs afterMeasurementMutation via ";
const TAIL_VIA = (file: string) => `${TAIL_VIA_PREFIX}${file}`;
/**
* Runs the provider write step (`integrations/sync-kit/after-write.ts`),
* which is `afterMeasurementMutation` plus the arrivals and, where asked, the
* dashboard snapshot.
*/
const PROVIDER_TAIL = "runs afterProviderWrite";
/**
* Every file with a measurement write, and how it keeps the two readers
* current. A file listed here must still contain a write (a stale entry
@@ -66,17 +73,13 @@ const WRITE_SITES: Record<string, string> = {
// Retiring the equivalent-distance rows earlier Oura syncs wrote.
"lib/oura/retire-distance.ts": TAIL,
// Integration syncs: each collects the (type, day) pairs it touched and
// runs both legs itself at the end of the sync.
"lib/fitbit/sync-core.ts":
"refolds the touched days and re-warms the touched types at the end of the upsert",
"lib/google-health/sync-core.ts":
"refolds the touched days and re-warms the touched types at the end of the upsert",
"lib/nightscout/sync.ts":
"refolds the touched days and re-warms the touched types at the end of the sync",
// runs the provider write step over them.
"lib/fitbit/sync-core.ts": PROVIDER_TAIL,
"lib/google-health/sync-core.ts": PROVIDER_TAIL,
"lib/nightscout/sync.ts": PROVIDER_TAIL,
"lib/withings/sync.ts": PROVIDER_TAIL,
"lib/oura/sync-heart-rate.ts":
"collects the days each heart-rate page touched; refolds them and re-warms the type once per window",
"lib/withings/sync.ts":
"refolds the touched days and re-warms the touched types at the end of the sync",
"lib/withings/sync-activity.ts":
"refolds the touched days (and the day a moved row left) and re-warms the touched types",
"lib/withings/sync-ecg.ts":
@@ -165,10 +168,15 @@ describe("measurement write tail", () => {
});
it("the files that claim the shared tail call it", () => {
const missing = Object.entries(WRITE_SITES)
.filter(
([, reason]) => reason === TAIL || reason.startsWith(TAIL_VIA_PREFIX),
)
const claims = Object.entries(WRITE_SITES).filter(
([, reason]) => reason === TAIL || reason.startsWith(TAIL_VIA_PREFIX),
);
// Both shapes are in use; a set that lost either would check nothing.
expect(claims.filter(([, r]) => r === TAIL).length).toBeGreaterThan(5);
expect(
claims.filter(([, r]) => r.startsWith(TAIL_VIA_PREFIX)).length,
).toBeGreaterThan(0);
const missing = claims
.map(([file, reason]) =>
reason.startsWith(TAIL_VIA_PREFIX)
? reason.slice(TAIL_VIA_PREFIX.length)
@@ -183,6 +191,18 @@ describe("measurement write tail", () => {
expect(missing).toEqual([]);
});
it("the files that claim the provider write step call it", () => {
const claimed = Object.entries(WRITE_SITES)
.filter(([, reason]) => reason === PROVIDER_TAIL)
.map(([file]) => file);
expect(claimed.length).toBeGreaterThan(0);
const missing = claimed.filter(
(file) =>
!/afterProviderWrite\s*\(/.test(readFileSync(join(SRC, file), "utf8")),
);
expect(missing).toEqual([]);
});
it("the syncs that run both legs by hand call both", () => {
const handRolled = Object.entries(WRITE_SITES)
.filter(([, reason]) => reason.includes("re-warms"))
+5 -7
View File
@@ -18,11 +18,12 @@
*/
import { readFileSync, existsSync } from "node:fs";
import path from "node:path";
import { allJobDefinitions } from "@/lib/jobs/registry/index";
import { describe, expect, it } from "vitest";
import { INTEGRATION_CADENCE } from "@/lib/integrations/sync-verdict";
import { INTEGRATION_DISPLAY_NAMES } from "@/components/settings/integrations/integration-fallback-row";
import { readSource } from "@/__tests__/helpers/source-files";
const ROOT = path.resolve(__dirname, "../..");
@@ -76,12 +77,9 @@ describe("moodLog integration stays removed", () => {
});
it("registers no moodlog queue, cron or handler binding", () => {
const registrar = readSource(
"src/lib/jobs/reminder/register-integration-sync.ts",
"export async function registerIntegrationSyncQueues",
);
expect(registrar).not.toContain("moodlog");
expect(registrar).not.toContain("MoodLog");
const queues = allJobDefinitions().map((job) => job.queue);
expect(queues.length).toBeGreaterThan(100);
expect(queues.filter((queue) => /mood-?log/i.test(queue))).toEqual([]);
});
it("carries no moodlog integration key on the status envelope", () => {
+31 -14
View File
@@ -10,17 +10,23 @@
*
* A. Each kit provider has `src/lib/<provider>/oauth-descriptor.ts`, and the
* descriptor names that provider.
* B. A provider client's `refreshAccessToken` is imported by that
* provider's descriptor and by nothing else outside tests.
* B. A provider's refresh (`<provider>OAuth.refresh(…)`, from the shared
* OAuth client factory) is called by that provider's descriptor and by
* nothing else outside tests.
* C. The refresh lock is taken by the kit and by the AI provider store
* (`src/lib/ai/provider.ts`, which is not an integration) only.
* D. A token is sealed into a connection row only by a callback route
* (the connect-time write). The kit writes ciphertext the descriptor is
* handed and never encrypts on its own.
* E. A provider integration speaks to a token endpoint only through the
* factory (`oauth-kit/client.ts`): no other integration module builds a
* `grant_type` form. Sign-in (OIDC), the MCP authorization server and
* the Codex device login are not provider integrations.
*
* Fitbit is the one listed exception: the classic Fitbit Web API is retired
* on 2026-10-30 and its integration rides out on its own refresh until then,
* rather than being migrated onto a lifecycle it will never use.
* Fitbit is the one listed exception to the lifecycle: the classic Fitbit Web
* API is retired on 2026-10-30 and its integration rides out on its own
* refresh until then, rather than being migrated onto a lifecycle it will
* never use. Its wire goes through the factory like every other provider's.
*
* Every leg asserts a non-empty match set before it compares, so a matcher
* that stops matching fails instead of passing.
@@ -68,21 +74,18 @@ describe("OAuth token lifecycle guard", () => {
}
});
it("B: a provider's refreshAccessToken is imported only by its descriptor", () => {
const importers = files.filter((f) => {
const src = read(f);
return /import\s*\{[^}]*\brefreshAccessToken\b[^}]*\}\s*from\s*["'](?:\.\/client|@\/lib\/[a-z-]+\/client)["']/.test(
src,
);
});
expect(importers.length).toBeGreaterThan(0);
it("B: a provider's refresh is called only by its descriptor", () => {
const callers = files.filter((f) =>
/\b[a-z][A-Za-z]*OAuth\.refresh\s*\(/.test(read(f)),
);
expect(callers.length).toBeGreaterThan(0);
const allowed = new Set([
...KIT_PROVIDERS.filter((p) => p !== "polar").map(
(p) => `src/lib/${p}/oauth-descriptor.ts`,
),
...RETIRED,
]);
expect(importers.filter((f) => !allowed.has(f))).toEqual([]);
expect(callers.filter((f) => !allowed.has(f))).toEqual([]);
});
it("C: only the kit and the AI provider store take the refresh lock", () => {
@@ -111,4 +114,18 @@ describe("OAuth token lifecycle guard", () => {
);
expect(offenders).toEqual([]);
});
it("E: provider integrations reach a token endpoint only through the factory", () => {
const forms = files.filter((f) => /\bgrant_type\b/.test(read(f)));
expect(forms.length).toBeGreaterThan(0);
const NOT_INTEGRATIONS = new Set([
"src/lib/integrations/oauth-kit/client.ts",
"src/lib/auth/oidc.ts",
"src/lib/ai/codex-oauth.ts",
"src/lib/mcp/oauth/metadata.ts",
"src/app/api/mcp/oauth/token/route.ts",
"src/app/api/mcp/oauth/register/route.ts",
]);
expect(forms.filter((f) => !NOT_INTEGRATIONS.has(f)).sort()).toEqual([]);
});
});
+1 -1
View File
@@ -35,7 +35,7 @@ const LIB = join(process.cwd(), "src", "lib");
* three outcomes cannot carry.
*/
const EXCEPTIONS: Readonly<Record<string, { matches: number; why: string }>> = {
"google-health/client.ts": {
"google-health/data-points.ts": {
matches: 1,
why: "progress-bounded streaming list walk with slice budget and resume (forEachDataPointPage)",
},
@@ -0,0 +1,70 @@
/**
* A provider sync loads none of the AI layer.
*
* Thirteen sync modules once imported `insights/comprehensive-generate` to
* reach one invalidation function it re-exported, so every sync worker
* evaluated the whole insight generator. The write tail is
* `integrations/sync-kit/after-write.ts` now, and it reaches the status cards
* through `insights/status-invalidation`. This guard holds that:
*
* - no production module in a provider folder (`PROVIDER_DIRS`) imports
* anything under `lib/ai/` or the comprehensive generator;
* - and loading any of them does not evaluate the comprehensive generator
* transitively either.
*
* The planted case runs the transitive check over a module that does load
* it, so a walk that resolves nothing cannot pass.
*/
import { describe, expect, it } from "vitest";
import { PROVIDER_DIRS } from "./helpers/provider-dirs";
import { requireFloor, walkSourceFiles } from "./helpers/source-files";
import {
SRC,
moduleImports,
parse,
readSrc,
staticImportClosure,
} from "./helpers/source-metrics";
const GENERATOR = "lib/insights/comprehensive-generate.ts";
const FORBIDDEN_SPEC = /^@\/lib\/(?:ai\/|insights\/comprehensive-generate$)/;
function providerModules(): string[] {
return PROVIDER_DIRS.flatMap((dir) =>
walkSourceFiles(`${SRC}/lib/${dir}`, { floor: 1 })
.filter((f) => !f.includes("__tests__") && !/\.test\.tsx?$/.test(f))
.map((f) => `lib/${dir}/${f}`),
);
}
describe("provider folders and the AI layer", () => {
const modules = providerModules();
it("reads every provider folder", () => {
requireFloor("provider modules", modules, 60);
});
it("imports nothing from lib/ai or the comprehensive generator", () => {
const offenders = modules.flatMap((rel) =>
moduleImports(parse(rel, readSrc(rel)))
.value.filter((spec) => FORBIDDEN_SPEC.test(spec))
.map((spec) => `${rel} → ${spec}`),
);
expect(offenders).toEqual([]);
});
it("does not load the comprehensive generator transitively", () => {
const closure = staticImportClosure(modules);
expect(closure.size).toBeGreaterThan(modules.length);
expect(closure.has(GENERATOR)).toBe(false);
});
it("catches a module that loads it (planted)", () => {
expect(
staticImportClosure(["lib/jobs/morning-digest-refresh.ts"]).has(
GENERATOR,
),
).toBe(true);
});
});
@@ -83,7 +83,7 @@ const PROVIDER_INGESTS: ProviderIngest[] = [
provider: "Polar",
mapper: "mapPolarSportType",
mapperFile: "lib/polar/sport-map.ts",
captureFile: "lib/polar/client.ts",
captureFile: "lib/polar/map.ts",
captureField: "detailed_sport_info",
writeFile: "lib/polar/sync-workouts.ts",
metadataKey: "polarSport",
@@ -92,7 +92,7 @@ const PROVIDER_INGESTS: ProviderIngest[] = [
provider: "Strava",
mapper: "mapStravaSportType",
mapperFile: "lib/strava/sport-map.ts",
captureFile: "lib/strava/client.ts",
captureFile: "lib/strava/map.ts",
captureField: "rawSportType",
writeFile: "lib/strava/sync.ts",
metadataKey: "stravaType",
+1 -1
View File
@@ -32,10 +32,10 @@ import {
import { readBackupPassActivity } from "@/lib/jobs/backup-pass-attempts";
import { summariseBackupSchedule } from "@/lib/jobs/backup-schedule-status";
import {
OFFHOST_BACKUP_QUEUE,
offhostBackupConfigured,
probeOffhostLifecycle,
} from "@/lib/jobs/offhost-backup";
import { OFFHOST_BACKUP_QUEUE } from "@/lib/jobs/offhost-backup-queue";
import {
classifyOffhostBackup,
OFFHOST_BACKUP_PERIOD_HOURS,
+1 -1
View File
@@ -31,10 +31,10 @@ import { annotate } from "@/lib/logging/context";
import { auditLog } from "@/lib/auth/audit";
import { getGlobalBoss } from "@/lib/jobs/boss-instance";
import {
ENCRYPTION_KEY_ROTATE_QUEUE,
ENCRYPTION_KEY_ROTATE_SINGLETON,
ENCRYPTION_KEY_ROTATE_EXPIRE_SECONDS,
} from "@/lib/jobs/encryption-key-rotate";
import { ENCRYPTION_KEY_ROTATE_QUEUE } from "@/lib/jobs/encryption-key-rotate-queue";
import { getActiveKeyId } from "@/lib/crypto";
import { NextRequest } from "next/server";
@@ -21,12 +21,12 @@ import { auditLog } from "@/lib/auth/audit";
import { checkRateLimit } from "@/lib/rate-limit";
import { getGlobalBoss } from "@/lib/jobs/boss-instance";
import {
APPLE_HEALTH_IMPORT_V2_QUEUE,
APPLE_HEALTH_IMPORT_KIND,
APPLE_HEALTH_IMPORT_PARSER_REVISION,
APPLE_HEALTH_IMPORT_SEND_OPTIONS,
type AppleHealthImportPayload,
} from "@/lib/jobs/apple-health-import-worker";
import { APPLE_HEALTH_IMPORT_V2_QUEUE } from "@/lib/jobs/apple-health-import-queue";
import { streamMultipartToDisk } from "@/lib/multipart/stream-to-disk";
import {
createImportJobUnlessBusy,
+7 -8
View File
@@ -7,15 +7,12 @@ import { apiHandler, requireAuth } from "@/lib/api-handler";
import { annotate, getEvent } from "@/lib/logging/context";
import { auditLog } from "@/lib/auth/audit";
import { encrypt } from "@/lib/crypto";
import {
exchangeCode,
fetchProfile,
resolveFitbitUserId,
FITBIT_OAUTH_SCOPE,
} from "@/lib/fitbit/client";
import { fetchProfile, resolveFitbitUserId } from "@/lib/fitbit/client";
import { fitbitOAuth } from "@/lib/fitbit/oauth";
import { FITBIT_OAUTH_SCOPE } from "@/lib/fitbit/oauth";
import { getUserFitbitCredentials } from "@/lib/fitbit/credentials";
import { FITBIT_OAUTH_STATE_COOKIE } from "@/lib/fitbit/oauth-state";
import { FITBIT_BACKFILL_QUEUE } from "@/lib/jobs/fitbit-backfill";
import { FITBIT_BACKFILL_QUEUE } from "@/lib/fitbit/jobs/queues";
import { enqueueWhenBossReady } from "@/lib/jobs/boss-ready";
import { markReconnected } from "@/lib/integrations/status";
import { Prisma } from "@/generated/prisma/client";
@@ -150,7 +147,9 @@ export const GET = apiHandler(async (request: NextRequest) => {
return ERR("nocreds");
}
const tokens = await exchangeCode(code, consumed.codeVerifier, creds);
const tokens = await fitbitOAuth.exchange(code, creds, {
codeVerifier: consumed.codeVerifier,
});
// The initial code exchange MUST return a refresh token. If Fitbit ever
// omits it, refuse to persist — a stored empty refresh token silently bricks
@@ -27,10 +27,15 @@ vi.mock("@/lib/fitbit/credentials", () => ({
getUserFitbitCredentials: vi.fn(),
}));
vi.mock("@/lib/fitbit/client", () => ({
getAuthorizationUrl: vi.fn(
() => "https://www.fitbit.com/oauth2/authorize?state=nonce-stub",
),
vi.mock("@/lib/fitbit/oauth", () => ({
fitbitOAuth: {
authorizationUrl: vi.fn(
() => "https://www.fitbit.com/oauth2/authorize?state=nonce-stub",
),
},
}));
vi.mock("@/lib/integrations/oauth-kit/pkce", () => ({
generatePkcePair: vi.fn(() => ({
verifier: "verifier-stub",
challenge: "challenge-stub",
+3 -2
View File
@@ -2,7 +2,8 @@ import { apiHandler, requireAuth } from "@/lib/api-handler";
import { prisma } from "@/lib/db";
import { annotate, getEvent } from "@/lib/logging/context";
import { checkRateLimit } from "@/lib/rate-limit";
import { getAuthorizationUrl, generatePkcePair } from "@/lib/fitbit/client";
import { fitbitOAuth } from "@/lib/fitbit/oauth";
import { generatePkcePair } from "@/lib/integrations/oauth-kit/pkce";
import { getUserFitbitCredentials } from "@/lib/fitbit/credentials";
import {
FITBIT_OAUTH_STATE_COOKIE,
@@ -94,7 +95,7 @@ export const GET = apiHandler(async (req: NextRequest) => {
);
}
const url = getAuthorizationUrl(nonce, creds, pkce.challenge);
const url = fitbitOAuth.authorizationUrl(nonce, creds, pkce.challenge);
const response = NextResponse.redirect(url);
response.cookies.set(FITBIT_OAUTH_STATE_COOKIE, nonce, {
+10 -10
View File
@@ -10,14 +10,14 @@ import { annotate, getEvent } from "@/lib/logging/context";
import { auditLog } from "@/lib/auth/audit";
import { encrypt } from "@/lib/crypto";
import {
exchangeCode,
fetchProfile,
resolveGoogleHealthUserId,
getGoogleHealthScopeString,
} from "@/lib/google-health/client";
import { googleHealthOAuth } from "@/lib/google-health/oauth";
import { getGoogleHealthScopeString } from "@/lib/google-health/oauth";
import { getUserGoogleHealthCredentials } from "@/lib/google-health/credentials";
import { GOOGLE_HEALTH_OAUTH_STATE_COOKIE } from "@/lib/google-health/oauth-state";
import { GOOGLE_HEALTH_BACKFILL_QUEUE } from "@/lib/jobs/google-health-backfill";
import { GOOGLE_HEALTH_BACKFILL_QUEUE } from "@/lib/jobs/google-health-backfill-queue";
import { enqueueWhenBossReady } from "@/lib/jobs/boss-ready";
import { markReconnected } from "@/lib/integrations/status";
import { Prisma } from "@/generated/prisma/client";
@@ -162,14 +162,14 @@ export const GET = apiHandler(async (request: NextRequest) => {
return ERR("nocreds");
}
const tokens = await exchangeCode(code, consumed.codeVerifier, creds);
const tokens = await googleHealthOAuth.exchange(code, creds, {
codeVerifier: consumed.codeVerifier,
});
// The initial code exchange MUST return a refresh token (the connect route
// forces `prompt=consent` + `access_type=offline`). If Google ever omits
// it, refuse to persist — a stored empty refresh token silently bricks
// every future token refresh (the refresh POST then 400s and the
// connection dies the moment the access token expires). Error out cleanly
// so the user re-consents rather than landing a dead connection.
// The exchange must return a refresh token (the connect route forces
// `prompt=consent` and `access_type=offline`). A stored empty one would
// fail every later refresh, so the person re-consents instead of landing
// a connection that dies with its first access token.
if (!tokens.refresh_token) {
getEvent()?.addWarning(
`google-health callback: token response carried no refresh_token for user ${recordUserId}`,
@@ -30,12 +30,12 @@ import { emitSignal } from "@/lib/logging/signal";
import { checkRateLimit } from "@/lib/rate-limit";
import { getGlobalBoss } from "@/lib/jobs/boss-instance";
import {
APPLE_HEALTH_IMPORT_V2_QUEUE,
APPLE_HEALTH_IMPORT_KIND,
APPLE_HEALTH_IMPORT_PARSER_REVISION,
APPLE_HEALTH_IMPORT_SEND_OPTIONS,
type AppleHealthImportPayload,
} from "@/lib/jobs/apple-health-import-worker";
import { APPLE_HEALTH_IMPORT_V2_QUEUE } from "@/lib/jobs/apple-health-import-queue";
import { streamMultipartToDisk } from "@/lib/multipart/stream-to-disk";
import {
createImportJobUnlessBusy,
@@ -28,10 +28,10 @@ import { getGlobalBoss } from "@/lib/jobs/boss-instance";
import {
HEALTH_CONNECT_IMPORT_KIND,
HEALTH_CONNECT_IMPORT_PARSER_REVISION,
HEALTH_CONNECT_IMPORT_QUEUE,
HEALTH_CONNECT_IMPORT_SEND_OPTIONS,
type HealthConnectImportPayload,
} from "@/lib/jobs/health-connect-import-worker";
import { HEALTH_CONNECT_IMPORT_QUEUE } from "@/lib/jobs/health-connect-import-queue";
import { streamMultipartToDisk } from "@/lib/multipart/stream-to-disk";
import {
createImportJobUnlessBusy,
@@ -5,13 +5,13 @@ import { checkRateLimit } from "@/lib/rate-limit";
import { annotate } from "@/lib/logging/context";
import { prisma } from "@/lib/db";
import { getValidToken } from "@/lib/google-health/sync-core";
import { fetchDailyRollUp } from "@/lib/google-health/client";
import {
GOOGLE_HEALTH_ACTIVITY_PAGE_SIZE,
GOOGLE_HEALTH_API_BASE,
GOOGLE_HEALTH_DATA_TYPES,
fetchDailyRollUp,
fetchDataPoints,
} from "@/lib/google-health/client";
} from "@/lib/google-health/data-points";
import { GOOGLE_HEALTH_API_BASE } from "@/lib/google-health/request";
import { GOOGLE_HEALTH_DATA_TYPES } from "@/lib/google-health/mappers";
import { GoogleHealthApiError } from "@/lib/google-health/response-classifier";
import { resolveUserTimezone } from "@/lib/tz/resolver";
import { safeFetch, SafeFetchError } from "@/lib/safe-fetch";
+1 -1
View File
@@ -50,7 +50,7 @@ import {
} from "@/lib/withings/plan-features";
import { getPolarClientCredentials } from "@/lib/polar/credentials";
import { getStravaClientCredentials } from "@/lib/strava/credentials";
import { hasActivityScope } from "@/lib/withings/client";
import { hasActivityScope } from "@/lib/withings/oauth";
import { loadConnectionHealth } from "@/lib/integrations/connection-health-load";
import {
googleHealthBackfillEndedList,
@@ -63,10 +63,8 @@ import { auditLog } from "@/lib/auth/audit";
import { getSession } from "@/lib/auth/session";
import { getGlobalBoss } from "@/lib/jobs/boss-instance";
import { checkRateLimit } from "@/lib/rate-limit";
import {
MEDICATION_INTAKE_IMPORT_QUEUE,
MEDICATION_INTAKE_IMPORT_STALE_AFTER_MS,
} from "@/lib/jobs/medication-intake-import";
import { MEDICATION_INTAKE_IMPORT_STALE_AFTER_MS } from "@/lib/jobs/medication-intake-import";
import { MEDICATION_INTAKE_IMPORT_QUEUE } from "@/lib/jobs/medication-intake-import-queue";
const SESSION_OK = {
session: { id: "sess-1", expiresAt: new Date(Date.now() + 3_600_000) },
@@ -74,13 +74,15 @@ const {
vi.mock("@/lib/jobs/boss-instance", () => ({ getGlobalBoss: bossMock }));
vi.mock("@/lib/oura/client", async (importOriginal) => ({
isAcceptedOuraIssuer: (
await importOriginal<typeof import("@/lib/oura/client")>()
).isAcceptedOuraIssuer,
exchangeCode: exchangeMock,
vi.mock("@/lib/oura/client", () => ({
fetchOuraAccountId: vi.fn(async () => "oura-account-1"),
}));
vi.mock("@/lib/oura/oauth", async (importOriginal) => ({
isAcceptedOuraIssuer: (
await importOriginal<typeof import("@/lib/oura/oauth")>()
).isAcceptedOuraIssuer,
exchangeOuraCode: exchangeMock,
}));
vi.mock("@/lib/oura/credentials", () => ({
getOuraClientCredentials: getCredsMock,
}));
+5 -10
View File
@@ -6,11 +6,8 @@ import { verifyCallbackActor } from "@/lib/integrations/oauth-kit/connect-target
import { annotate, getEvent } from "@/lib/logging/context";
import { auditLog } from "@/lib/auth/audit";
import { encrypt } from "@/lib/crypto";
import {
exchangeCode,
fetchOuraAccountId,
isAcceptedOuraIssuer,
} from "@/lib/oura/client";
import { fetchOuraAccountId } from "@/lib/oura/client";
import { exchangeOuraCode, isAcceptedOuraIssuer } from "@/lib/oura/oauth";
import {
ALREADY_LINKED_REASON,
accountLinkedElsewhere,
@@ -27,10 +24,8 @@ import { markReconnected } from "@/lib/integrations/status";
import { OURA_SYNC_QUEUE } from "@/lib/jobs/integration-poll-queues";
import { enqueueWhenBossReady } from "@/lib/jobs/boss-ready";
import { integrationBackfillSourceOptions } from "@/lib/jobs/integration-backfill-admission";
import {
OURA_BACKFILL_QUEUE,
type OuraBackfillPayload,
} from "@/lib/jobs/oura-backfill";
import { type OuraBackfillPayload } from "@/lib/jobs/oura-backfill";
import { OURA_BACKFILL_QUEUE } from "@/lib/jobs/oura-backfill-queue";
import { NextRequest, NextResponse } from "next/server";
/**
@@ -139,7 +134,7 @@ async function completeCallback(
const creds = await getOuraClientCredentials(userId);
if (!creds) return ERR("nocreds");
const tokens = await exchangeCode(code, creds, iss);
const tokens = await exchangeOuraCode(code, creds, iss);
// One Oura account, one record: a guardian signed in to their own Oura
// account must not attach it to the profile they manage.
@@ -76,9 +76,11 @@ vi.mock("@/lib/crypto", () => ({
}));
vi.mock("@/lib/auth/session", () => ({ getSession: getSessionMock }));
vi.mock("@/lib/polar/client", () => ({
exchangeCode: exchangeCodeMock,
registerUser: registerUserMock,
}));
vi.mock("@/lib/polar/oauth", () => ({
polarOAuth: { exchange: exchangeCodeMock },
}));
vi.mock("@/lib/polar/credentials", () => ({
getPolarClientCredentials: getCredsMock,
}));
+3 -2
View File
@@ -9,7 +9,8 @@ import { verifyCallbackActor } from "@/lib/integrations/oauth-kit/connect-target
import { annotate, getEvent } from "@/lib/logging/context";
import { auditLog } from "@/lib/auth/audit";
import { encrypt } from "@/lib/crypto";
import { exchangeCode, registerUser } from "@/lib/polar/client";
import { registerUser } from "@/lib/polar/client";
import { polarOAuth } from "@/lib/polar/oauth";
import { getPolarClientCredentials } from "@/lib/polar/credentials";
import {
oauthStateCookieName,
@@ -115,7 +116,7 @@ async function completeCallback(
const creds = await getPolarClientCredentials(userId);
if (!creds) return ERR("nocreds");
const tokens = await exchangeCode(code, creds);
const tokens = await polarOAuth.exchange(code, creds);
// Guard against an off-spec token body without `x_user_id`: persisting the
// string "undefined" as the member id would break every later data read
// (`/v3/users/undefined/...`) with no clear cause.
@@ -83,7 +83,9 @@ vi.mock("@/lib/logging/context", () => ({
}));
vi.mock("@/lib/auth/audit", () => ({ auditLog: vi.fn(async () => {}) }));
vi.mock("@/lib/crypto", () => ({ encrypt: (s: string) => `enc(${s})` }));
vi.mock("@/lib/strava/client", () => ({ exchangeCode: exchangeCodeMock }));
vi.mock("@/lib/strava/oauth", () => ({
stravaOAuth: { exchange: exchangeCodeMock },
}));
vi.mock("@/lib/strava/credentials", () => ({
getStravaClientCredentials: getCredsMock,
}));
+3 -3
View File
@@ -9,7 +9,7 @@ import { verifyCallbackActor } from "@/lib/integrations/oauth-kit/connect-target
import { annotate, getEvent } from "@/lib/logging/context";
import { auditLog } from "@/lib/auth/audit";
import { encrypt } from "@/lib/crypto";
import { exchangeCode } from "@/lib/strava/client";
import { stravaOAuth } from "@/lib/strava/oauth";
import { getStravaClientCredentials } from "@/lib/strava/credentials";
import {
oauthStateCookieName,
@@ -18,7 +18,7 @@ import {
} from "@/lib/oauth/signed-state";
import { runOAuthCallbackOnce } from "@/lib/oauth/callback-claim";
import { markReconnected } from "@/lib/integrations/status";
import { STRAVA_BACKFILL_QUEUE } from "@/lib/jobs/strava-backfill";
import { STRAVA_BACKFILL_QUEUE } from "@/lib/jobs/strava-backfill-queue";
import { enqueueWhenBossReady } from "@/lib/jobs/boss-ready";
import { NextRequest, NextResponse } from "next/server";
@@ -114,7 +114,7 @@ async function completeCallback(
const creds = await getStravaClientCredentials(userId);
if (!creds) return ERR("nocreds");
const tokens = await exchangeCode(code, creds);
const tokens = await stravaOAuth.exchange(code, creds);
const athleteId =
typeof tokens.athlete?.id === "number" ? String(tokens.athlete.id) : null;
@@ -52,15 +52,19 @@ vi.mock("@/lib/auth/audit", () => ({ auditLog: vi.fn() }));
vi.mock("@/lib/crypto", () => ({ encrypt: (s: string) => `enc:${s}` }));
vi.mock("@/lib/whoop/client", () => ({
WHOOP_OAUTH_SCOPE: "read:recovery",
exchangeCode: vi.fn(async () => ({
access_token: "at",
refresh_token: "rt",
expires_in: 3600,
scope: "read:recovery",
})),
fetchProfile: vi.fn(async () => ({ user_id: 42 })),
}));
vi.mock("@/lib/whoop/oauth", () => ({
WHOOP_OAUTH_SCOPE: "read:recovery",
whoopOAuth: {
exchange: vi.fn(async () => ({
access_token: "at",
refresh_token: "rt",
expires_in: 3600,
scope: "read:recovery",
})),
},
}));
vi.mock("@/lib/whoop/credentials", () => ({
getUserWhoopCredentials: vi.fn(async () => ({
+4 -7
View File
@@ -6,15 +6,12 @@ import { verifyCallbackActor } from "@/lib/integrations/oauth-kit/connect-target
import { annotate, getEvent } from "@/lib/logging/context";
import { auditLog } from "@/lib/auth/audit";
import { encrypt } from "@/lib/crypto";
import {
exchangeCode,
fetchProfile,
WHOOP_OAUTH_SCOPE,
} from "@/lib/whoop/client";
import { fetchProfile } from "@/lib/whoop/client";
import { WHOOP_OAUTH_SCOPE, whoopOAuth } from "@/lib/whoop/oauth";
import { getUserWhoopCredentials } from "@/lib/whoop/credentials";
import { WHOOP_OAUTH_STATE_COOKIE } from "@/lib/whoop/oauth-state";
import { buildReturnSchemeRedirect } from "@/lib/whoop/return-scheme";
import { WHOOP_BACKFILL_QUEUE } from "@/lib/jobs/whoop-backfill";
import { WHOOP_BACKFILL_QUEUE } from "@/lib/jobs/whoop-backfill-queue";
import { enqueueWhenBossReady } from "@/lib/jobs/boss-ready";
import { markReconnected } from "@/lib/integrations/status";
import { Prisma } from "@/generated/prisma/client";
@@ -205,7 +202,7 @@ export const GET = apiHandler(async (request: NextRequest) => {
return outcomeRedirect(returnScheme, "error", "nocreds");
}
const tokens = await exchangeCode(code, creds);
const tokens = await whoopOAuth.exchange(code, creds);
const profile = await fetchProfile(tokens.access_token);
const whoopUserId = String(profile.user_id);
const expiresAt = new Date(Date.now() + tokens.expires_in * 1000);
@@ -81,8 +81,8 @@ vi.mock("@/lib/crypto", () => ({
encrypt: vi.fn((s: string) => `enc(${s})`),
}));
vi.mock("@/lib/withings/client", () => ({
exchangeCode: vi.fn(),
vi.mock("@/lib/withings/oauth", () => ({
withingsOAuth: { exchange: vi.fn() },
WITHINGS_OAUTH_SCOPE: "user.metrics,user.activity",
}));
@@ -157,8 +157,8 @@ describe("withings/callback atomic nonce consumption (M3)", () => {
clientId: "cid",
clientSecret: "csec",
} as never);
const { exchangeCode } = await import("@/lib/withings/client");
vi.mocked(exchangeCode).mockResolvedValue({
const { withingsOAuth } = await import("@/lib/withings/oauth");
vi.mocked(withingsOAuth.exchange).mockResolvedValue({
userid: "wuid",
access_token: "atok",
refresh_token: "rtok",
@@ -221,8 +221,8 @@ describe("withings/callback atomic nonce consumption (M3)", () => {
clientId: "cid",
clientSecret: "csec",
} as never);
const { exchangeCode } = await import("@/lib/withings/client");
vi.mocked(exchangeCode).mockResolvedValue({
const { withingsOAuth } = await import("@/lib/withings/oauth");
vi.mocked(withingsOAuth.exchange).mockResolvedValue({
userid: "wuid",
access_token: "atok",
refresh_token: "rtok",
@@ -300,7 +300,7 @@ describe("withings/callback atomic nonce consumption (M3)", () => {
expiredRow as never,
);
const { exchangeCode } = await import("@/lib/withings/client");
const { withingsOAuth } = await import("@/lib/withings/oauth");
const req = callbackRequest(NONCE, NONCE);
const res = await GET(req);
@@ -310,7 +310,7 @@ describe("withings/callback atomic nonce consumption (M3)", () => {
"withings=error&reason=expired",
);
// Single atomic round-trip consumed the row; no token exchange.
expect(exchangeCode).not.toHaveBeenCalled();
expect(withingsOAuth.exchange).not.toHaveBeenCalled();
expect(annotateMock).toHaveBeenCalledWith({ meta: { reason: "expired" } });
});
@@ -325,7 +325,7 @@ describe("withings/callback atomic nonce consumption (M3)", () => {
crossUserRow as never,
);
const { exchangeCode } = await import("@/lib/withings/client");
const { withingsOAuth } = await import("@/lib/withings/oauth");
const req = callbackRequest(NONCE, NONCE);
const res = await GET(req);
@@ -334,7 +334,7 @@ describe("withings/callback atomic nonce consumption (M3)", () => {
expect(res.headers.get("location")).toContain(
"withings=error&reason=cross_user",
);
expect(exchangeCode).not.toHaveBeenCalled();
expect(withingsOAuth.exchange).not.toHaveBeenCalled();
expect(annotateMock).toHaveBeenCalledWith({
meta: { reason: "cross_user" },
});
+2 -2
View File
@@ -9,7 +9,7 @@ import { verifyCallbackActor } from "@/lib/integrations/oauth-kit/connect-target
import { annotate, getEvent } from "@/lib/logging/context";
import { auditLog } from "@/lib/auth/audit";
import { encrypt } from "@/lib/crypto";
import { exchangeCode, WITHINGS_OAUTH_SCOPE } from "@/lib/withings/client";
import { WITHINGS_OAUTH_SCOPE, withingsOAuth } from "@/lib/withings/oauth";
import { getUserWithingsCredentials } from "@/lib/withings/credentials";
import { WITHINGS_OAUTH_STATE_COOKIE } from "@/lib/withings/oauth-state";
import { setupWebhook } from "@/lib/withings/sync";
@@ -209,7 +209,7 @@ export const GET = apiHandler(async (request: NextRequest) => {
);
}
const tokens = await exchangeCode(code, creds);
const tokens = await withingsOAuth.exchange(code, creds);
// One Withings account, one record. A guardian signed in to their own
// Health Mate account must not attach it to the profile they manage:
+1 -1
View File
@@ -3,7 +3,7 @@ import { apiHandler, requireAuth } from "@/lib/api-handler";
import { annotate, getEvent } from "@/lib/logging/context";
import { apiSuccess } from "@/lib/api-response";
import { loadOneConnectionHealth } from "@/lib/integrations/connection-health-load";
import { hasActivityScope } from "@/lib/withings/client";
import { hasActivityScope } from "@/lib/withings/oauth";
import { obtainAccessToken } from "@/lib/integrations/oauth-kit/token";
import { withingsTokenDescriptor } from "@/lib/withings/oauth-descriptor";
import { isWithingsRefreshReauthFailure } from "@/lib/withings/sync";
+21 -15
View File
@@ -4,13 +4,9 @@ import {
FITBIT_API_BASE,
FITBIT_RETRY_AFTER_WAIT_CAP_S,
FITBIT_FIELD_MAP,
FITBIT_OAUTH_SCOPE,
exchangeCode,
fetchSleepRange,
fetchSpo2Range,
fetchWeightRange,
generatePkcePair,
getAuthorizationUrl,
mapActiveCalories,
mapBodyFat,
mapDistance,
@@ -29,9 +25,11 @@ import {
parseVo2Max,
readActivityList,
readSleepSessions,
refreshAccessToken,
resolveFitbitUserId,
} from "../client";
import { FITBIT_OAUTH_SCOPE } from "@/lib/fitbit/oauth";
import { fitbitOAuth } from "../oauth";
import { generatePkcePair } from "@/lib/integrations/oauth-kit/pkce";
const CREDS = { clientId: "cid", clientSecret: "csecret" };
@@ -89,7 +87,7 @@ describe("generatePkcePair", () => {
describe("getAuthorizationUrl", () => {
it("builds the classic Fitbit authorize URL with PKCE S256", () => {
const url = getAuthorizationUrl("nonce123", CREDS, "chal-xyz");
const url = fitbitOAuth.authorizationUrl("nonce123", CREDS, "chal-xyz");
expect(url).toContain("www.fitbit.com/oauth2/authorize");
expect(url).toContain("response_type=code");
expect(url).toContain("client_id=cid");
@@ -118,7 +116,7 @@ describe("getAuthorizationUrl", () => {
});
it("carries the configured app origin in the redirect_uri", () => {
const url = getAuthorizationUrl("nonce123", CREDS, "c");
const url = fitbitOAuth.authorizationUrl("nonce123", CREDS, "c");
const redirect = new URL(url).searchParams.get("redirect_uri");
expect(redirect).toBe("https://app.example.test/api/fitbit/callback");
});
@@ -133,12 +131,14 @@ describe("redirect_uri allowlist", () => {
it("rejects a non-https app origin", () => {
process.env.NEXT_PUBLIC_APP_URL = "http://app.example.test";
expect(() => getAuthorizationUrl("n", CREDS, "c")).toThrow(/must be https/);
expect(() => fitbitOAuth.authorizationUrl("n", CREDS, "c")).toThrow(
/must be https/,
);
});
it("allows http on localhost (dev)", () => {
process.env.NEXT_PUBLIC_APP_URL = "http://localhost:3000";
const url = getAuthorizationUrl("n", CREDS, "c");
const url = fitbitOAuth.authorizationUrl("n", CREDS, "c");
expect(new URL(url).searchParams.get("redirect_uri")).toBe(
"http://localhost:3000/api/fitbit/callback",
);
@@ -146,7 +146,7 @@ describe("redirect_uri allowlist", () => {
it("rejects an explicit redirect that targets the wrong path", () => {
process.env.FITBIT_REDIRECT_URI = "https://app.example.test/evil";
expect(() => getAuthorizationUrl("n", CREDS, "c")).toThrow(
expect(() => fitbitOAuth.authorizationUrl("n", CREDS, "c")).toThrow(
/must target \/api\/fitbit\/callback/,
);
});
@@ -155,7 +155,7 @@ describe("redirect_uri allowlist", () => {
process.env.NEXT_PUBLIC_APP_URL = "https://app.example.test";
process.env.FITBIT_REDIRECT_URI =
"https://attacker.example.test/api/fitbit/callback";
expect(() => getAuthorizationUrl("n", CREDS, "c")).toThrow(
expect(() => fitbitOAuth.authorizationUrl("n", CREDS, "c")).toThrow(
/does not match/,
);
});
@@ -175,7 +175,9 @@ describe("token exchange + refresh", () => {
},
},
]);
const tok = await exchangeCode("code", "verifier-123", CREDS);
const tok = await fitbitOAuth.exchange("code", CREDS, {
codeVerifier: "verifier-123",
});
expect(tok.access_token).toBe("at");
expect(tok.refresh_token).toBe("rt");
expect(tok.expires_in).toBe(28800);
@@ -202,7 +204,7 @@ describe("token exchange + refresh", () => {
},
},
]);
const tok = await refreshAccessToken("rt1", CREDS);
const tok = await fitbitOAuth.refresh("rt1", CREDS);
expect(tok.access_token).toBe("at2");
// Classic Fitbit rotates: a fresh refresh token is returned every time.
expect(tok.refresh_token).toBe("rt2");
@@ -220,7 +222,9 @@ describe("token exchange + refresh", () => {
installFetchMock([
{ status: 401, body: { errors: [{ errorType: "invalid_grant" }] } },
]);
await expect(exchangeCode("bad", "v", CREDS)).rejects.toMatchObject({
await expect(
fitbitOAuth.exchange("bad", CREDS, { codeVerifier: "v" }),
).rejects.toMatchObject({
name: "FitbitApiError",
classification: "reauth_required",
});
@@ -231,7 +235,9 @@ describe("token exchange + refresh", () => {
// TypeError. It must surface as a classified transient integration error.
it("classifies a 2xx with an empty body instead of casting null", async () => {
installFetchMock([{ status: 200, body: null }]);
await expect(exchangeCode("code", "v", CREDS)).rejects.toMatchObject({
await expect(
fitbitOAuth.exchange("code", CREDS, { codeVerifier: "v" }),
).rejects.toMatchObject({
name: "FitbitApiError",
classification: "transient",
reason: "empty_token_body",
+1 -1
View File
@@ -33,7 +33,7 @@ vi.mock("@/lib/rollups/measurement-rollups", () => ({
recomputeBucketsForMeasurement: vi.fn(async () => {}),
recomputeUserRollups: vi.fn(async () => ({ rowsUpserted: 0, durationMs: 0 })),
}));
vi.mock("@/lib/insights/comprehensive-generate", () => ({
vi.mock("@/lib/insights/status-invalidation", () => ({
invalidateStatusInsightsForTypes: vi.fn(async () => {}),
}));
@@ -3,9 +3,9 @@ import { join } from "node:path";
import { describe, expect, it } from "vitest";
import { GOOGLE_HEALTH_CORE_SCOPES } from "@/lib/google-health/client";
import { GOOGLE_HEALTH_CORE_SCOPES } from "@/lib/google-health/oauth";
import { FITBIT_OAUTH_SCOPE } from "../client";
import { FITBIT_OAUTH_SCOPE } from "@/lib/fitbit/oauth";
/**
* Guard against the class of bug reported in #396: the setup docs drifting away
@@ -60,7 +60,7 @@ vi.mock("@/lib/rollups/measurement-rollups", () => ({
recomputeBucketsForMeasurement: vi.fn(async () => {}),
recomputeUserRollups: vi.fn(async () => {}),
}));
vi.mock("@/lib/insights/comprehensive-generate", () => ({
vi.mock("@/lib/insights/status-invalidation", () => ({
invalidateStatusInsightsForTypes: vi.fn(async () => {}),
}));
vi.mock("@/lib/logging/context", () => ({
@@ -70,7 +70,9 @@ vi.mock("@/lib/logging/context", () => ({
vi.mock("../credentials", () => ({
getUserFitbitCredentials: getUserFitbitCredentialsMock,
}));
vi.mock("../client", () => ({ refreshAccessToken: refreshAccessTokenMock }));
vi.mock("../oauth", () => ({
fitbitOAuth: { refresh: refreshAccessTokenMock },
}));
vi.mock("../sync-metrics", () => ({ syncUserMetrics: resourceFake }));
vi.mock("../sync-activity", () => ({ syncUserActivity: resourceFake }));
@@ -35,7 +35,7 @@ vi.mock("@/lib/rollups/measurement-rollups", () => ({
recomputeBucketsForMeasurement: vi.fn(async () => {}),
recomputeUserRollups: vi.fn(async () => {}),
}));
vi.mock("@/lib/insights/comprehensive-generate", () => ({
vi.mock("@/lib/insights/status-invalidation", () => ({
invalidateStatusInsightsForTypes: vi.fn(async () => {}),
}));
vi.mock("@/lib/integrations/oauth-refresh", () => ({
@@ -44,7 +44,13 @@ vi.mock("@/lib/integrations/oauth-refresh", () => ({
vi.mock("../credentials", () => ({
getUserFitbitCredentials: vi.fn(async () => null),
}));
vi.mock("../client", () => ({ refreshAccessToken: vi.fn() }));
vi.mock("../oauth", async (importOriginal) => {
const actual = await importOriginal<typeof import("../oauth")>();
return {
...actual,
fitbitOAuth: { ...actual.fitbitOAuth, refresh: vi.fn() },
};
});
import { eventStorage } from "@/lib/logging/context";
import { WideEventBuilder } from "@/lib/logging/event-builder";
@@ -60,7 +60,7 @@ vi.mock("@/lib/rollups/measurement-rollups", () => ({
recomputeBucketsForMeasurement: vi.fn(async () => {}),
recomputeUserRollups: vi.fn(async () => {}),
}));
vi.mock("@/lib/insights/comprehensive-generate", () => ({
vi.mock("@/lib/insights/status-invalidation", () => ({
invalidateStatusInsightsForTypes: vi.fn(async () => {}),
}));
vi.mock("@/lib/integrations/oauth-refresh", () => ({
@@ -48,7 +48,7 @@ vi.mock("@/lib/rollups/measurement-rollups", () => ({
recomputeBucketsForMeasurement: vi.fn(async () => {}),
recomputeUserRollups: vi.fn(async () => {}),
}));
vi.mock("@/lib/insights/comprehensive-generate", () => ({
vi.mock("@/lib/insights/status-invalidation", () => ({
invalidateStatusInsightsForTypes: vi.fn(async () => {}),
}));
vi.mock("@/lib/integrations/oauth-refresh", () => ({
@@ -57,7 +57,13 @@ vi.mock("@/lib/integrations/oauth-refresh", () => ({
vi.mock("../credentials", () => ({
getUserFitbitCredentials: vi.fn(async () => null),
}));
vi.mock("../client", () => ({ refreshAccessToken: vi.fn() }));
vi.mock("../oauth", async (importOriginal) => {
const actual = await importOriginal<typeof import("../oauth")>();
return {
...actual,
fitbitOAuth: { ...actual.fitbitOAuth, refresh: vi.fn() },
};
});
import { upsertFitbitMeasurements } from "../sync-core";
@@ -73,7 +73,7 @@ vi.mock("@/lib/rollups/measurement-rollups", () => ({
recomputeBucketsForMeasurement: vi.fn(async () => {}),
recomputeUserRollups: (...a: unknown[]) => recomputeUserRollups(...a),
}));
vi.mock("@/lib/insights/comprehensive-generate", () => ({
vi.mock("@/lib/insights/status-invalidation", () => ({
invalidateStatusInsightsForTypes: vi.fn(async () => {}),
}));
+8 -4
View File
@@ -43,10 +43,11 @@ vi.mock("@/lib/rollups/measurement-rollups", () => ({
collapseToTypeDayKeys: (rows: Array<{ type: string; measuredAt: Date }>) =>
rows,
recomputeBucketsForMeasurement: vi.fn(async () => {}),
recomputeBucketsForKeys: vi.fn(async () => {}),
recomputeUserRollups: vi.fn(async () => ({ rowsUpserted: 0, durationMs: 0 })),
}));
vi.mock("@/lib/insights/comprehensive-generate", () => ({
vi.mock("@/lib/insights/status-invalidation", () => ({
invalidateStatusInsightsForTypes: vi.fn(async () => {}),
}));
@@ -55,11 +56,14 @@ vi.mock("@/lib/crypto", () => ({
decrypt: (s: string) => s.replace(/^enc\(|\)$/g, ""),
}));
vi.mock("../client", async (orig) => {
const actual = await orig<typeof import("../client")>();
vi.mock("../oauth", async (orig) => {
const actual = await orig<typeof import("../oauth")>();
return {
...actual,
refreshAccessToken: (...a: unknown[]) => refreshAccessTokenMock(...a),
fitbitOAuth: {
...actual.fitbitOAuth,
refresh: (...a: unknown[]) => refreshAccessTokenMock(...a),
},
};
});
-267
View File
@@ -36,19 +36,15 @@
* self-serve Google path exists, the classic API is the only viable transport
* for self-hosters; the card keeps its experimental badge.
*/
import { createHash, randomBytes } from "node:crypto";
import { getEvent } from "@/lib/logging/context";
import { safeFetch } from "@/lib/safe-fetch";
import { canonicalDailyTimestamp } from "@/lib/measurements/consolidation-tz";
import { zonedWallClockToUtc } from "@/lib/tz/wall-clock";
import { FitbitApiError, classifyFitbitResponse } from "./response-classifier";
import { envValue } from "@/lib/env";
import { importValue } from "@/lib/integrations/import-value";
/** Classic Fitbit Web API base. */
export const FITBIT_API_BASE = "https://api.fitbit.com";
const FITBIT_OAUTH_AUTH_URL = "https://www.fitbit.com/oauth2/authorize";
const FITBIT_OAUTH_TOKEN_URL = "https://api.fitbit.com/oauth2/token";
/**
* Fitbit returns body weight / distance in metric (kg / km) only when an
@@ -58,269 +54,6 @@ const FITBIT_OAUTH_TOKEN_URL = "https://api.fitbit.com/oauth2/token";
*/
const FITBIT_METRIC_LOCALE = "en_GB";
export interface FitbitCredentials {
clientId: string;
clientSecret: string;
}
/**
* Resolve the OAuth `redirect_uri`, then assert it against an allowlist
* (defence-in-depth; carried over from the prior transport).
*
* The value is operator-controlled config (`FITBIT_REDIRECT_URI`, else derived
* from `NEXT_PUBLIC_APP_URL`), not user input, and Fitbit's registered-redirect
* check is the real backstop. But a misconfigured or `Host`-coerced
* `NEXT_PUBLIC_APP_URL` (a mis-deployed reverse proxy reflecting a forwarded
* Host) would otherwise send the authorization code's landing URL off-origin.
* Pin the target so a malformed origin fails fast at the handshake:
* - must be an absolute, parseable URL,
* - must be https (the one exception is a localhost/loopback dev host),
* - must land on the fixed `/api/fitbit/callback` path,
* - when derived from `NEXT_PUBLIC_APP_URL`, must stay same-origin with it.
*/
export function getFitbitRedirectUri(): string {
// An empty or blank value counts as unset: the compose whitelist
// materialises an unset var as an empty string, which must fall through to
// the derived URI rather than read as "not configured".
const explicit = envValue("FITBIT_REDIRECT_URI");
const appUrl = envValue("NEXT_PUBLIC_APP_URL");
const raw =
explicit ?? (appUrl ? `${appUrl}/api/fitbit/callback` : undefined);
if (!raw) {
throw new Error(
"Fitbit redirect_uri is not configured — set FITBIT_REDIRECT_URI or NEXT_PUBLIC_APP_URL",
);
}
let parsed: URL;
try {
parsed = new URL(raw);
} catch {
throw new Error(`Fitbit redirect_uri is not an absolute URL: ${raw}`);
}
const isLoopback =
parsed.hostname === "localhost" ||
parsed.hostname === "127.0.0.1" ||
parsed.hostname === "[::1]" ||
parsed.hostname === "::1";
if (
parsed.protocol !== "https:" &&
!(parsed.protocol === "http:" && isLoopback)
) {
throw new Error(
`Fitbit redirect_uri must be https (or http on localhost): ${parsed.origin}`,
);
}
if (parsed.pathname !== "/api/fitbit/callback") {
throw new Error(
`Fitbit redirect_uri must target /api/fitbit/callback, got ${parsed.pathname}`,
);
}
// When an explicit FITBIT_REDIRECT_URI is set alongside NEXT_PUBLIC_APP_URL,
// require them to share an origin so the pinned value can't drift to an
// unexpected host relative to the app's own base URL.
if (explicit && appUrl) {
let appOrigin: string;
try {
appOrigin = new URL(appUrl).origin;
} catch {
throw new Error(`NEXT_PUBLIC_APP_URL is not an absolute URL: ${appUrl}`);
}
if (parsed.origin !== appOrigin) {
throw new Error(
`Fitbit redirect_uri origin ${parsed.origin} does not match NEXT_PUBLIC_APP_URL origin ${appOrigin}`,
);
}
}
return parsed.toString();
}
/**
* Classic Fitbit OAuth scopes HealthLog requests (space-separated on the wire).
* Each is independently self-serve grantable at the consent screen and the
* `temperature` set is omitted on purpose (see the skin-temperature note in
* `mapping.md` — the classic skin-temp reading is a baseline DELTA, not an
* absolute reading, so it has no honest canonical slot here). The launch bundle
* covers every metric the v1.20.0 mappers extract.
*/
export const FITBIT_OAUTH_SCOPE = [
"activity",
"cardio_fitness",
"heartrate",
"oxygen_saturation",
"profile",
"respiratory_rate",
"sleep",
"weight",
].join(" ");
// ─── PKCE ──────────────────────────────────────────────────────
//
// Fitbit recommends PKCE (S256). The verifier is a high-entropy random string;
// the challenge is BASE64URL(SHA256(verifier)). The verifier is stashed on the
// OAuth-state row at connect and presented on the token exchange at callback.
export interface FitbitPkcePair {
verifier: string;
challenge: string;
}
/**
* Mint a PKCE verifier + S256 challenge. 64 random bytes → 86 base64url chars,
* comfortably inside Fitbit's 43–128 char verifier range and well past the
* 256-bit entropy floor.
*/
export function generatePkcePair(): FitbitPkcePair {
const verifier = randomBytes(64).toString("base64url");
const challenge = createHash("sha256").update(verifier).digest("base64url");
return { verifier, challenge };
}
/**
* Build the Fitbit authorization URL (a browser redirect, not a fetch). `state`
* is the opaque CSRF nonce minted by `oauth-state.ts`; `codeChallenge` is the
* S256 PKCE challenge whose verifier the callback presents on token exchange.
*/
export function getAuthorizationUrl(
state: string,
creds: FitbitCredentials,
codeChallenge: string,
): string {
const params = new URLSearchParams({
response_type: "code",
client_id: creds.clientId,
redirect_uri: getFitbitRedirectUri(),
scope: FITBIT_OAUTH_SCOPE,
code_challenge: codeChallenge,
code_challenge_method: "S256",
state,
});
return `${FITBIT_OAUTH_AUTH_URL}?${params}`;
}
export interface FitbitTokenResponse {
access_token: string;
/**
* Classic Fitbit refresh tokens ROTATE (one-time use) — every token response
* (initial exchange AND every refresh) carries a fresh `refresh_token` that
* MUST replace the stored one. Typed optional only to guard a malformed
* response defensively; the callers treat an absent value as a hard error /
* keep-existing fallback.
*/
refresh_token?: string;
expires_in: number;
scope?: string;
token_type?: string;
user_id?: string;
}
/** Basic-auth header carrying the confidential client credentials. */
function basicAuthHeader(creds: FitbitCredentials): string {
const raw = `${creds.clientId}:${creds.clientSecret}`;
return `Basic ${Buffer.from(raw, "utf8").toString("base64")}`;
}
async function postToken(
params: URLSearchParams,
creds: FitbitCredentials,
verb: string,
): Promise<FitbitTokenResponse> {
const start = performance.now();
const res = await safeFetch(FITBIT_OAUTH_TOKEN_URL, {
method: "POST",
headers: {
"Content-Type": "application/x-www-form-urlencoded",
Authorization: basicAuthHeader(creds),
},
body: params.toString(),
});
const json = await res.json().catch(() => null);
const verdict = classifyFitbitResponse(res.status);
getEvent()?.addExternalCall({
service: "fitbit",
method: verb,
duration_ms: Math.round(performance.now() - start),
status: res.status,
error: verdict.classification === "success" ? undefined : verdict.reason,
});
if (verdict.classification !== "success") {
throw new FitbitApiError({
verb,
classification: verdict.classification,
httpStatus: verdict.httpStatus,
reason: verdict.reason,
upstreamError:
typeof json?.errors?.[0]?.errorType === "string"
? json.errors[0].errorType
: typeof json?.error === "string"
? json.error
: undefined,
});
}
// A 2xx with an empty or non-JSON body (captive portal, gateway 200, 204)
// leaves `json` null. Casting it straight to the token type handed callers a
// null they then dereferenced, surfacing an unclassified TypeError instead of
// a handled integration error. Narrow explicitly and classify as transient.
if (json === null || typeof json !== "object") {
throw new FitbitApiError({
verb,
classification: "transient",
httpStatus: res.status,
reason: "empty_token_body",
});
}
return json as FitbitTokenResponse;
}
/**
* Exchange an authorization code for the initial token pair, presenting the PKCE
* verifier. `redirect_uri` must exactly match the one sent to the authorize
* endpoint.
*/
export async function exchangeCode(
code: string,
codeVerifier: string,
creds: FitbitCredentials,
): Promise<FitbitTokenResponse> {
return postToken(
new URLSearchParams({
grant_type: "authorization_code",
code,
client_id: creds.clientId,
redirect_uri: getFitbitRedirectUri(),
code_verifier: codeVerifier,
}),
creds,
"exchangeCode",
);
}
/**
* Refresh an expired access token. Classic Fitbit ROTATES the refresh token:
* the response carries a fresh `access_token` AND a fresh `refresh_token`; the
* caller MUST persist the new refresh token (the old one is now invalid). The
* grant's scope is preserved, so no `scope` param is re-sent.
*/
export async function refreshAccessToken(
refreshToken: string,
creds: FitbitCredentials,
): Promise<FitbitTokenResponse> {
return postToken(
new URLSearchParams({
grant_type: "refresh_token",
refresh_token: refreshToken,
}),
creds,
"refreshAccessToken",
);
}
/**
* The Fitbit profile carries the stable external user id (`encodedId`) used as
* the connection's `fitbitUserId`. A single GET, not a paginated collection.
+1 -1
View File
@@ -10,7 +10,7 @@
*/
import { prisma } from "@/lib/db";
import { decrypt } from "@/lib/crypto";
import type { FitbitCredentials } from "./client";
import type { FitbitCredentials } from "./oauth";
/**
* Fetch and decrypt the user's Fitbit API credentials.
@@ -42,7 +42,7 @@ vi.mock("@/lib/logging/context", () => ({
import {
enqueueBootTimeFitbitBackfill,
runFitbitBackfillForUser,
} from "../fitbit-backfill";
} from "@/lib/fitbit/jobs/backfill";
import { isFitbitRetired } from "@/lib/fitbit/retirement";
beforeEach(() => {
@@ -0,0 +1,117 @@
/**
* pg-boss queue registration for the Fitbit sync layer.
*
* Reads the worker registry instead of booting the worker: every Fitbit queue
* is declared in the integration-sync group with its cron and handler, the
* full-history queues forward to the shared admission lane, and the boot
* discovery offers the backfill and the sleep repair again. This is the fast
* guard for the dead-queue class (a queue declared but never registered,
* which silently never drains).
*
* Fitbit is poll-only at launch (no webhook — Pub/Sub deferred), so a single
* hourly `fitbit-sync` queue drives the per-user `syncUserFitbit` driver across
* every connection, alongside the backfill + the daily OAuth-state sweep.
*/
import { readFileSync } from "node:fs";
import { join } from "node:path";
import { describe, expect, it } from "vitest";
import {
FITBIT_BACKFILL_QUEUE,
FITBIT_OAUTH_STATE_CLEANUP_CRON,
FITBIT_OAUTH_STATE_CLEANUP_QUEUE,
FITBIT_SLEEP_REPAIR_QUEUE,
FITBIT_SYNC_CRON,
FITBIT_SYNC_QUEUE,
} from "@/lib/fitbit/jobs/queues";
import { INTEGRATION_BACKFILL_ADMISSION_QUEUE } from "@/lib/jobs/integration-backfill-admission";
import {
handleFitbitOAuthStateCleanup,
handleFitbitSync,
} from "@/lib/fitbit/jobs/sync-handlers";
import { cronIsTheRetry } from "@/lib/jobs/reminder/registrar-shared";
import {
discoveryOf,
groupOf,
jobOf,
loadedHandler,
recordingBoss,
} from "@/lib/jobs/registry/__tests__/registry-helpers";
const handlerSource = readFileSync(
join(__dirname, "..", "sync-handlers.ts"),
"utf8",
);
const FITBIT_QUEUES = [
FITBIT_SYNC_QUEUE,
FITBIT_BACKFILL_QUEUE,
FITBIT_SLEEP_REPAIR_QUEUE,
FITBIT_OAUTH_STATE_CLEANUP_QUEUE,
] as const;
/** The admission sends the source queue's handler makes for one job. */
async function forwardOneJob(queue: string) {
const { boss, sends } = recordingBoss();
const handler = await jobOf(queue).load({ boss });
await handler([
{
id: "j1",
name: queue,
data: { userId: "u1" },
expireInSeconds: 60,
heartbeatSeconds: null,
retryCount: 0,
signal: new AbortController().signal,
},
]);
return sends;
}
describe("Fitbit sync queues", () => {
it("declares the Fitbit sync + oauth-state-cleanup queue constants", () => {
expect(FITBIT_SYNC_QUEUE).toBe("fitbit-sync");
expect(FITBIT_OAUTH_STATE_CLEANUP_QUEUE).toBe("fitbit-oauth-state-cleanup");
});
it("declares EVERY Fitbit queue in the integration-sync group", () => {
for (const q of FITBIT_QUEUES) {
expect(groupOf(q).name).toBe("integration_sync");
}
});
it("schedules the hourly poll cron + the daily oauth-state sweep", () => {
expect(jobOf(FITBIT_SYNC_QUEUE).cron).toBe(FITBIT_SYNC_CRON);
expect(jobOf(FITBIT_SYNC_QUEUE).scheduleOptions).toBeUndefined();
const sweep = jobOf(FITBIT_OAUTH_STATE_CLEANUP_QUEUE);
expect(sweep.cron).toBe(FITBIT_OAUTH_STATE_CLEANUP_CRON);
expect(sweep.scheduleOptions).toBe(cronIsTheRetry);
});
it("binds a handler for every Fitbit queue", async () => {
expect(await loadedHandler(FITBIT_SYNC_QUEUE)).toBe(handleFitbitSync);
expect(await loadedHandler(FITBIT_OAUTH_STATE_CLEANUP_QUEUE)).toBe(
handleFitbitOAuthStateCleanup,
);
for (const [queue, kind] of [
[FITBIT_BACKFILL_QUEUE, "fitbit-backfill"],
[FITBIT_SLEEP_REPAIR_QUEUE, "fitbit-sleep-repair"],
] as const) {
const sends = await forwardOneJob(queue);
expect(sends).toHaveLength(1);
expect(sends[0]!.queue).toBe(INTEGRATION_BACKFILL_ADMISSION_QUEUE);
expect(sends[0]!.data).toMatchObject({ kind, data: { userId: "u1" } });
}
});
it("wires the self-converging backfill and the one-shot sleep-repair boot discovery", () => {
const passes = discoveryOf("integration_sync");
expect(passes).toContain("fitbit-backfill");
expect(passes).toContain("fitbit-sleep-repair");
});
it("imports the Fitbit sync driver into the handler module", () => {
expect(handlerSource).toMatch(/from\s*["']@\/lib\/fitbit\/sync["']/);
});
});
@@ -50,7 +50,7 @@ vi.mock("@/lib/rollups/measurement-rollups", () => ({
recomputeBucketsForMeasurement: vi.fn(async () => {}),
recomputeUserRollups: vi.fn(async () => {}),
}));
vi.mock("@/lib/insights/comprehensive-generate", () => ({
vi.mock("@/lib/insights/status-invalidation", () => ({
invalidateStatusInsightsForTypes: vi.fn(async () => {}),
}));
vi.mock("@/lib/integrations/oauth-refresh", () => ({
@@ -59,7 +59,13 @@ vi.mock("@/lib/integrations/oauth-refresh", () => ({
vi.mock("@/lib/fitbit/credentials", () => ({
getUserFitbitCredentials: vi.fn(async () => null),
}));
vi.mock("@/lib/fitbit/client", () => ({ refreshAccessToken: vi.fn() }));
vi.mock("@/lib/fitbit/oauth", async (importOriginal) => {
const actual = await importOriginal<typeof import("@/lib/fitbit/oauth")>();
return {
...actual,
fitbitOAuth: { ...actual.fitbitOAuth, refresh: vi.fn() },
};
});
import {
FITBIT_BACKFILL_DAYS,
@@ -70,7 +76,7 @@ import {
import {
enqueueBootTimeFitbitSleepRepair,
runFitbitSleepRepairForUser,
} from "../fitbit-sleep-repair";
} from "@/lib/fitbit/jobs/sleep-repair";
import { isFitbitRetired } from "@/lib/fitbit/retirement";
beforeEach(() => {
@@ -21,7 +21,7 @@ vi.mock("@/lib/jobs/reminder-satisfy", () => ({
enqueueReminderSatisfy: vi.fn(async () => {}),
}));
import { handleFitbitSync } from "../reminder/fitbit-sync";
import { handleFitbitSync } from "@/lib/fitbit/jobs/sync-handlers";
import { isFitbitRetired } from "@/lib/fitbit/retirement";
describe("handleFitbitSync", () => {
@@ -21,15 +21,7 @@ import {
} from "@/lib/integrations/page-walk";
import { syncUserFitbit } from "@/lib/fitbit/sync";
import { isFitbitRetired } from "@/lib/fitbit/retirement";
export const FITBIT_BACKFILL_QUEUE = "fitbit-backfill";
/**
* Serial concurrency — a backfill walks years of history for one account and is
* rate-bounded by Google's per-app quota; concurrency-1 keeps it from crowding
* the request pool, matching `WHOOP_BACKFILL_CONCURRENCY`.
*/
export const FITBIT_BACKFILL_CONCURRENCY = 1;
import { FITBIT_BACKFILL_QUEUE } from "./queues";
export interface FitbitBackfillPayload {
userId: string;
+94
View File
@@ -0,0 +1,94 @@
/**
* Every queue the Fitbit integration runs, as one block of the worker
* registry.
*
* Google turns the classic Fitbit Web API off on 2026-10-30
* (`lib/fitbit/retirement.ts`). Until the integration is removed it keeps
* syncing; removing it then is this folder plus the one line in
* `jobs/registry/integration-sync.ts` that spreads these definitions in.
*/
import { bootStaggerSecondsFor } from "@/lib/jobs/integration-backfill-admission";
import { cronIsTheRetry } from "@/lib/jobs/reminder/registrar-shared";
import {
defineJob,
type BootDiscovery,
type JobDefinition,
} from "@/lib/jobs/registry/define-job";
import {
FITBIT_BACKFILL_CONCURRENCY,
FITBIT_BACKFILL_QUEUE,
FITBIT_OAUTH_STATE_CLEANUP_CRON,
FITBIT_OAUTH_STATE_CLEANUP_QUEUE,
FITBIT_SLEEP_REPAIR_CONCURRENCY,
FITBIT_SLEEP_REPAIR_QUEUE,
FITBIT_SYNC_CRON,
FITBIT_SYNC_QUEUE,
} from "./queues";
export const FITBIT_JOBS: readonly JobDefinition[] = [
// The cron walks every connection; serial so a backfill-heavy tick never
// crowds the request pool.
defineJob({
queue: FITBIT_SYNC_QUEUE,
cron: FITBIT_SYNC_CRON,
work: { localConcurrency: 1 },
load: async () => (await import("./sync-handlers")).handleFitbitSync,
}),
// The source queue keeps the per-connection singleton and forwards
// admitted work to the shared drain.
defineJob({
queue: FITBIT_BACKFILL_QUEUE,
policy: {
policy: "exclusive",
reason:
"Per-connection full-history backfill; boot-discovery driven and self-converging.",
},
work: { localConcurrency: FITBIT_BACKFILL_CONCURRENCY },
load: async ({ boss }) =>
(
await import("@/lib/jobs/integration-backfill-forward")
).forwardToBackfillAdmission(boss, "fitbit-backfill"),
}),
// One-shot sleep duplicate repair: a bounded sleep re-read per connection,
// forwarded to the shared drain.
defineJob({
queue: FITBIT_SLEEP_REPAIR_QUEUE,
policy: {
policy: "exclusive",
reason:
"Per-connection one-shot sleep re-read; discovery drops the connection once it is marked repaired.",
},
work: { localConcurrency: FITBIT_SLEEP_REPAIR_CONCURRENCY },
load: async ({ boss }) =>
(
await import("@/lib/jobs/integration-backfill-forward")
).forwardToBackfillAdmission(boss, "fitbit-sleep-repair"),
}),
defineJob({
queue: FITBIT_OAUTH_STATE_CLEANUP_QUEUE,
cron: FITBIT_OAUTH_STATE_CLEANUP_CRON,
scheduleOptions: cronIsTheRetry,
work: { localConcurrency: 1 },
load: async () =>
(await import("./sync-handlers")).handleFitbitOAuthStateCleanup,
}),
];
/** The Fitbit backfill and sleep repair, offered again at every boot. */
export const FITBIT_BOOT_DISCOVERY: readonly BootDiscovery[] = [
{
name: "fitbit-backfill",
run: async () =>
(await import("./backfill")).enqueueBootTimeFitbitBackfill(
bootStaggerSecondsFor("fitbit-backfill"),
),
},
{
name: "fitbit-sleep-repair",
run: async () =>
(await import("./sleep-repair")).enqueueBootTimeFitbitSleepRepair(
bootStaggerSecondsFor("fitbit-sleep-repair"),
),
},
];
+30
View File
@@ -0,0 +1,30 @@
/**
* The queue facts of the Fitbit jobs: names and concurrency. They live apart
* from the handlers so the worker registry and every sender can read them
* without loading the Fitbit sync.
*/
export const FITBIT_SLEEP_REPAIR_QUEUE = "fitbit-sleep-repair";
/**
* Serial concurrency — a repair walks a year of sleep history for one account
* in 30-day requests against the tight per-user budget; concurrency-1 keeps it
* from crowding the request pool, matching `FITBIT_BACKFILL_CONCURRENCY`.
*/
export const FITBIT_SLEEP_REPAIR_CONCURRENCY = 1;
export const FITBIT_BACKFILL_QUEUE = "fitbit-backfill";
/**
* Serial concurrency — a backfill walks years of history for one account and is
* rate-bounded by Google's per-app quota; concurrency-1 keeps it from crowding
* the request pool, matching `WHOOP_BACKFILL_CONCURRENCY`.
*/
export const FITBIT_BACKFILL_CONCURRENCY = 1;
/** Hourly Fitbit poll (no webhook). */
export const FITBIT_SYNC_QUEUE = "fitbit-sync";
export const FITBIT_SYNC_CRON = "8 * * * *";
/** Daily sweep of the Fitbit OAuth state ledger. */
export const FITBIT_OAUTH_STATE_CLEANUP_QUEUE = "fitbit-oauth-state-cleanup";
export const FITBIT_OAUTH_STATE_CLEANUP_CRON = "24 3 * * *";
@@ -46,15 +46,7 @@ import {
} from "@/lib/fitbit/sync-core";
import { syncUserSleep } from "@/lib/fitbit/sync-sleep";
import { isFitbitRetired } from "@/lib/fitbit/retirement";
export const FITBIT_SLEEP_REPAIR_QUEUE = "fitbit-sleep-repair";
/**
* Serial concurrency — a repair walks a year of sleep history for one account
* in 30-day requests against the tight per-user budget; concurrency-1 keeps it
* from crowding the request pool, matching `FITBIT_BACKFILL_CONCURRENCY`.
*/
export const FITBIT_SLEEP_REPAIR_CONCURRENCY = 1;
import { FITBIT_SLEEP_REPAIR_QUEUE } from "./queues";
export interface FitbitSleepRepairPayload {
userId: string;
@@ -11,8 +11,8 @@ import { jobDone, jobFailed, type JobOutcome } from "@/lib/jobs/job-outcome";
import { withBackgroundEvent } from "@/lib/logging/background";
import { runFitbitPollCohort } from "@/lib/fitbit/sync";
import { enqueueReminderSatisfy } from "@/lib/jobs/reminder-satisfy";
import { cleanupExpiredFitbitOAuthStates } from "@/lib/jobs/fitbit-oauth-state-cleanup";
import { getWorkerPrisma } from "./shared";
import { cleanupExpiredFitbitOAuthStates } from "./oauth-state-cleanup";
import { getWorkerPrisma } from "@/lib/jobs/reminder/shared";
import { isFitbitRetired } from "@/lib/fitbit/retirement";
/**
+78
View File
@@ -0,0 +1,78 @@
/**
* The Fitbit Web API OAuth client: consent URL, code exchange and refresh,
* over the shared factory (`integrations/oauth-kit/client.ts`).
*
* S256 PKCE with the confidential client's secret by HTTP Basic beside it.
* Refresh tokens rotate (one-time use): every answer carries a new refresh
* token that replaces the stored one. A refusal names itself in
* `errors[0].errorType`, or in an RFC 6749 `error`.
*/
import {
defineOAuthClient,
oauthErrorCode,
type OAuthClientCredentials,
} from "@/lib/integrations/oauth-kit/client";
import { checkedRedirectUri } from "@/lib/integrations/oauth-kit/redirect-uri";
import { FitbitApiError } from "./response-classifier";
export type FitbitCredentials = OAuthClientCredentials;
/**
* The classic scopes HealthLog requests, space-separated on the wire. Each is
* self-serve grantable; `temperature` is left out on purpose (the classic
* skin-temperature reading is a delta, not an absolute value).
*/
export const FITBIT_OAUTH_SCOPE = [
"activity",
"cardio_fitness",
"heartrate",
"oxygen_saturation",
"profile",
"respiratory_rate",
"sleep",
"weight",
].join(" ");
export interface FitbitTokenResponse {
access_token: string;
/**
* Rotates on every answer and must replace the stored one. Optional only to
* guard a malformed answer.
*/
refresh_token?: string;
expires_in: number;
scope?: string;
token_type?: string;
user_id?: string;
}
function fitbitErrorType(body: unknown): string | undefined {
if (body !== null && typeof body === "object") {
const first = (body as { errors?: Array<{ errorType?: unknown }> })
.errors?.[0];
if (typeof first?.errorType === "string") return first.errorType;
}
return oauthErrorCode(body);
}
export const fitbitOAuth = defineOAuthClient<FitbitTokenResponse>({
service: "fitbit",
authorizeUrl: "https://www.fitbit.com/oauth2/authorize",
tokenUrl: "https://api.fitbit.com/oauth2/token",
redirectUri: () =>
checkedRedirectUri("Fitbit", "FITBIT_REDIRECT_URI", "/api/fitbit/callback"),
authorizeParams: () => ({ scope: FITBIT_OAUTH_SCOPE }),
pkce: true,
clientAuth: { kind: "basic", idInForm: { exchange: true, refresh: false } },
refresh: true,
upstreamError: fitbitErrorType,
error: (failure) =>
new FitbitApiError({
verb: failure.verb,
classification: failure.classification,
httpStatus: failure.status,
reason: failure.reason,
upstreamError: failure.upstreamError,
}),
});
+7 -24
View File
@@ -10,17 +10,14 @@ import type { MeasurementType } from "@/generated/prisma/client";
import { emitInsertedMeasurementArrivals } from "@/lib/arrivals/measurement-emit";
import { encrypt, decrypt } from "@/lib/crypto";
import { prisma } from "@/lib/db";
import { invalidateStatusInsightsForTypes } from "@/lib/insights/comprehensive-generate";
import { persistRotatedToken } from "@/lib/integrations/oauth-refresh";
import { recordSyncFailure } from "@/lib/integrations/status";
import type { FailureKind } from "@/lib/integrations/status";
import { getEvent } from "@/lib/logging/context";
import { dropImplausibleMeasurements } from "@/lib/measurements/plausibility-gate";
import {
collapseToTypeDayKeys,
recomputeBucketsForKeys,
} from "@/lib/rollups/measurement-rollups";
import { refreshAccessToken } from "./client";
import { collapseToTypeDayKeys } from "@/lib/rollups/measurement-rollups";
import { afterProviderWrite } from "@/lib/integrations/sync-kit/after-write";
import { fitbitOAuth } from "./oauth";
import { isFitbitRetired } from "./retirement";
import { getUserFitbitCredentials } from "./credentials";
import { FitbitApiError, classifyFitbitError } from "./response-classifier";
@@ -115,7 +112,7 @@ export async function getValidToken(
return null;
}
const newTokens = await refreshAccessToken(refreshToken, creds);
const newTokens = await fitbitOAuth.refresh(refreshToken, creds);
const expiresAt = new Date(Date.now() + newTokens.expires_in * 1000);
// Classic Fitbit ROTATES refresh tokens: the refresh response carries a
@@ -898,23 +895,9 @@ export async function upsertFitbitMeasurements(
return { imported, touched, inserted: insertedRows };
}
try {
const keys = collapseToTypeDayKeys(touched);
await recomputeBucketsForKeys(userId, keys);
invalidateStatusInsightsForTypes(
userId,
keys.map((k) => k.type),
{ source: "sync" },
).catch((err) => {
getEvent()?.addWarning(
`fitbit: status-insight invalidate failed for ${userId}: ${err}`,
);
});
} catch (err) {
getEvent()?.addWarning(
`fitbit: rollup recompute failed for ${userId}: ${err}`,
);
}
// The arrivals went out per chunk above; the shared tail folds, marks the
// caches and re-warms the status cards.
await afterProviderWrite({ userId, provider: "fitbit", touched });
return { imported, touched, inserted: insertedRows };
}
+5 -18
View File
@@ -2,10 +2,9 @@
import pLimit from "p-limit";
import { invalidateUserMeasurements } from "@/lib/cache/invalidate";
import { prisma } from "@/lib/db";
import { invalidateStatusInsightsForTypes } from "@/lib/insights/comprehensive-generate";
import { isReauthRequired, recordSyncSuccess } from "@/lib/integrations/status";
import { annotate, getEvent } from "@/lib/logging/context";
import { recomputeUserRollups } from "@/lib/rollups/measurement-rollups";
import { afterProviderBackfill } from "@/lib/integrations/sync-kit/after-write";
import { syncUserActivity } from "./sync-activity";
import {
hardFailStorage,
@@ -127,22 +126,10 @@ export async function syncUserFitbit(
// + 3 queue sends). Best-effort: a populator hiccup never fails the backfill.
if (opts.fullSync && deferTracker.keys.size > 0) {
try {
const days = Array.from(deferTracker.keys.values());
const types = Array.from(new Set(days.map((k) => k.type)));
const sorted = days
.map((k) => k.measuredAt.getTime())
.sort((a, b) => a - b);
const from = new Date(sorted[0]!);
// The keys are UTC day-starts; extend `to` past the last touched day so
// the aggregator's `< to` upper bound covers it.
const to = new Date(sorted[sorted.length - 1]! + 24 * 60 * 60 * 1000);
await recomputeUserRollups(userId, { types, from, to });
invalidateStatusInsightsForTypes(userId, types, {
source: "sync",
}).catch((err) => {
getEvent()?.addWarning(
`fitbit: status-insight invalidate failed for ${userId}: ${err}`,
);
await afterProviderBackfill({
userId,
provider: "fitbit",
keys: Array.from(deferTracker.keys.values()),
});
} catch (err) {
getEvent()?.addWarning(
@@ -0,0 +1,94 @@
[
{
"name": "users/me/dataTypes/daily-oxygen-saturation/dataPoints/spo2-0001",
"dailyOxygenSaturation": {
"date": { "year": 2026, "month": 6, "day": 1 },
"averagePercentage": 96.5
}
},
{
"name": "users/me/dataTypes/daily-oxygen-saturation/dataPoints/spo2-0002",
"dailyOxygenSaturation": {
"date": "2026-06-02T00:00:00",
"averagePercentage": "97"
}
},
{
"name": "users/me/dataTypes/daily-heart-rate-variability/dataPoints/hrv-0001",
"dailyHeartRateVariability": {
"date": { "year": 2026, "month": 6, "day": 1 },
"averageHeartRateVariabilityMilliseconds": 42.123456
}
},
{
"name": "users/me/dataTypes/daily-heart-rate-variability/dataPoints/hrv-0002",
"dailyHeartRateVariability": {
"date": { "year": 2026, "month": 12, "day": 31 },
"averageHeartRateVariabilityMilliseconds": 0
}
},
{
"name": "users/me/dataTypes/daily-resting-heart-rate/dataPoints/rhr-0001",
"dailyRestingHeartRate": {
"date": "2026-06-03",
"beatsPerMinute": "55"
}
},
{
"name": "users/me/dataTypes/daily-resting-heart-rate/dataPoints/rhr-0002",
"dailyRestingHeartRate": {
"date": { "year": 2026, "month": "6", "day": 4 },
"beatsPerMinute": 57
}
},
{
"name": "users/me/dataTypes/daily-respiratory-rate/dataPoints/resp-0001",
"dailyRespiratoryRate": {
"date": { "year": 2026, "month": 3, "day": 29 },
"breathsPerMinute": 14.8
}
},
{
"name": "users/me/dataTypes/daily-respiratory-rate/dataPoints/resp-0002",
"dailyRespiratoryRate": {
"date": { "year": 2026, "month": 3, "day": 30 },
"dailyRespiratoryRateBpm": 15.1
}
},
{
"name": "users/me/dataTypes/daily-sleep-temperature-derivations/dataPoints/temp-0001",
"dailySleepTemperatureDerivations": {
"date": { "year": 2026, "month": 6, "day": 5 },
"nightlyTemperatureCelsius": 34.42,
"baselineTemperatureCelsius": 34.1,
"relativeNightlyStddev30dCelsius": 0.3
}
},
{
"name": "users/me/dataTypes/daily-sleep-temperature-derivations/dataPoints/temp-0002",
"dailySleepTemperatureDerivations": {
"date": "garbage",
"nightlyTemperatureCelsius": 34.6
}
},
{
"name": "users/me/dataTypes/daily-vo2-max/dataPoints/vo2-0001",
"dailyVo2Max": {
"date": { "year": 2026, "month": 6, "day": 6 },
"vo2Max": 47.25
}
},
{
"name": "users/me/dataTypes/daily-vo2-max/dataPoints/vo2-0002",
"dailyVo2Max": {
"vo2Max": "45"
}
},
{
"name": "users/me/dataTypes/daily-vo2-max/dataPoints/vo2-0003",
"dailyVo2Max": {
"date": { "year": 2026, "month": 6, "day": 7 },
"vo2Max": null
}
}
]
@@ -0,0 +1,102 @@
[
{
"name": "users/me/dataTypes/exercise/dataPoints/exercise-0001",
"exercise": {
"interval": {
"startTime": "2026-06-02T17:00:00Z",
"endTime": "2026-06-02T17:42:30Z",
"civilStartTime": {
"date": { "year": 2026, "month": 6, "day": 2 },
"time": { "hours": 19 }
}
},
"exerciseType": "RUNNING",
"metricsSummary": {
"caloriesKcal": 412.6,
"distanceMillimeters": "7012345",
"averageHeartRateBeatsPerMinute": "151"
}
}
},
{
"name": "users/me/dataTypes/exercise/dataPoints/exercise-0002",
"exercise": {
"interval": {
"startTime": "2026-06-03T07:00:00",
"endTime": "2026-06-03T08:00:00"
},
"exerciseType": "strength training",
"metricsSummary": {
"caloriesKcal": "250",
"averageHeartRateBeatsPerMinute": 0
}
}
},
{
"exercise": {
"interval": {
"startTime": "2026-06-04T06:30:00+02:00",
"endTime": "2026-06-04T07:15:00+02:00"
},
"exerciseType": " KITESURFING ",
"metricsSummary": {
"distanceMillimeters": -100,
"averageHeartRateBeatsPerMinute": "128.6"
}
}
},
{
"name": "users/me/dataTypes/exercise/dataPoints/exercise-0004",
"exercise": {
"interval": {
"startTime": "2026-06-05T12:00:00Z",
"endTime": "2026-06-05T12:30:00Z"
},
"exerciseType": " "
}
},
{
"name": "users/me/dataTypes/exercise/dataPoints/exercise-0005",
"exercise": {
"interval": {
"startTime": "2026-06-06T12:00:00Z",
"endTime": "2026-06-06T12:00:00Z"
},
"exerciseType": "WALKING"
}
},
{
"name": "users/me/dataTypes/exercise/dataPoints/exercise-0006",
"exercise": {
"interval": {
"startTime": "2026-06-07T12:00:00Z"
},
"exerciseType": "WALKING"
}
},
{
"name": "users/me/dataTypes/exercise/dataPoints/exercise-0007",
"exercise": {
"interval": {
"startTime": "2026-06-08T18:00:00Z",
"endTime": "2026-06-08T19:05:00Z"
},
"exerciseType": "Mountain-Biking",
"metricsSummary": {
"caloriesKcal": 0,
"distanceMillimeters": "0",
"averageHeartRateBeatsPerMinute": "x"
}
}
},
{
"name": "users/me/dataTypes/exercise/dataPoints/exercise-0008",
"exercise": {
"interval": {
"startTime": "2026-06-09T20:00:00Z",
"endTime": "2026-06-09T20:20:00Z"
},
"exerciseType": "STAIR_CLIMBER"
}
}
]
@@ -0,0 +1,72 @@
[
{
"civilStartTime": {
"date": { "year": 2026, "month": 6, "day": 1 },
"time": {}
},
"civilEndTime": {
"date": { "year": 2026, "month": 6, "day": 1 },
"time": { "hours": 23, "minutes": 59, "seconds": 59 }
},
"steps": { "countSum": "8432" }
},
{
"civilStartTime": {
"date": { "year": 2026, "month": 6, "day": 2 },
"time": {}
},
"steps": { "countSum": "0" }
},
{
"civilStartTime": "2026-06-03T00:00:00",
"steps": { "countSum": 10250 }
},
{
"civilEndTime": {
"date": { "year": 2026, "month": 6, "day": 4 }
},
"steps": { "countSum": "5000" }
},
{
"civilStartTime": {
"date": { "year": 2026, "month": 6, "day": 1 },
"time": {}
},
"distance": { "millimetersSum": "6240512" }
},
{
"civilStartTime": {
"date": { "year": 2026, "month": 6, "day": 2 },
"time": {}
},
"distance": { "millimetersSum": "-1" }
},
{
"civilStartTime": {
"date": { "year": 2026, "month": 6, "day": 1 },
"time": {}
},
"activeEnergyBurned": { "kcalSum": 512.75 }
},
{
"civilStartTime": {
"date": { "year": 2026, "month": 6, "day": 2 },
"time": {}
},
"activeEnergyBurned": {}
},
{
"civilStartTime": {
"date": { "year": 2026, "month": 12, "day": 31 },
"time": {}
},
"floors": { "countSum": "12" }
},
{
"civilStartTime": {
"date": { "year": 2026, "month": 1, "day": 1 },
"time": {}
},
"floors": { "countSum": "" }
}
]
@@ -0,0 +1,105 @@
[
{
"name": "users/me/dataTypes/weight/dataPoints/weight-0001",
"weight": {
"sampleTime": { "physicalTime": "2026-06-02T06:15:00Z" },
"weightGrams": 78400
}
},
{
"name": "users/me/dataTypes/weight/dataPoints/weight-0002",
"weight": {
"sampleTime": { "physicalTime": "2026-06-03T07:30:00+02:00" },
"weightGrams": "78250.5"
}
},
{
"name": "users/me/dataTypes/weight/dataPoints/weight-0003",
"weight": {
"sampleTime": { "physicalTime": "2026-06-04T23:45:00" },
"weightGrams": 0
}
},
{
"name": "users/me/dataTypes/weight/dataPoints/weight-0004",
"weight": {
"sampleTime": { "physicalTime": "2026-06-04T23:45:00" },
"weightGrams": 77900
}
},
{
"name": "users/me/dataTypes/body-fat/dataPoints/body-fat-0001",
"bodyFat": {
"sampleTime": { "physicalTime": "2026-06-02T06:15:00.000Z" },
"percentage": 21.4
}
},
{
"name": "users/me/dataTypes/body-fat/dataPoints/body-fat-0002",
"bodyFat": {
"percentage": "20.9"
}
},
{
"name": "users/me/dataTypes/heart-rate/dataPoints/heart-rate-0001",
"heartRate": {
"sampleTime": { "physicalTime": "2026-06-02T12:00:30-04:00" },
"beatsPerMinute": "64"
}
},
{
"name": "users/me/dataTypes/heart-rate/dataPoints/heart-rate-0002",
"heartRate": {
"sampleTime": { "physicalTime": "2026-06-02 00:10" },
"beatsPerMinute": " "
}
},
{
"name": "users/me/dataTypes/heart-rate/dataPoints/heart-rate-0003",
"heartRate": {
"sampleTime": { "physicalTime": "not-a-time" },
"beatsPerMinute": 58
}
},
{
"name": "users/me/dataTypes/blood-glucose/dataPoints/blood-glucose-0001",
"bloodGlucose": {
"sampleTime": { "physicalTime": "2026-06-05T08:00:00Z" },
"bloodGlucoseMilligramsPerDeciliter": 96.333333
}
},
{
"name": "users/me/dataTypes/blood-glucose/dataPoints/blood-glucose-0002",
"bloodGlucose": {
"sampleTime": { "physicalTime": "2026-06-05T09:00:00Z" },
"bloodGlucoseMilligramsPerDeciliter": -5
}
},
{
"name": "users/me/dataTypes/core-body-temperature/dataPoints/core-temp-0001",
"coreBodyTemperature": {
"sampleTime": { "physicalTime": "2026-06-06T03:00:00" },
"temperatureCelsius": 36.75
}
},
{
"name": "users/me/dataTypes/height/dataPoints/height-0001",
"height": {
"sampleTime": { "physicalTime": "2026-01-15T10:00:00Z" },
"heightMillimeters": "1805"
}
},
{
"name": "users/me/dataTypes/height/dataPoints/height-0002",
"height": {
"heightMillimeters": 1798
}
},
{
"name": "users/me/dataTypes/height/dataPoints/height-0003",
"height": {
"sampleTime": { "physicalTime": "2026-01-16T10:00:00Z" },
"heightMillimeters": "0"
}
}
]
@@ -0,0 +1,177 @@
[
{
"name": "users/me/dataTypes/sleep/dataPoints/sleep-0001",
"sleep": {
"interval": {
"startTime": "2026-06-01T22:30:00Z",
"endTime": "2026-06-02T06:10:00Z"
},
"stages": [
{
"startTime": "2026-06-01T22:30:00Z",
"startUtcOffset": "7200s",
"endTime": "2026-06-01T22:45:00Z",
"endUtcOffset": "7200s",
"type": "AWAKE"
},
{
"startTime": "2026-06-01T22:45:00Z",
"endTime": "2026-06-02T00:15:00Z",
"type": "LIGHT"
},
{
"startTime": "2026-06-02T00:15:00Z",
"endTime": "2026-06-02T01:20:30Z",
"type": "DEEP"
},
{
"startTime": "2026-06-02T01:20:30Z",
"endTime": "2026-06-02T02:05:00Z",
"type": "REM"
},
{
"startTime": "2026-06-02T02:05:00Z",
"endTime": "2026-06-02T06:10:00Z",
"type": "LIGHT"
}
]
}
},
{
"name": "users/me/dataTypes/sleep/dataPoints/sleep-0002",
"sleep": {
"interval": {
"startTime": "2026-06-02T23:50:00",
"endTime": "2026-06-03T07:00:00"
},
"stages": [
{
"startTime": "2026-06-02T23:50:00",
"endTime": "2026-06-03T00:20:00",
"type": "ASLEEP"
},
{
"startTime": "2026-06-03T00:20:00",
"endTime": "2026-06-03T00:25:00",
"type": "RESTLESS"
},
{
"startTime": "2026-06-03T00:25:00",
"endTime": "2026-06-03T07:00:00",
"type": "asleep"
}
]
}
},
{
"sleep": {
"interval": {
"startTime": "2026-06-04T22:00:00+02:00",
"endTime": "2026-06-05T05:30:00+02:00"
},
"stages": [
{
"startTime": "2026-06-04T22:00:00+02:00",
"endTime": "2026-06-05T01:00:00+02:00",
"type": "DEEP"
},
{
"startTime": "2026-06-05T01:00:00+02:00",
"endTime": "2026-06-05T05:30:00+02:00",
"type": "SLEEP_STAGE_TYPE_UNSPECIFIED"
}
]
}
},
{
"name": "",
"sleep": {
"interval": {
"startTime": "2026-06-06T23:00:00Z"
},
"stages": [
{
"startTime": "2026-06-06T23:00:00Z",
"endTime": "2026-06-06T23:00:00Z",
"type": "LIGHT"
},
{
"startTime": "2026-06-07T01:00:00Z",
"endTime": "2026-06-07T00:30:00Z",
"type": "REM"
},
{
"startTime": "2026-06-07T01:00:00Z",
"type": "DEEP"
},
{
"endTime": "2026-06-07T02:00:00Z",
"type": "DEEP"
},
null,
"LIGHT",
{
"startTime": "2026-06-07T02:00:00Z",
"endTime": "2026-06-07T02:30:00Z"
},
{
"startTime": "2026-06-07T02:00:00Z",
"endTime": "2026-06-07T02:30:00Z",
"type": 3
}
]
}
},
{
"sleep": {
"stages": "not-a-list"
}
},
{
"name": "users/me/dataTypes/sleep/dataPoints/sleep-0006",
"sleep": {
"stages": [
{
"startTime": "2026-06-08T00:00:00Z",
"endTime": "2026-06-08T00:10:00Z",
"type": "NAP_STAGE_ALPHA"
},
{
"startTime": "2026-06-08T00:10:00Z",
"endTime": "2026-06-08T00:20:00Z",
"type": "NAP_STAGE_BETA"
},
{
"startTime": "2026-06-08T00:20:00Z",
"endTime": "2026-06-08T00:30:00Z",
"type": "NAP_STAGE_GAMMA"
},
{
"startTime": "2026-06-08T00:30:00Z",
"endTime": "2026-06-08T00:40:00Z",
"type": "NAP_STAGE_ALPHA"
},
{
"startTime": "2026-06-08T00:40:00Z",
"endTime": "2026-06-08T00:50:00Z",
"type": "NAP_STAGE_DELTA"
},
{
"startTime": "2026-06-08T00:50:00Z",
"endTime": "2026-06-08T01:00:00Z",
"type": "NAP_STAGE_EPSILON"
},
{
"startTime": "2026-06-08T01:00:00Z",
"endTime": "2026-06-08T01:10:00Z",
"type": "NAP_STAGE_ZETA_WITH_A_LABEL_MUCH_LONGER_THAN_FORTY_CHARACTERS"
},
{
"startTime": "2026-06-08T01:10:00Z",
"endTime": "2026-06-08T01:40:00Z",
"type": "light"
}
]
}
}
]
@@ -0,0 +1,604 @@
{
"resolveGoogleHealthUserId": [
{ "profile": { "name": "users/1234567890" }, "userId": "1234567890" },
{ "profile": { "name": "users/", "id": "id-0002" }, "userId": "id-0002" },
{ "profile": { "name": "plain-0003" }, "userId": "plain-0003" },
{ "profile": { "id": "id-0004" }, "userId": "id-0004" },
{ "profile": {}, "userId": "me" }
],
"formatCivilBound": [
{
"instant": "2026-06-01T21:59:59.000Z",
"tz": null,
"bound": "2026-06-01T21:59:59"
},
{
"instant": "2026-06-01T21:59:59.000Z",
"tz": "Europe/Berlin",
"bound": "2026-06-01T23:59:59"
},
{
"instant": "2026-06-01T21:59:59.000Z",
"tz": "America/New_York",
"bound": "2026-06-01T17:59:59"
},
{
"instant": "2026-06-01T22:00:00.000Z",
"tz": null,
"bound": "2026-06-01T22:00:00"
},
{
"instant": "2026-06-01T22:00:00.000Z",
"tz": "Europe/Berlin",
"bound": "2026-06-02T00:00:00"
},
{
"instant": "2026-06-01T22:00:00.000Z",
"tz": "America/New_York",
"bound": "2026-06-01T18:00:00"
},
{
"instant": "2026-03-29T00:30:00.000Z",
"tz": null,
"bound": "2026-03-29T00:30:00"
},
{
"instant": "2026-03-29T00:30:00.000Z",
"tz": "Europe/Berlin",
"bound": "2026-03-29T01:30:00"
},
{
"instant": "2026-03-29T00:30:00.000Z",
"tz": "America/New_York",
"bound": "2026-03-28T20:30:00"
},
{
"instant": "2026-12-31T23:30:15.500Z",
"tz": null,
"bound": "2026-12-31T23:30:15"
},
{
"instant": "2026-12-31T23:30:15.500Z",
"tz": "Europe/Berlin",
"bound": "2027-01-01T00:30:15"
},
{
"instant": "2026-12-31T23:30:15.500Z",
"tz": "America/New_York",
"bound": "2026-12-31T18:30:15"
}
],
"civilDateInTz": [
{
"instant": "2026-06-01T21:59:59.000Z",
"tz": null,
"date": { "year": 2026, "month": 6, "day": 1 }
},
{
"instant": "2026-06-01T21:59:59.000Z",
"tz": "Europe/Berlin",
"date": { "year": 2026, "month": 6, "day": 1 }
},
{
"instant": "2026-06-01T21:59:59.000Z",
"tz": "America/New_York",
"date": { "year": 2026, "month": 6, "day": 1 }
},
{
"instant": "2026-06-01T22:00:00.000Z",
"tz": null,
"date": { "year": 2026, "month": 6, "day": 1 }
},
{
"instant": "2026-06-01T22:00:00.000Z",
"tz": "Europe/Berlin",
"date": { "year": 2026, "month": 6, "day": 2 }
},
{
"instant": "2026-06-01T22:00:00.000Z",
"tz": "America/New_York",
"date": { "year": 2026, "month": 6, "day": 1 }
},
{
"instant": "2026-03-29T00:30:00.000Z",
"tz": null,
"date": { "year": 2026, "month": 3, "day": 29 }
},
{
"instant": "2026-03-29T00:30:00.000Z",
"tz": "Europe/Berlin",
"date": { "year": 2026, "month": 3, "day": 29 }
},
{
"instant": "2026-03-29T00:30:00.000Z",
"tz": "America/New_York",
"date": { "year": 2026, "month": 3, "day": 28 }
},
{
"instant": "2026-12-31T23:30:15.500Z",
"tz": null,
"date": { "year": 2026, "month": 12, "day": 31 }
},
{
"instant": "2026-12-31T23:30:15.500Z",
"tz": "Europe/Berlin",
"date": { "year": 2027, "month": 1, "day": 1 }
},
{
"instant": "2026-12-31T23:30:15.500Z",
"tz": "America/New_York",
"date": { "year": 2026, "month": 12, "day": 31 }
}
],
"incrementalFilter": [
{
"dataType": "weight",
"style": "camel",
"tz": null,
"filter": {
"field": "weight.sample_time.physical_time",
"bound": "2026-06-01T22:00:00.000Z"
}
},
{
"dataType": "weight",
"style": "camel",
"tz": "Europe/Berlin",
"filter": {
"field": "weight.sample_time.physical_time",
"bound": "2026-06-01T22:00:00.000Z"
}
},
{
"dataType": "weight",
"style": "camel",
"tz": "America/New_York",
"filter": {
"field": "weight.sample_time.physical_time",
"bound": "2026-06-01T22:00:00.000Z"
}
},
{
"dataType": "weight",
"style": "snake",
"tz": null,
"filter": {
"field": "weight.sample_time.physical_time",
"bound": "2026-06-01T22:00:00.000Z"
}
},
{
"dataType": "weight",
"style": "snake",
"tz": "Europe/Berlin",
"filter": {
"field": "weight.sample_time.physical_time",
"bound": "2026-06-01T22:00:00.000Z"
}
},
{
"dataType": "weight",
"style": "snake",
"tz": "America/New_York",
"filter": {
"field": "weight.sample_time.physical_time",
"bound": "2026-06-01T22:00:00.000Z"
}
},
{
"dataType": "daily-heart-rate-variability",
"style": "camel",
"tz": null,
"filter": {
"field": "dailyHeartRateVariability.date",
"bound": "2026-06-01"
}
},
{
"dataType": "daily-heart-rate-variability",
"style": "camel",
"tz": "Europe/Berlin",
"filter": {
"field": "dailyHeartRateVariability.date",
"bound": "2026-06-01"
}
},
{
"dataType": "daily-heart-rate-variability",
"style": "camel",
"tz": "America/New_York",
"filter": {
"field": "dailyHeartRateVariability.date",
"bound": "2026-06-01"
}
},
{
"dataType": "daily-heart-rate-variability",
"style": "snake",
"tz": null,
"filter": {
"field": "daily_heart_rate_variability.date",
"bound": "2026-06-01"
}
},
{
"dataType": "daily-heart-rate-variability",
"style": "snake",
"tz": "Europe/Berlin",
"filter": {
"field": "daily_heart_rate_variability.date",
"bound": "2026-06-01"
}
},
{
"dataType": "daily-heart-rate-variability",
"style": "snake",
"tz": "America/New_York",
"filter": {
"field": "daily_heart_rate_variability.date",
"bound": "2026-06-01"
}
},
{
"dataType": "sleep",
"style": "camel",
"tz": null,
"filter": {
"field": "sleep.interval.end_time",
"bound": "2026-06-01T22:00:00.000Z"
}
},
{
"dataType": "sleep",
"style": "camel",
"tz": "Europe/Berlin",
"filter": {
"field": "sleep.interval.end_time",
"bound": "2026-06-01T22:00:00.000Z"
}
},
{
"dataType": "sleep",
"style": "camel",
"tz": "America/New_York",
"filter": {
"field": "sleep.interval.end_time",
"bound": "2026-06-01T22:00:00.000Z"
}
},
{
"dataType": "sleep",
"style": "snake",
"tz": null,
"filter": {
"field": "sleep.interval.end_time",
"bound": "2026-06-01T22:00:00.000Z"
}
},
{
"dataType": "sleep",
"style": "snake",
"tz": "Europe/Berlin",
"filter": {
"field": "sleep.interval.end_time",
"bound": "2026-06-01T22:00:00.000Z"
}
},
{
"dataType": "sleep",
"style": "snake",
"tz": "America/New_York",
"filter": {
"field": "sleep.interval.end_time",
"bound": "2026-06-01T22:00:00.000Z"
}
},
{
"dataType": "exercise",
"style": "camel",
"tz": null,
"filter": {
"field": "exercise.interval.civil_start_time",
"bound": "2026-06-01T22:00:00"
}
},
{
"dataType": "exercise",
"style": "camel",
"tz": "Europe/Berlin",
"filter": {
"field": "exercise.interval.civil_start_time",
"bound": "2026-06-02T00:00:00"
}
},
{
"dataType": "exercise",
"style": "camel",
"tz": "America/New_York",
"filter": {
"field": "exercise.interval.civil_start_time",
"bound": "2026-06-01T18:00:00"
}
},
{
"dataType": "exercise",
"style": "snake",
"tz": null,
"filter": {
"field": "exercise.interval.civil_start_time",
"bound": "2026-06-01T22:00:00"
}
},
{
"dataType": "exercise",
"style": "snake",
"tz": "Europe/Berlin",
"filter": {
"field": "exercise.interval.civil_start_time",
"bound": "2026-06-02T00:00:00"
}
},
{
"dataType": "exercise",
"style": "snake",
"tz": "America/New_York",
"filter": {
"field": "exercise.interval.civil_start_time",
"bound": "2026-06-01T18:00:00"
}
},
{
"dataType": "steps",
"style": "camel",
"tz": null,
"filter": {
"threw": "Google Health data type steps reads via :dailyRollUp, not dataPoints.list"
}
},
{
"dataType": "steps",
"style": "camel",
"tz": "Europe/Berlin",
"filter": {
"threw": "Google Health data type steps reads via :dailyRollUp, not dataPoints.list"
}
},
{
"dataType": "steps",
"style": "camel",
"tz": "America/New_York",
"filter": {
"threw": "Google Health data type steps reads via :dailyRollUp, not dataPoints.list"
}
},
{
"dataType": "steps",
"style": "snake",
"tz": null,
"filter": {
"threw": "Google Health data type steps reads via :dailyRollUp, not dataPoints.list"
}
},
{
"dataType": "steps",
"style": "snake",
"tz": "Europe/Berlin",
"filter": {
"threw": "Google Health data type steps reads via :dailyRollUp, not dataPoints.list"
}
},
{
"dataType": "steps",
"style": "snake",
"tz": "America/New_York",
"filter": {
"threw": "Google Health data type steps reads via :dailyRollUp, not dataPoints.list"
}
}
],
"incrementalFilterDefaultStyle": {
"field": "dailyRestingHeartRate.date",
"bound": "2026-06-01"
},
"chunkCivilRange": [
{
"start": { "year": 2026, "month": 1, "day": 1 },
"end": { "year": 2026, "month": 7, "day": 1 },
"maxDays": null,
"chunks": [
{
"start": { "year": 2026, "month": 1, "day": 1 },
"end": { "year": 2026, "month": 4, "day": 1 }
},
{
"start": { "year": 2026, "month": 4, "day": 1 },
"end": { "year": 2026, "month": 6, "day": 30 }
},
{
"start": { "year": 2026, "month": 6, "day": 30 },
"end": { "year": 2026, "month": 7, "day": 1 }
}
]
},
{
"start": { "year": 2026, "month": 2, "day": 20 },
"end": { "year": 2026, "month": 3, "day": 10 },
"maxDays": 14,
"chunks": [
{
"start": { "year": 2026, "month": 2, "day": 20 },
"end": { "year": 2026, "month": 3, "day": 6 }
},
{
"start": { "year": 2026, "month": 3, "day": 6 },
"end": { "year": 2026, "month": 3, "day": 10 }
}
]
},
{
"start": { "year": 2026, "month": 6, "day": 1 },
"end": { "year": 2026, "month": 6, "day": 1 },
"maxDays": null,
"chunks": []
},
{
"start": { "year": 2026, "month": 6, "day": 2 },
"end": { "year": 2026, "month": 6, "day": 1 },
"maxDays": null,
"chunks": []
}
],
"buildDailyRollUpBody": [
{
"chunk": {
"start": { "year": 2026, "month": 1, "day": 1 },
"end": { "year": 2026, "month": 4, "day": 1 }
},
"pageToken": null,
"body": {
"range": {
"start": {
"date": { "year": 2026, "month": 1, "day": 1 },
"time": { "hours": 0, "minutes": 0, "seconds": 0, "nanos": 0 }
},
"end": {
"date": { "year": 2026, "month": 3, "day": 31 },
"time": { "hours": 23, "minutes": 59, "seconds": 59, "nanos": 0 }
}
},
"windowSizeDays": 1
}
},
{
"chunk": {
"start": { "year": 2026, "month": 2, "day": 20 },
"end": { "year": 2026, "month": 3, "day": 1 }
},
"pageToken": "page-token-0002",
"body": {
"range": {
"start": {
"date": { "year": 2026, "month": 2, "day": 20 },
"time": { "hours": 0, "minutes": 0, "seconds": 0, "nanos": 0 }
},
"end": {
"date": { "year": 2026, "month": 2, "day": 28 },
"time": { "hours": 23, "minutes": 59, "seconds": 59, "nanos": 0 }
}
},
"windowSizeDays": 1,
"pageToken": "page-token-0002"
}
},
{
"chunk": {
"start": { "year": 2026, "month": 12, "day": 31 },
"end": { "year": 2027, "month": 1, "day": 1 }
},
"pageToken": "",
"body": {
"range": {
"start": {
"date": { "year": 2026, "month": 12, "day": 31 },
"time": { "hours": 0, "minutes": 0, "seconds": 0, "nanos": 0 }
},
"end": {
"date": { "year": 2026, "month": 12, "day": 31 },
"time": { "hours": 23, "minutes": 59, "seconds": 59, "nanos": 0 }
}
},
"windowSizeDays": 1
}
}
],
"extractGoogleApiErrorDetail": [
{
"body": {
"error": {
"code": 400,
"status": "INVALID_ARGUMENT",
"message": "Invalid filter for https://health.googleapis.com/v4/users/me/dataTypes/weight/dataPoints?filter=weight.date",
"details": [
{
"@type": "type.googleapis.com/google.rpc.BadRequest",
"fieldViolations": [
{
"field": "filter",
"description": "Unknown field weight.date"
},
{ "field": "page_size" },
{ "description": "Authorization: Bearer abc.def.ghi rejected" },
{ "field": "fourth", "description": "dropped past the third" }
]
}
]
}
},
"detail": "INVALID_ARGUMENT: Invalid filter for https://health.googleapis.com/v4/users/me/dataTypes/weight/dataPoints?[REDACTED] [filter: Unknown field weight.date; page_size; Authorization: Bearer [REDACTED] rejected]"
},
{
"body": { "error": { "status": "RESOURCE_EXHAUSTED" } },
"detail": "RESOURCE_EXHAUSTED"
},
{
"body": {
"error": {
"message": "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
}
},
"detail": "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
},
{
"body": {
"error": { "details": [null, { "fieldViolations": [{}, 5] }] }
},
"detail": null
},
{ "body": { "error": "invalid_grant" }, "detail": null },
{ "body": { "error": {} }, "detail": null },
{ "body": null, "detail": null },
{ "body": "plain text", "detail": null }
],
"refusesEdgeFilter": [
{ "detail": null, "refuses": true },
{ "detail": "INVALID_ARGUMENT", "refuses": true },
{ "detail": "INVALID_ARGUMENT: Invalid filter", "refuses": true },
{ "detail": "INVALID_ARGUMENT: Invalid page token", "refuses": false },
{ "detail": "INVALID_ARGUMENT: page_size out of range", "refuses": false },
{ "detail": "INVALID_ARGUMENT: pageSize too large", "refuses": false },
{ "detail": "UNAUTHENTICATED: access token expired", "refuses": false },
{ "detail": "FAILED_PRECONDITION: something else", "refuses": false },
{ "detail": "Bad filter and bad token", "refuses": true }
],
"googleHealthRetryDelayMs": [
{ "headers": { "retry-after": "30" }, "body": null, "delayMs": 30000 },
{ "headers": { "retry-after": "1.5" }, "body": null, "delayMs": 1500 },
{
"headers": { "retry-after": "Wed, 01 Jul 2026 09:00:45 GMT" },
"body": null,
"delayMs": 45000
},
{ "headers": { "retry-after": "-3" }, "body": null, "delayMs": null },
{
"headers": {},
"body": {
"error": {
"details": [
{ "@type": "type.googleapis.com/google.rpc.ErrorInfo" },
{
"@type": "type.googleapis.com/google.rpc.RetryInfo",
"retryDelay": "12.5s"
}
]
}
},
"delayMs": 12500
},
{
"headers": {},
"body": { "error": { "details": [{ "retryDelay": "soon" }] } },
"delayMs": null
},
{ "headers": {}, "body": null, "delayMs": null }
]
}
@@ -0,0 +1,457 @@
[
{
"index": 0,
"key": "dailyOxygenSaturation",
"tz": null,
"output": [
{
"type": "OXYGEN_SATURATION",
"value": 96.5,
"unit": "%",
"measuredAt": "2026-06-01T12:00:00.000Z",
"fieldTag": "2026-06-01:spo2"
}
]
},
{
"index": 0,
"key": "dailyOxygenSaturation",
"tz": "Europe/Berlin",
"output": [
{
"type": "OXYGEN_SATURATION",
"value": 96.5,
"unit": "%",
"measuredAt": "2026-06-01T10:00:00.000Z",
"fieldTag": "2026-06-01:spo2"
}
]
},
{
"index": 0,
"key": "dailyOxygenSaturation",
"tz": "America/New_York",
"output": [
{
"type": "OXYGEN_SATURATION",
"value": 96.5,
"unit": "%",
"measuredAt": "2026-06-01T16:00:00.000Z",
"fieldTag": "2026-06-01:spo2"
}
]
},
{
"index": 1,
"key": "dailyOxygenSaturation",
"tz": null,
"output": [
{
"type": "OXYGEN_SATURATION",
"value": 97,
"unit": "%",
"measuredAt": "2026-06-02T12:00:00.000Z",
"fieldTag": "2026-06-02:spo2"
}
]
},
{
"index": 1,
"key": "dailyOxygenSaturation",
"tz": "Europe/Berlin",
"output": [
{
"type": "OXYGEN_SATURATION",
"value": 97,
"unit": "%",
"measuredAt": "2026-06-02T10:00:00.000Z",
"fieldTag": "2026-06-02:spo2"
}
]
},
{
"index": 1,
"key": "dailyOxygenSaturation",
"tz": "America/New_York",
"output": [
{
"type": "OXYGEN_SATURATION",
"value": 97,
"unit": "%",
"measuredAt": "2026-06-02T16:00:00.000Z",
"fieldTag": "2026-06-02:spo2"
}
]
},
{
"index": 2,
"key": "dailyHeartRateVariability",
"tz": null,
"output": [
{
"type": "HRV_RMSSD",
"value": 42.12,
"unit": "ms",
"measuredAt": "2026-06-01T12:00:00.000Z",
"fieldTag": "2026-06-01:hrv"
}
]
},
{
"index": 2,
"key": "dailyHeartRateVariability",
"tz": "Europe/Berlin",
"output": [
{
"type": "HRV_RMSSD",
"value": 42.12,
"unit": "ms",
"measuredAt": "2026-06-01T10:00:00.000Z",
"fieldTag": "2026-06-01:hrv"
}
]
},
{
"index": 2,
"key": "dailyHeartRateVariability",
"tz": "America/New_York",
"output": [
{
"type": "HRV_RMSSD",
"value": 42.12,
"unit": "ms",
"measuredAt": "2026-06-01T16:00:00.000Z",
"fieldTag": "2026-06-01:hrv"
}
]
},
{ "index": 3, "key": "dailyHeartRateVariability", "tz": null, "output": [] },
{
"index": 3,
"key": "dailyHeartRateVariability",
"tz": "Europe/Berlin",
"output": []
},
{
"index": 3,
"key": "dailyHeartRateVariability",
"tz": "America/New_York",
"output": []
},
{
"index": 4,
"key": "dailyRestingHeartRate",
"tz": null,
"output": [
{
"type": "RESTING_HEART_RATE",
"value": 55,
"unit": "bpm",
"measuredAt": "2026-06-03T12:00:00.000Z",
"fieldTag": "2026-06-03:rhr"
}
]
},
{
"index": 4,
"key": "dailyRestingHeartRate",
"tz": "Europe/Berlin",
"output": [
{
"type": "RESTING_HEART_RATE",
"value": 55,
"unit": "bpm",
"measuredAt": "2026-06-03T10:00:00.000Z",
"fieldTag": "2026-06-03:rhr"
}
]
},
{
"index": 4,
"key": "dailyRestingHeartRate",
"tz": "America/New_York",
"output": [
{
"type": "RESTING_HEART_RATE",
"value": 55,
"unit": "bpm",
"measuredAt": "2026-06-03T16:00:00.000Z",
"fieldTag": "2026-06-03:rhr"
}
]
},
{
"index": 5,
"key": "dailyRestingHeartRate",
"tz": null,
"output": [
{
"type": "RESTING_HEART_RATE",
"value": 57,
"unit": "bpm",
"measuredAt": "2026-07-01T09:00:00.000Z",
"fieldTag": "1970-01-01T00:00:00.000Z:rhr"
}
]
},
{
"index": 5,
"key": "dailyRestingHeartRate",
"tz": "Europe/Berlin",
"output": [
{
"type": "RESTING_HEART_RATE",
"value": 57,
"unit": "bpm",
"measuredAt": "2026-07-01T09:00:00.000Z",
"fieldTag": "1970-01-01T00:00:00.000Z:rhr"
}
]
},
{
"index": 5,
"key": "dailyRestingHeartRate",
"tz": "America/New_York",
"output": [
{
"type": "RESTING_HEART_RATE",
"value": 57,
"unit": "bpm",
"measuredAt": "2026-07-01T09:00:00.000Z",
"fieldTag": "1970-01-01T00:00:00.000Z:rhr"
}
]
},
{
"index": 6,
"key": "dailyRespiratoryRate",
"tz": null,
"output": [
{
"type": "RESPIRATORY_RATE",
"value": 14.8,
"unit": "breaths/min",
"measuredAt": "2026-03-29T12:00:00.000Z",
"fieldTag": "2026-03-29:resp_rate"
}
]
},
{
"index": 6,
"key": "dailyRespiratoryRate",
"tz": "Europe/Berlin",
"output": [
{
"type": "RESPIRATORY_RATE",
"value": 14.8,
"unit": "breaths/min",
"measuredAt": "2026-03-29T10:00:00.000Z",
"fieldTag": "2026-03-29:resp_rate"
}
]
},
{
"index": 6,
"key": "dailyRespiratoryRate",
"tz": "America/New_York",
"output": [
{
"type": "RESPIRATORY_RATE",
"value": 14.8,
"unit": "breaths/min",
"measuredAt": "2026-03-29T16:00:00.000Z",
"fieldTag": "2026-03-29:resp_rate"
}
]
},
{ "index": 7, "key": "dailyRespiratoryRate", "tz": null, "output": [] },
{
"index": 7,
"key": "dailyRespiratoryRate",
"tz": "Europe/Berlin",
"output": []
},
{
"index": 7,
"key": "dailyRespiratoryRate",
"tz": "America/New_York",
"output": []
},
{
"index": 8,
"key": "dailySleepTemperatureDerivations",
"tz": null,
"output": [
{
"type": "WRIST_TEMPERATURE",
"value": 34.42,
"unit": "celsius",
"measuredAt": "2026-06-05T12:00:00.000Z",
"fieldTag": "2026-06-05:wrist_temp"
}
]
},
{
"index": 8,
"key": "dailySleepTemperatureDerivations",
"tz": "Europe/Berlin",
"output": [
{
"type": "WRIST_TEMPERATURE",
"value": 34.42,
"unit": "celsius",
"measuredAt": "2026-06-05T10:00:00.000Z",
"fieldTag": "2026-06-05:wrist_temp"
}
]
},
{
"index": 8,
"key": "dailySleepTemperatureDerivations",
"tz": "America/New_York",
"output": [
{
"type": "WRIST_TEMPERATURE",
"value": 34.42,
"unit": "celsius",
"measuredAt": "2026-06-05T16:00:00.000Z",
"fieldTag": "2026-06-05:wrist_temp"
}
]
},
{
"index": 9,
"key": "dailySleepTemperatureDerivations",
"tz": null,
"output": [
{
"type": "WRIST_TEMPERATURE",
"value": 34.6,
"unit": "celsius",
"measuredAt": "2026-07-01T09:00:00.000Z",
"fieldTag": "1970-01-01T00:00:00.000Z:wrist_temp"
}
]
},
{
"index": 9,
"key": "dailySleepTemperatureDerivations",
"tz": "Europe/Berlin",
"output": [
{
"type": "WRIST_TEMPERATURE",
"value": 34.6,
"unit": "celsius",
"measuredAt": "2026-07-01T09:00:00.000Z",
"fieldTag": "1970-01-01T00:00:00.000Z:wrist_temp"
}
]
},
{
"index": 9,
"key": "dailySleepTemperatureDerivations",
"tz": "America/New_York",
"output": [
{
"type": "WRIST_TEMPERATURE",
"value": 34.6,
"unit": "celsius",
"measuredAt": "2026-07-01T09:00:00.000Z",
"fieldTag": "1970-01-01T00:00:00.000Z:wrist_temp"
}
]
},
{
"index": 10,
"key": "dailyVo2Max",
"tz": null,
"output": [
{
"type": "VO2_MAX",
"value": 47.25,
"unit": "mL/(kg·min)",
"measuredAt": "2026-06-06T12:00:00.000Z",
"fieldTag": "vo2_max:2026-06-06",
"cumulativeDaily": true
}
]
},
{
"index": 10,
"key": "dailyVo2Max",
"tz": "Europe/Berlin",
"output": [
{
"type": "VO2_MAX",
"value": 47.25,
"unit": "mL/(kg·min)",
"measuredAt": "2026-06-06T10:00:00.000Z",
"fieldTag": "vo2_max:2026-06-06",
"cumulativeDaily": true
}
]
},
{
"index": 10,
"key": "dailyVo2Max",
"tz": "America/New_York",
"output": [
{
"type": "VO2_MAX",
"value": 47.25,
"unit": "mL/(kg·min)",
"measuredAt": "2026-06-06T16:00:00.000Z",
"fieldTag": "vo2_max:2026-06-06",
"cumulativeDaily": true
}
]
},
{
"index": 11,
"key": "dailyVo2Max",
"tz": null,
"output": [
{
"type": "VO2_MAX",
"value": 45,
"unit": "mL/(kg·min)",
"measuredAt": "2026-07-01T09:00:00.000Z",
"fieldTag": "vo2_max:1970-01-01T00:00:00.000Z",
"cumulativeDaily": true
}
]
},
{
"index": 11,
"key": "dailyVo2Max",
"tz": "Europe/Berlin",
"output": [
{
"type": "VO2_MAX",
"value": 45,
"unit": "mL/(kg·min)",
"measuredAt": "2026-07-01T09:00:00.000Z",
"fieldTag": "vo2_max:1970-01-01T00:00:00.000Z",
"cumulativeDaily": true
}
]
},
{
"index": 11,
"key": "dailyVo2Max",
"tz": "America/New_York",
"output": [
{
"type": "VO2_MAX",
"value": 45,
"unit": "mL/(kg·min)",
"measuredAt": "2026-07-01T09:00:00.000Z",
"fieldTag": "vo2_max:1970-01-01T00:00:00.000Z",
"cumulativeDaily": true
}
]
},
{ "index": 12, "key": "dailyVo2Max", "tz": null, "output": [] },
{ "index": 12, "key": "dailyVo2Max", "tz": "Europe/Berlin", "output": [] },
{ "index": 12, "key": "dailyVo2Max", "tz": "America/New_York", "output": [] }
]
@@ -0,0 +1,314 @@
[
{
"index": 0,
"tz": null,
"workout": {
"externalId": "users/me/dataTypes/exercise/dataPoints/exercise-0001",
"sportType": "running",
"sportTypeRaw": "RUNNING",
"startedAt": "2026-06-02T17:00:00.000Z",
"endedAt": "2026-06-02T17:42:30.000Z",
"durationSec": 2550,
"totalEnergyKcal": 413,
"totalDistanceM": 7012.35,
"avgHeartRate": 151,
"maxHeartRate": null,
"minHeartRate": null
}
},
{
"index": 0,
"tz": "Europe/Berlin",
"workout": {
"externalId": "users/me/dataTypes/exercise/dataPoints/exercise-0001",
"sportType": "running",
"sportTypeRaw": "RUNNING",
"startedAt": "2026-06-02T17:00:00.000Z",
"endedAt": "2026-06-02T17:42:30.000Z",
"durationSec": 2550,
"totalEnergyKcal": 413,
"totalDistanceM": 7012.35,
"avgHeartRate": 151,
"maxHeartRate": null,
"minHeartRate": null
}
},
{
"index": 0,
"tz": "America/New_York",
"workout": {
"externalId": "users/me/dataTypes/exercise/dataPoints/exercise-0001",
"sportType": "running",
"sportTypeRaw": "RUNNING",
"startedAt": "2026-06-02T17:00:00.000Z",
"endedAt": "2026-06-02T17:42:30.000Z",
"durationSec": 2550,
"totalEnergyKcal": 413,
"totalDistanceM": 7012.35,
"avgHeartRate": 151,
"maxHeartRate": null,
"minHeartRate": null
}
},
{
"index": 1,
"tz": null,
"workout": {
"externalId": "users/me/dataTypes/exercise/dataPoints/exercise-0002",
"sportType": "strength",
"sportTypeRaw": "strength training",
"startedAt": "2026-06-03T07:00:00.000Z",
"endedAt": "2026-06-03T08:00:00.000Z",
"durationSec": 3600,
"totalEnergyKcal": 250,
"totalDistanceM": null,
"avgHeartRate": null,
"maxHeartRate": null,
"minHeartRate": null
}
},
{
"index": 1,
"tz": "Europe/Berlin",
"workout": {
"externalId": "users/me/dataTypes/exercise/dataPoints/exercise-0002",
"sportType": "strength",
"sportTypeRaw": "strength training",
"startedAt": "2026-06-03T05:00:00.000Z",
"endedAt": "2026-06-03T06:00:00.000Z",
"durationSec": 3600,
"totalEnergyKcal": 250,
"totalDistanceM": null,
"avgHeartRate": null,
"maxHeartRate": null,
"minHeartRate": null
}
},
{
"index": 1,
"tz": "America/New_York",
"workout": {
"externalId": "users/me/dataTypes/exercise/dataPoints/exercise-0002",
"sportType": "strength",
"sportTypeRaw": "strength training",
"startedAt": "2026-06-03T11:00:00.000Z",
"endedAt": "2026-06-03T12:00:00.000Z",
"durationSec": 3600,
"totalEnergyKcal": 250,
"totalDistanceM": null,
"avgHeartRate": null,
"maxHeartRate": null,
"minHeartRate": null
}
},
{
"index": 2,
"tz": null,
"workout": {
"externalId": "exercise:2026-06-04T04:30:00.000Z",
"sportType": "other",
"sportTypeRaw": "KITESURFING",
"startedAt": "2026-06-04T04:30:00.000Z",
"endedAt": "2026-06-04T05:15:00.000Z",
"durationSec": 2700,
"totalEnergyKcal": null,
"totalDistanceM": null,
"avgHeartRate": 129,
"maxHeartRate": null,
"minHeartRate": null
}
},
{
"index": 2,
"tz": "Europe/Berlin",
"workout": {
"externalId": "exercise:2026-06-04T04:30:00.000Z",
"sportType": "other",
"sportTypeRaw": "KITESURFING",
"startedAt": "2026-06-04T04:30:00.000Z",
"endedAt": "2026-06-04T05:15:00.000Z",
"durationSec": 2700,
"totalEnergyKcal": null,
"totalDistanceM": null,
"avgHeartRate": 129,
"maxHeartRate": null,
"minHeartRate": null
}
},
{
"index": 2,
"tz": "America/New_York",
"workout": {
"externalId": "exercise:2026-06-04T04:30:00.000Z",
"sportType": "other",
"sportTypeRaw": "KITESURFING",
"startedAt": "2026-06-04T04:30:00.000Z",
"endedAt": "2026-06-04T05:15:00.000Z",
"durationSec": 2700,
"totalEnergyKcal": null,
"totalDistanceM": null,
"avgHeartRate": 129,
"maxHeartRate": null,
"minHeartRate": null
}
},
{
"index": 3,
"tz": null,
"workout": {
"externalId": "users/me/dataTypes/exercise/dataPoints/exercise-0004",
"sportType": "other",
"sportTypeRaw": null,
"startedAt": "2026-06-05T12:00:00.000Z",
"endedAt": "2026-06-05T12:30:00.000Z",
"durationSec": 1800,
"totalEnergyKcal": null,
"totalDistanceM": null,
"avgHeartRate": null,
"maxHeartRate": null,
"minHeartRate": null
}
},
{
"index": 3,
"tz": "Europe/Berlin",
"workout": {
"externalId": "users/me/dataTypes/exercise/dataPoints/exercise-0004",
"sportType": "other",
"sportTypeRaw": null,
"startedAt": "2026-06-05T12:00:00.000Z",
"endedAt": "2026-06-05T12:30:00.000Z",
"durationSec": 1800,
"totalEnergyKcal": null,
"totalDistanceM": null,
"avgHeartRate": null,
"maxHeartRate": null,
"minHeartRate": null
}
},
{
"index": 3,
"tz": "America/New_York",
"workout": {
"externalId": "users/me/dataTypes/exercise/dataPoints/exercise-0004",
"sportType": "other",
"sportTypeRaw": null,
"startedAt": "2026-06-05T12:00:00.000Z",
"endedAt": "2026-06-05T12:30:00.000Z",
"durationSec": 1800,
"totalEnergyKcal": null,
"totalDistanceM": null,
"avgHeartRate": null,
"maxHeartRate": null,
"minHeartRate": null
}
},
{ "index": 4, "tz": null, "workout": null },
{ "index": 4, "tz": "Europe/Berlin", "workout": null },
{ "index": 4, "tz": "America/New_York", "workout": null },
{ "index": 5, "tz": null, "workout": null },
{ "index": 5, "tz": "Europe/Berlin", "workout": null },
{ "index": 5, "tz": "America/New_York", "workout": null },
{
"index": 6,
"tz": null,
"workout": {
"externalId": "users/me/dataTypes/exercise/dataPoints/exercise-0007",
"sportType": "cycling",
"sportTypeRaw": "Mountain-Biking",
"startedAt": "2026-06-08T18:00:00.000Z",
"endedAt": "2026-06-08T19:05:00.000Z",
"durationSec": 3900,
"totalEnergyKcal": 0,
"totalDistanceM": 0,
"avgHeartRate": null,
"maxHeartRate": null,
"minHeartRate": null
}
},
{
"index": 6,
"tz": "Europe/Berlin",
"workout": {
"externalId": "users/me/dataTypes/exercise/dataPoints/exercise-0007",
"sportType": "cycling",
"sportTypeRaw": "Mountain-Biking",
"startedAt": "2026-06-08T18:00:00.000Z",
"endedAt": "2026-06-08T19:05:00.000Z",
"durationSec": 3900,
"totalEnergyKcal": 0,
"totalDistanceM": 0,
"avgHeartRate": null,
"maxHeartRate": null,
"minHeartRate": null
}
},
{
"index": 6,
"tz": "America/New_York",
"workout": {
"externalId": "users/me/dataTypes/exercise/dataPoints/exercise-0007",
"sportType": "cycling",
"sportTypeRaw": "Mountain-Biking",
"startedAt": "2026-06-08T18:00:00.000Z",
"endedAt": "2026-06-08T19:05:00.000Z",
"durationSec": 3900,
"totalEnergyKcal": 0,
"totalDistanceM": 0,
"avgHeartRate": null,
"maxHeartRate": null,
"minHeartRate": null
}
},
{
"index": 7,
"tz": null,
"workout": {
"externalId": "users/me/dataTypes/exercise/dataPoints/exercise-0008",
"sportType": "stairClimber",
"sportTypeRaw": "STAIR_CLIMBER",
"startedAt": "2026-06-09T20:00:00.000Z",
"endedAt": "2026-06-09T20:20:00.000Z",
"durationSec": 1200,
"totalEnergyKcal": null,
"totalDistanceM": null,
"avgHeartRate": null,
"maxHeartRate": null,
"minHeartRate": null
}
},
{
"index": 7,
"tz": "Europe/Berlin",
"workout": {
"externalId": "users/me/dataTypes/exercise/dataPoints/exercise-0008",
"sportType": "stairClimber",
"sportTypeRaw": "STAIR_CLIMBER",
"startedAt": "2026-06-09T20:00:00.000Z",
"endedAt": "2026-06-09T20:20:00.000Z",
"durationSec": 1200,
"totalEnergyKcal": null,
"totalDistanceM": null,
"avgHeartRate": null,
"maxHeartRate": null,
"minHeartRate": null
}
},
{
"index": 7,
"tz": "America/New_York",
"workout": {
"externalId": "users/me/dataTypes/exercise/dataPoints/exercise-0008",
"sportType": "stairClimber",
"sportTypeRaw": "STAIR_CLIMBER",
"startedAt": "2026-06-09T20:00:00.000Z",
"endedAt": "2026-06-09T20:20:00.000Z",
"durationSec": 1200,
"totalEnergyKcal": null,
"totalDistanceM": null,
"avgHeartRate": null,
"maxHeartRate": null,
"minHeartRate": null
}
}
]
@@ -0,0 +1,37 @@
{
"sleepStages": [
{ "raw": "AWAKE", "stage": "AWAKE" },
{ "raw": "wake", "stage": "AWAKE" },
{ "raw": "LIGHT", "stage": "CORE" },
{ "raw": " core ", "stage": "CORE" },
{ "raw": "Deep", "stage": "DEEP" },
{ "raw": "REM", "stage": "REM" },
{ "raw": "ASLEEP", "stage": "ASLEEP" },
{ "raw": "RESTLESS", "stage": "AWAKE" },
{ "raw": "in bed", "stage": "IN_BED" },
{ "raw": "IN-BED", "stage": "IN_BED" },
{ "raw": "inbed", "stage": "IN_BED" },
{ "raw": "SLEEP_STAGE_TYPE_UNSPECIFIED", "stage": null },
{ "raw": "", "stage": null },
{ "raw": 42, "stage": null },
{ "raw": null, "stage": null }
],
"sportTypes": [
{ "raw": "RUNNING", "sportType": "running" },
{ "raw": "TREADMILL_RUNNING", "sportType": "running" },
{ "raw": "STRENGTH_TRAINING", "sportType": "strength" },
{ "raw": "strength training", "sportType": "strength" },
{ "raw": "Mountain-Biking", "sportType": "cycling" },
{ "raw": "STAIR_CLIMBER", "sportType": "stairClimber" },
{ "raw": "STAIRCLIMBER", "sportType": "stairClimber" },
{ "raw": "HIGH INTENSITY INTERVAL TRAINING", "sportType": "hiit" },
{ "raw": "FOOTBALL", "sportType": "soccer" },
{ "raw": "PILATES", "sportType": "mindAndBody" },
{ "raw": "OTHER_WORKOUT", "sportType": "other" },
{ "raw": "KITESURFING", "sportType": "other" },
{ "raw": "", "sportType": "other" },
{ "raw": " ", "sportType": "other" },
{ "raw": null, "sportType": "other" },
{ "raw": 7, "sportType": "other" }
]
}
@@ -0,0 +1,294 @@
[
{
"index": 0,
"key": "steps",
"tz": null,
"output": [
{
"type": "ACTIVITY_STEPS",
"value": 8432,
"unit": "steps",
"measuredAt": "2026-06-01T12:00:00.000Z",
"fieldTag": "steps:2026-06-01",
"cumulativeDaily": true
}
]
},
{
"index": 0,
"key": "steps",
"tz": "Europe/Berlin",
"output": [
{
"type": "ACTIVITY_STEPS",
"value": 8432,
"unit": "steps",
"measuredAt": "2026-06-01T10:00:00.000Z",
"fieldTag": "steps:2026-06-01",
"cumulativeDaily": true
}
]
},
{
"index": 0,
"key": "steps",
"tz": "America/New_York",
"output": [
{
"type": "ACTIVITY_STEPS",
"value": 8432,
"unit": "steps",
"measuredAt": "2026-06-01T16:00:00.000Z",
"fieldTag": "steps:2026-06-01",
"cumulativeDaily": true
}
]
},
{
"index": 1,
"key": "steps",
"tz": null,
"output": [
{
"type": "ACTIVITY_STEPS",
"value": 0,
"unit": "steps",
"measuredAt": "2026-06-02T12:00:00.000Z",
"fieldTag": "steps:2026-06-02",
"cumulativeDaily": true
}
]
},
{
"index": 1,
"key": "steps",
"tz": "Europe/Berlin",
"output": [
{
"type": "ACTIVITY_STEPS",
"value": 0,
"unit": "steps",
"measuredAt": "2026-06-02T10:00:00.000Z",
"fieldTag": "steps:2026-06-02",
"cumulativeDaily": true
}
]
},
{
"index": 1,
"key": "steps",
"tz": "America/New_York",
"output": [
{
"type": "ACTIVITY_STEPS",
"value": 0,
"unit": "steps",
"measuredAt": "2026-06-02T16:00:00.000Z",
"fieldTag": "steps:2026-06-02",
"cumulativeDaily": true
}
]
},
{
"index": 2,
"key": "steps",
"tz": null,
"output": [
{
"type": "ACTIVITY_STEPS",
"value": 10250,
"unit": "steps",
"measuredAt": "2026-06-03T12:00:00.000Z",
"fieldTag": "steps:2026-06-03",
"cumulativeDaily": true
}
]
},
{
"index": 2,
"key": "steps",
"tz": "Europe/Berlin",
"output": [
{
"type": "ACTIVITY_STEPS",
"value": 10250,
"unit": "steps",
"measuredAt": "2026-06-03T10:00:00.000Z",
"fieldTag": "steps:2026-06-03",
"cumulativeDaily": true
}
]
},
{
"index": 2,
"key": "steps",
"tz": "America/New_York",
"output": [
{
"type": "ACTIVITY_STEPS",
"value": 10250,
"unit": "steps",
"measuredAt": "2026-06-03T16:00:00.000Z",
"fieldTag": "steps:2026-06-03",
"cumulativeDaily": true
}
]
},
{ "index": 3, "key": "steps", "tz": null, "output": [] },
{ "index": 3, "key": "steps", "tz": "Europe/Berlin", "output": [] },
{ "index": 3, "key": "steps", "tz": "America/New_York", "output": [] },
{
"index": 4,
"key": "distance",
"tz": null,
"output": [
{
"type": "WALKING_RUNNING_DISTANCE",
"value": 6240.51,
"unit": "m",
"measuredAt": "2026-06-01T12:00:00.000Z",
"fieldTag": "distance:2026-06-01",
"cumulativeDaily": true
}
]
},
{
"index": 4,
"key": "distance",
"tz": "Europe/Berlin",
"output": [
{
"type": "WALKING_RUNNING_DISTANCE",
"value": 6240.51,
"unit": "m",
"measuredAt": "2026-06-01T10:00:00.000Z",
"fieldTag": "distance:2026-06-01",
"cumulativeDaily": true
}
]
},
{
"index": 4,
"key": "distance",
"tz": "America/New_York",
"output": [
{
"type": "WALKING_RUNNING_DISTANCE",
"value": 6240.51,
"unit": "m",
"measuredAt": "2026-06-01T16:00:00.000Z",
"fieldTag": "distance:2026-06-01",
"cumulativeDaily": true
}
]
},
{ "index": 5, "key": "distance", "tz": null, "output": [] },
{ "index": 5, "key": "distance", "tz": "Europe/Berlin", "output": [] },
{ "index": 5, "key": "distance", "tz": "America/New_York", "output": [] },
{
"index": 6,
"key": "activeEnergyBurned",
"tz": null,
"output": [
{
"type": "ACTIVE_ENERGY_BURNED",
"value": 512.75,
"unit": "kcal",
"measuredAt": "2026-06-01T12:00:00.000Z",
"fieldTag": "active_energy:2026-06-01",
"cumulativeDaily": true
}
]
},
{
"index": 6,
"key": "activeEnergyBurned",
"tz": "Europe/Berlin",
"output": [
{
"type": "ACTIVE_ENERGY_BURNED",
"value": 512.75,
"unit": "kcal",
"measuredAt": "2026-06-01T10:00:00.000Z",
"fieldTag": "active_energy:2026-06-01",
"cumulativeDaily": true
}
]
},
{
"index": 6,
"key": "activeEnergyBurned",
"tz": "America/New_York",
"output": [
{
"type": "ACTIVE_ENERGY_BURNED",
"value": 512.75,
"unit": "kcal",
"measuredAt": "2026-06-01T16:00:00.000Z",
"fieldTag": "active_energy:2026-06-01",
"cumulativeDaily": true
}
]
},
{ "index": 7, "key": "activeEnergyBurned", "tz": null, "output": [] },
{
"index": 7,
"key": "activeEnergyBurned",
"tz": "Europe/Berlin",
"output": []
},
{
"index": 7,
"key": "activeEnergyBurned",
"tz": "America/New_York",
"output": []
},
{
"index": 8,
"key": "floors",
"tz": null,
"output": [
{
"type": "FLIGHTS_CLIMBED",
"value": 12,
"unit": "flights",
"measuredAt": "2026-12-31T12:00:00.000Z",
"fieldTag": "floors:2026-12-31",
"cumulativeDaily": true
}
]
},
{
"index": 8,
"key": "floors",
"tz": "Europe/Berlin",
"output": [
{
"type": "FLIGHTS_CLIMBED",
"value": 12,
"unit": "flights",
"measuredAt": "2026-12-31T11:00:00.000Z",
"fieldTag": "floors:2026-12-31",
"cumulativeDaily": true
}
]
},
{
"index": 8,
"key": "floors",
"tz": "America/New_York",
"output": [
{
"type": "FLIGHTS_CLIMBED",
"value": 12,
"unit": "flights",
"measuredAt": "2026-12-31T17:00:00.000Z",
"fieldTag": "floors:2026-12-31",
"cumulativeDaily": true
}
]
},
{ "index": 9, "key": "floors", "tz": null, "output": [] },
{ "index": 9, "key": "floors", "tz": "Europe/Berlin", "output": [] },
{ "index": 9, "key": "floors", "tz": "America/New_York", "output": [] }
]
@@ -0,0 +1,693 @@
{
"measurements": [
{
"index": 0,
"key": "weight",
"tz": null,
"output": [
{
"type": "WEIGHT",
"value": 78.4,
"unit": "kg",
"measuredAt": "2026-06-02T06:15:00.000Z",
"fieldTag": "2026-06-02T06:15:00.000Z:weight"
}
]
},
{
"index": 0,
"key": "weight",
"tz": "Europe/Berlin",
"output": [
{
"type": "WEIGHT",
"value": 78.4,
"unit": "kg",
"measuredAt": "2026-06-02T06:15:00.000Z",
"fieldTag": "2026-06-02T06:15:00.000Z:weight"
}
]
},
{
"index": 0,
"key": "weight",
"tz": "America/New_York",
"output": [
{
"type": "WEIGHT",
"value": 78.4,
"unit": "kg",
"measuredAt": "2026-06-02T06:15:00.000Z",
"fieldTag": "2026-06-02T06:15:00.000Z:weight"
}
]
},
{
"index": 1,
"key": "weight",
"tz": null,
"output": [
{
"type": "WEIGHT",
"value": 78.25,
"unit": "kg",
"measuredAt": "2026-06-03T05:30:00.000Z",
"fieldTag": "2026-06-03T05:30:00.000Z:weight"
}
]
},
{
"index": 1,
"key": "weight",
"tz": "Europe/Berlin",
"output": [
{
"type": "WEIGHT",
"value": 78.25,
"unit": "kg",
"measuredAt": "2026-06-03T05:30:00.000Z",
"fieldTag": "2026-06-03T05:30:00.000Z:weight"
}
]
},
{
"index": 1,
"key": "weight",
"tz": "America/New_York",
"output": [
{
"type": "WEIGHT",
"value": 78.25,
"unit": "kg",
"measuredAt": "2026-06-03T05:30:00.000Z",
"fieldTag": "2026-06-03T05:30:00.000Z:weight"
}
]
},
{ "index": 2, "key": "weight", "tz": null, "output": [] },
{ "index": 2, "key": "weight", "tz": "Europe/Berlin", "output": [] },
{ "index": 2, "key": "weight", "tz": "America/New_York", "output": [] },
{
"index": 3,
"key": "weight",
"tz": null,
"output": [
{
"type": "WEIGHT",
"value": 77.9,
"unit": "kg",
"measuredAt": "2026-06-04T23:45:00.000Z",
"fieldTag": "2026-06-04T23:45:00.000Z:weight"
}
]
},
{
"index": 3,
"key": "weight",
"tz": "Europe/Berlin",
"output": [
{
"type": "WEIGHT",
"value": 77.9,
"unit": "kg",
"measuredAt": "2026-06-04T21:45:00.000Z",
"fieldTag": "2026-06-04T23:45:00.000Z:weight"
}
]
},
{
"index": 3,
"key": "weight",
"tz": "America/New_York",
"output": [
{
"type": "WEIGHT",
"value": 77.9,
"unit": "kg",
"measuredAt": "2026-06-05T03:45:00.000Z",
"fieldTag": "2026-06-04T23:45:00.000Z:weight"
}
]
},
{
"index": 4,
"key": "bodyFat",
"tz": null,
"output": [
{
"type": "BODY_FAT",
"value": 21.4,
"unit": "%",
"measuredAt": "2026-06-02T06:15:00.000Z",
"fieldTag": "2026-06-02T06:15:00.000Z:body_fat"
}
]
},
{
"index": 4,
"key": "bodyFat",
"tz": "Europe/Berlin",
"output": [
{
"type": "BODY_FAT",
"value": 21.4,
"unit": "%",
"measuredAt": "2026-06-02T06:15:00.000Z",
"fieldTag": "2026-06-02T06:15:00.000Z:body_fat"
}
]
},
{
"index": 4,
"key": "bodyFat",
"tz": "America/New_York",
"output": [
{
"type": "BODY_FAT",
"value": 21.4,
"unit": "%",
"measuredAt": "2026-06-02T06:15:00.000Z",
"fieldTag": "2026-06-02T06:15:00.000Z:body_fat"
}
]
},
{
"index": 5,
"key": "bodyFat",
"tz": null,
"output": [
{
"type": "BODY_FAT",
"value": 20.9,
"unit": "%",
"measuredAt": "2026-07-01T09:00:00.000Z",
"fieldTag": "1970-01-01T00:00:00.000Z:body_fat"
}
]
},
{
"index": 5,
"key": "bodyFat",
"tz": "Europe/Berlin",
"output": [
{
"type": "BODY_FAT",
"value": 20.9,
"unit": "%",
"measuredAt": "2026-07-01T09:00:00.000Z",
"fieldTag": "1970-01-01T00:00:00.000Z:body_fat"
}
]
},
{
"index": 5,
"key": "bodyFat",
"tz": "America/New_York",
"output": [
{
"type": "BODY_FAT",
"value": 20.9,
"unit": "%",
"measuredAt": "2026-07-01T09:00:00.000Z",
"fieldTag": "1970-01-01T00:00:00.000Z:body_fat"
}
]
},
{
"index": 6,
"key": "heartRate",
"tz": null,
"output": [
{
"type": "PULSE",
"value": 64,
"unit": "bpm",
"measuredAt": "2026-06-02T16:00:30.000Z",
"fieldTag": "2026-06-02T16:00:30.000Z:hr"
}
]
},
{
"index": 6,
"key": "heartRate",
"tz": "Europe/Berlin",
"output": [
{
"type": "PULSE",
"value": 64,
"unit": "bpm",
"measuredAt": "2026-06-02T16:00:30.000Z",
"fieldTag": "2026-06-02T16:00:30.000Z:hr"
}
]
},
{
"index": 6,
"key": "heartRate",
"tz": "America/New_York",
"output": [
{
"type": "PULSE",
"value": 64,
"unit": "bpm",
"measuredAt": "2026-06-02T16:00:30.000Z",
"fieldTag": "2026-06-02T16:00:30.000Z:hr"
}
]
},
{ "index": 7, "key": "heartRate", "tz": null, "output": [] },
{ "index": 7, "key": "heartRate", "tz": "Europe/Berlin", "output": [] },
{ "index": 7, "key": "heartRate", "tz": "America/New_York", "output": [] },
{
"index": 8,
"key": "heartRate",
"tz": null,
"output": [
{
"type": "PULSE",
"value": 58,
"unit": "bpm",
"measuredAt": "2026-07-01T09:00:00.000Z",
"fieldTag": "1970-01-01T00:00:00.000Z:hr"
}
]
},
{
"index": 8,
"key": "heartRate",
"tz": "Europe/Berlin",
"output": [
{
"type": "PULSE",
"value": 58,
"unit": "bpm",
"measuredAt": "2026-07-01T09:00:00.000Z",
"fieldTag": "1970-01-01T00:00:00.000Z:hr"
}
]
},
{
"index": 8,
"key": "heartRate",
"tz": "America/New_York",
"output": [
{
"type": "PULSE",
"value": 58,
"unit": "bpm",
"measuredAt": "2026-07-01T09:00:00.000Z",
"fieldTag": "1970-01-01T00:00:00.000Z:hr"
}
]
},
{
"index": 9,
"key": "bloodGlucose",
"tz": null,
"output": [
{
"type": "BLOOD_GLUCOSE",
"value": 96.33,
"unit": "mg/dL",
"measuredAt": "2026-06-05T08:00:00.000Z",
"fieldTag": "2026-06-05T08:00:00.000Z:glucose"
}
]
},
{
"index": 9,
"key": "bloodGlucose",
"tz": "Europe/Berlin",
"output": [
{
"type": "BLOOD_GLUCOSE",
"value": 96.33,
"unit": "mg/dL",
"measuredAt": "2026-06-05T08:00:00.000Z",
"fieldTag": "2026-06-05T08:00:00.000Z:glucose"
}
]
},
{
"index": 9,
"key": "bloodGlucose",
"tz": "America/New_York",
"output": [
{
"type": "BLOOD_GLUCOSE",
"value": 96.33,
"unit": "mg/dL",
"measuredAt": "2026-06-05T08:00:00.000Z",
"fieldTag": "2026-06-05T08:00:00.000Z:glucose"
}
]
},
{ "index": 10, "key": "bloodGlucose", "tz": null, "output": [] },
{ "index": 10, "key": "bloodGlucose", "tz": "Europe/Berlin", "output": [] },
{
"index": 10,
"key": "bloodGlucose",
"tz": "America/New_York",
"output": []
},
{
"index": 11,
"key": "coreBodyTemperature",
"tz": null,
"output": [
{
"type": "BODY_TEMPERATURE",
"value": 36.75,
"unit": "celsius",
"measuredAt": "2026-06-06T03:00:00.000Z",
"fieldTag": "2026-06-06T03:00:00.000Z:core_temp"
}
]
},
{
"index": 11,
"key": "coreBodyTemperature",
"tz": "Europe/Berlin",
"output": [
{
"type": "BODY_TEMPERATURE",
"value": 36.75,
"unit": "celsius",
"measuredAt": "2026-06-06T01:00:00.000Z",
"fieldTag": "2026-06-06T03:00:00.000Z:core_temp"
}
]
},
{
"index": 11,
"key": "coreBodyTemperature",
"tz": "America/New_York",
"output": [
{
"type": "BODY_TEMPERATURE",
"value": 36.75,
"unit": "celsius",
"measuredAt": "2026-06-06T07:00:00.000Z",
"fieldTag": "2026-06-06T03:00:00.000Z:core_temp"
}
]
},
{
"index": 12,
"key": "height",
"tz": null,
"output": { "cm": 180.5, "sampledAt": "2026-01-15T10:00:00.000Z" }
},
{
"index": 12,
"key": "height",
"tz": "Europe/Berlin",
"output": { "cm": 180.5, "sampledAt": "2026-01-15T10:00:00.000Z" }
},
{
"index": 12,
"key": "height",
"tz": "America/New_York",
"output": { "cm": 180.5, "sampledAt": "2026-01-15T10:00:00.000Z" }
},
{
"index": 13,
"key": "height",
"tz": null,
"output": { "cm": 179.8, "sampledAt": null }
},
{
"index": 13,
"key": "height",
"tz": "Europe/Berlin",
"output": { "cm": 179.8, "sampledAt": null }
},
{
"index": 13,
"key": "height",
"tz": "America/New_York",
"output": { "cm": 179.8, "sampledAt": null }
},
{ "index": 14, "key": "height", "tz": null, "output": null },
{ "index": 14, "key": "height", "tz": "Europe/Berlin", "output": null },
{ "index": 14, "key": "height", "tz": "America/New_York", "output": null }
],
"sampleInstants": [
{
"index": 0,
"dataType": "weight",
"tz": null,
"instant": "2026-06-02T06:15:00.000Z"
},
{
"index": 0,
"dataType": "weight",
"tz": "Europe/Berlin",
"instant": "2026-06-02T06:15:00.000Z"
},
{
"index": 0,
"dataType": "weight",
"tz": "America/New_York",
"instant": "2026-06-02T06:15:00.000Z"
},
{
"index": 1,
"dataType": "weight",
"tz": null,
"instant": "2026-06-03T05:30:00.000Z"
},
{
"index": 1,
"dataType": "weight",
"tz": "Europe/Berlin",
"instant": "2026-06-03T05:30:00.000Z"
},
{
"index": 1,
"dataType": "weight",
"tz": "America/New_York",
"instant": "2026-06-03T05:30:00.000Z"
},
{
"index": 2,
"dataType": "weight",
"tz": null,
"instant": "2026-06-04T23:45:00.000Z"
},
{
"index": 2,
"dataType": "weight",
"tz": "Europe/Berlin",
"instant": "2026-06-04T21:45:00.000Z"
},
{
"index": 2,
"dataType": "weight",
"tz": "America/New_York",
"instant": "2026-06-05T03:45:00.000Z"
},
{
"index": 3,
"dataType": "weight",
"tz": null,
"instant": "2026-06-04T23:45:00.000Z"
},
{
"index": 3,
"dataType": "weight",
"tz": "Europe/Berlin",
"instant": "2026-06-04T21:45:00.000Z"
},
{
"index": 3,
"dataType": "weight",
"tz": "America/New_York",
"instant": "2026-06-05T03:45:00.000Z"
},
{
"index": 4,
"dataType": "bodyFat",
"tz": null,
"instant": "2026-06-02T06:15:00.000Z"
},
{
"index": 4,
"dataType": "bodyFat",
"tz": "Europe/Berlin",
"instant": "2026-06-02T06:15:00.000Z"
},
{
"index": 4,
"dataType": "bodyFat",
"tz": "America/New_York",
"instant": "2026-06-02T06:15:00.000Z"
},
{ "index": 5, "dataType": "bodyFat", "tz": null, "instant": null },
{
"index": 5,
"dataType": "bodyFat",
"tz": "Europe/Berlin",
"instant": null
},
{
"index": 5,
"dataType": "bodyFat",
"tz": "America/New_York",
"instant": null
},
{
"index": 6,
"dataType": "heartRate",
"tz": null,
"instant": "2026-06-02T16:00:30.000Z"
},
{
"index": 6,
"dataType": "heartRate",
"tz": "Europe/Berlin",
"instant": "2026-06-02T16:00:30.000Z"
},
{
"index": 6,
"dataType": "heartRate",
"tz": "America/New_York",
"instant": "2026-06-02T16:00:30.000Z"
},
{
"index": 7,
"dataType": "heartRate",
"tz": null,
"instant": "2026-06-02T00:10:00.000Z"
},
{
"index": 7,
"dataType": "heartRate",
"tz": "Europe/Berlin",
"instant": "2026-06-01T22:10:00.000Z"
},
{
"index": 7,
"dataType": "heartRate",
"tz": "America/New_York",
"instant": "2026-06-02T04:10:00.000Z"
},
{ "index": 8, "dataType": "heartRate", "tz": null, "instant": null },
{
"index": 8,
"dataType": "heartRate",
"tz": "Europe/Berlin",
"instant": null
},
{
"index": 8,
"dataType": "heartRate",
"tz": "America/New_York",
"instant": null
},
{
"index": 9,
"dataType": "bloodGlucose",
"tz": null,
"instant": "2026-06-05T08:00:00.000Z"
},
{
"index": 9,
"dataType": "bloodGlucose",
"tz": "Europe/Berlin",
"instant": "2026-06-05T08:00:00.000Z"
},
{
"index": 9,
"dataType": "bloodGlucose",
"tz": "America/New_York",
"instant": "2026-06-05T08:00:00.000Z"
},
{
"index": 10,
"dataType": "bloodGlucose",
"tz": null,
"instant": "2026-06-05T09:00:00.000Z"
},
{
"index": 10,
"dataType": "bloodGlucose",
"tz": "Europe/Berlin",
"instant": "2026-06-05T09:00:00.000Z"
},
{
"index": 10,
"dataType": "bloodGlucose",
"tz": "America/New_York",
"instant": "2026-06-05T09:00:00.000Z"
},
{
"index": 11,
"dataType": "coreBodyTemperature",
"tz": null,
"instant": "2026-06-06T03:00:00.000Z"
},
{
"index": 11,
"dataType": "coreBodyTemperature",
"tz": "Europe/Berlin",
"instant": "2026-06-06T01:00:00.000Z"
},
{
"index": 11,
"dataType": "coreBodyTemperature",
"tz": "America/New_York",
"instant": "2026-06-06T07:00:00.000Z"
},
{
"index": 12,
"dataType": "height",
"tz": null,
"instant": "2026-01-15T10:00:00.000Z"
},
{
"index": 12,
"dataType": "height",
"tz": "Europe/Berlin",
"instant": "2026-01-15T10:00:00.000Z"
},
{
"index": 12,
"dataType": "height",
"tz": "America/New_York",
"instant": "2026-01-15T10:00:00.000Z"
},
{ "index": 13, "dataType": "height", "tz": null, "instant": null },
{
"index": 13,
"dataType": "height",
"tz": "Europe/Berlin",
"instant": null
},
{
"index": 13,
"dataType": "height",
"tz": "America/New_York",
"instant": null
},
{
"index": 14,
"dataType": "height",
"tz": null,
"instant": "2026-01-16T10:00:00.000Z"
},
{
"index": 14,
"dataType": "height",
"tz": "Europe/Berlin",
"instant": "2026-01-16T10:00:00.000Z"
},
{
"index": 14,
"dataType": "height",
"tz": "America/New_York",
"instant": "2026-01-16T10:00:00.000Z"
}
],
"sampleInstantOfDailyType": null
}
@@ -0,0 +1,758 @@
[
{
"index": 0,
"tz": null,
"detailed": {
"anchor": "users/me/dataTypes/sleep/dataPoints/sleep-0001",
"windowStart": "2026-06-01T22:30:00.000Z",
"windowEnd": "2026-06-02T06:10:00.000Z",
"rows": [
{
"type": "SLEEP_DURATION",
"value": 15,
"unit": "minutes",
"measuredAt": "2026-06-01T22:45:00.000Z",
"fieldTag": "users/me/dataTypes/sleep/dataPoints/sleep-0001:sleep:2026-06-01T22:30:00.000Z",
"sleepStage": "AWAKE"
},
{
"type": "SLEEP_DURATION",
"value": 90,
"unit": "minutes",
"measuredAt": "2026-06-02T00:15:00.000Z",
"fieldTag": "users/me/dataTypes/sleep/dataPoints/sleep-0001:sleep:2026-06-01T22:45:00.000Z",
"sleepStage": "CORE"
},
{
"type": "SLEEP_DURATION",
"value": 65.5,
"unit": "minutes",
"measuredAt": "2026-06-02T01:20:30.000Z",
"fieldTag": "users/me/dataTypes/sleep/dataPoints/sleep-0001:sleep:2026-06-02T00:15:00.000Z",
"sleepStage": "DEEP"
},
{
"type": "SLEEP_DURATION",
"value": 44.5,
"unit": "minutes",
"measuredAt": "2026-06-02T02:05:00.000Z",
"fieldTag": "users/me/dataTypes/sleep/dataPoints/sleep-0001:sleep:2026-06-02T01:20:30.000Z",
"sleepStage": "REM"
},
{
"type": "SLEEP_DURATION",
"value": 245,
"unit": "minutes",
"measuredAt": "2026-06-02T06:10:00.000Z",
"fieldTag": "users/me/dataTypes/sleep/dataPoints/sleep-0001:sleep:2026-06-02T02:05:00.000Z",
"sleepStage": "CORE"
}
],
"unknownStageSegments": 0,
"unknownStageLabels": []
},
"rows": [
{
"type": "SLEEP_DURATION",
"value": 15,
"unit": "minutes",
"measuredAt": "2026-06-01T22:45:00.000Z",
"fieldTag": "users/me/dataTypes/sleep/dataPoints/sleep-0001:sleep:2026-06-01T22:30:00.000Z",
"sleepStage": "AWAKE"
},
{
"type": "SLEEP_DURATION",
"value": 90,
"unit": "minutes",
"measuredAt": "2026-06-02T00:15:00.000Z",
"fieldTag": "users/me/dataTypes/sleep/dataPoints/sleep-0001:sleep:2026-06-01T22:45:00.000Z",
"sleepStage": "CORE"
},
{
"type": "SLEEP_DURATION",
"value": 65.5,
"unit": "minutes",
"measuredAt": "2026-06-02T01:20:30.000Z",
"fieldTag": "users/me/dataTypes/sleep/dataPoints/sleep-0001:sleep:2026-06-02T00:15:00.000Z",
"sleepStage": "DEEP"
},
{
"type": "SLEEP_DURATION",
"value": 44.5,
"unit": "minutes",
"measuredAt": "2026-06-02T02:05:00.000Z",
"fieldTag": "users/me/dataTypes/sleep/dataPoints/sleep-0001:sleep:2026-06-02T01:20:30.000Z",
"sleepStage": "REM"
},
{
"type": "SLEEP_DURATION",
"value": 245,
"unit": "minutes",
"measuredAt": "2026-06-02T06:10:00.000Z",
"fieldTag": "users/me/dataTypes/sleep/dataPoints/sleep-0001:sleep:2026-06-02T02:05:00.000Z",
"sleepStage": "CORE"
}
]
},
{
"index": 0,
"tz": "Europe/Berlin",
"detailed": {
"anchor": "users/me/dataTypes/sleep/dataPoints/sleep-0001",
"windowStart": "2026-06-01T22:30:00.000Z",
"windowEnd": "2026-06-02T06:10:00.000Z",
"rows": [
{
"type": "SLEEP_DURATION",
"value": 15,
"unit": "minutes",
"measuredAt": "2026-06-01T22:45:00.000Z",
"fieldTag": "users/me/dataTypes/sleep/dataPoints/sleep-0001:sleep:2026-06-01T22:30:00.000Z",
"sleepStage": "AWAKE"
},
{
"type": "SLEEP_DURATION",
"value": 90,
"unit": "minutes",
"measuredAt": "2026-06-02T00:15:00.000Z",
"fieldTag": "users/me/dataTypes/sleep/dataPoints/sleep-0001:sleep:2026-06-01T22:45:00.000Z",
"sleepStage": "CORE"
},
{
"type": "SLEEP_DURATION",
"value": 65.5,
"unit": "minutes",
"measuredAt": "2026-06-02T01:20:30.000Z",
"fieldTag": "users/me/dataTypes/sleep/dataPoints/sleep-0001:sleep:2026-06-02T00:15:00.000Z",
"sleepStage": "DEEP"
},
{
"type": "SLEEP_DURATION",
"value": 44.5,
"unit": "minutes",
"measuredAt": "2026-06-02T02:05:00.000Z",
"fieldTag": "users/me/dataTypes/sleep/dataPoints/sleep-0001:sleep:2026-06-02T01:20:30.000Z",
"sleepStage": "REM"
},
{
"type": "SLEEP_DURATION",
"value": 245,
"unit": "minutes",
"measuredAt": "2026-06-02T06:10:00.000Z",
"fieldTag": "users/me/dataTypes/sleep/dataPoints/sleep-0001:sleep:2026-06-02T02:05:00.000Z",
"sleepStage": "CORE"
}
],
"unknownStageSegments": 0,
"unknownStageLabels": []
},
"rows": [
{
"type": "SLEEP_DURATION",
"value": 15,
"unit": "minutes",
"measuredAt": "2026-06-01T22:45:00.000Z",
"fieldTag": "users/me/dataTypes/sleep/dataPoints/sleep-0001:sleep:2026-06-01T22:30:00.000Z",
"sleepStage": "AWAKE"
},
{
"type": "SLEEP_DURATION",
"value": 90,
"unit": "minutes",
"measuredAt": "2026-06-02T00:15:00.000Z",
"fieldTag": "users/me/dataTypes/sleep/dataPoints/sleep-0001:sleep:2026-06-01T22:45:00.000Z",
"sleepStage": "CORE"
},
{
"type": "SLEEP_DURATION",
"value": 65.5,
"unit": "minutes",
"measuredAt": "2026-06-02T01:20:30.000Z",
"fieldTag": "users/me/dataTypes/sleep/dataPoints/sleep-0001:sleep:2026-06-02T00:15:00.000Z",
"sleepStage": "DEEP"
},
{
"type": "SLEEP_DURATION",
"value": 44.5,
"unit": "minutes",
"measuredAt": "2026-06-02T02:05:00.000Z",
"fieldTag": "users/me/dataTypes/sleep/dataPoints/sleep-0001:sleep:2026-06-02T01:20:30.000Z",
"sleepStage": "REM"
},
{
"type": "SLEEP_DURATION",
"value": 245,
"unit": "minutes",
"measuredAt": "2026-06-02T06:10:00.000Z",
"fieldTag": "users/me/dataTypes/sleep/dataPoints/sleep-0001:sleep:2026-06-02T02:05:00.000Z",
"sleepStage": "CORE"
}
]
},
{
"index": 0,
"tz": "America/New_York",
"detailed": {
"anchor": "users/me/dataTypes/sleep/dataPoints/sleep-0001",
"windowStart": "2026-06-01T22:30:00.000Z",
"windowEnd": "2026-06-02T06:10:00.000Z",
"rows": [
{
"type": "SLEEP_DURATION",
"value": 15,
"unit": "minutes",
"measuredAt": "2026-06-01T22:45:00.000Z",
"fieldTag": "users/me/dataTypes/sleep/dataPoints/sleep-0001:sleep:2026-06-01T22:30:00.000Z",
"sleepStage": "AWAKE"
},
{
"type": "SLEEP_DURATION",
"value": 90,
"unit": "minutes",
"measuredAt": "2026-06-02T00:15:00.000Z",
"fieldTag": "users/me/dataTypes/sleep/dataPoints/sleep-0001:sleep:2026-06-01T22:45:00.000Z",
"sleepStage": "CORE"
},
{
"type": "SLEEP_DURATION",
"value": 65.5,
"unit": "minutes",
"measuredAt": "2026-06-02T01:20:30.000Z",
"fieldTag": "users/me/dataTypes/sleep/dataPoints/sleep-0001:sleep:2026-06-02T00:15:00.000Z",
"sleepStage": "DEEP"
},
{
"type": "SLEEP_DURATION",
"value": 44.5,
"unit": "minutes",
"measuredAt": "2026-06-02T02:05:00.000Z",
"fieldTag": "users/me/dataTypes/sleep/dataPoints/sleep-0001:sleep:2026-06-02T01:20:30.000Z",
"sleepStage": "REM"
},
{
"type": "SLEEP_DURATION",
"value": 245,
"unit": "minutes",
"measuredAt": "2026-06-02T06:10:00.000Z",
"fieldTag": "users/me/dataTypes/sleep/dataPoints/sleep-0001:sleep:2026-06-02T02:05:00.000Z",
"sleepStage": "CORE"
}
],
"unknownStageSegments": 0,
"unknownStageLabels": []
},
"rows": [
{
"type": "SLEEP_DURATION",
"value": 15,
"unit": "minutes",
"measuredAt": "2026-06-01T22:45:00.000Z",
"fieldTag": "users/me/dataTypes/sleep/dataPoints/sleep-0001:sleep:2026-06-01T22:30:00.000Z",
"sleepStage": "AWAKE"
},
{
"type": "SLEEP_DURATION",
"value": 90,
"unit": "minutes",
"measuredAt": "2026-06-02T00:15:00.000Z",
"fieldTag": "users/me/dataTypes/sleep/dataPoints/sleep-0001:sleep:2026-06-01T22:45:00.000Z",
"sleepStage": "CORE"
},
{
"type": "SLEEP_DURATION",
"value": 65.5,
"unit": "minutes",
"measuredAt": "2026-06-02T01:20:30.000Z",
"fieldTag": "users/me/dataTypes/sleep/dataPoints/sleep-0001:sleep:2026-06-02T00:15:00.000Z",
"sleepStage": "DEEP"
},
{
"type": "SLEEP_DURATION",
"value": 44.5,
"unit": "minutes",
"measuredAt": "2026-06-02T02:05:00.000Z",
"fieldTag": "users/me/dataTypes/sleep/dataPoints/sleep-0001:sleep:2026-06-02T01:20:30.000Z",
"sleepStage": "REM"
},
{
"type": "SLEEP_DURATION",
"value": 245,
"unit": "minutes",
"measuredAt": "2026-06-02T06:10:00.000Z",
"fieldTag": "users/me/dataTypes/sleep/dataPoints/sleep-0001:sleep:2026-06-02T02:05:00.000Z",
"sleepStage": "CORE"
}
]
},
{
"index": 1,
"tz": null,
"detailed": {
"anchor": "users/me/dataTypes/sleep/dataPoints/sleep-0002",
"windowStart": "2026-06-02T23:50:00.000Z",
"windowEnd": "2026-06-03T07:00:00.000Z",
"rows": [
{
"type": "SLEEP_DURATION",
"value": 30,
"unit": "minutes",
"measuredAt": "2026-06-03T00:20:00.000Z",
"fieldTag": "users/me/dataTypes/sleep/dataPoints/sleep-0002:sleep:2026-06-02T23:50:00.000Z",
"sleepStage": "ASLEEP"
},
{
"type": "SLEEP_DURATION",
"value": 5,
"unit": "minutes",
"measuredAt": "2026-06-03T00:25:00.000Z",
"fieldTag": "users/me/dataTypes/sleep/dataPoints/sleep-0002:sleep:2026-06-03T00:20:00.000Z",
"sleepStage": "AWAKE"
},
{
"type": "SLEEP_DURATION",
"value": 395,
"unit": "minutes",
"measuredAt": "2026-06-03T07:00:00.000Z",
"fieldTag": "users/me/dataTypes/sleep/dataPoints/sleep-0002:sleep:2026-06-03T00:25:00.000Z",
"sleepStage": "ASLEEP"
}
],
"unknownStageSegments": 0,
"unknownStageLabels": []
},
"rows": [
{
"type": "SLEEP_DURATION",
"value": 30,
"unit": "minutes",
"measuredAt": "2026-06-03T00:20:00.000Z",
"fieldTag": "users/me/dataTypes/sleep/dataPoints/sleep-0002:sleep:2026-06-02T23:50:00.000Z",
"sleepStage": "ASLEEP"
},
{
"type": "SLEEP_DURATION",
"value": 5,
"unit": "minutes",
"measuredAt": "2026-06-03T00:25:00.000Z",
"fieldTag": "users/me/dataTypes/sleep/dataPoints/sleep-0002:sleep:2026-06-03T00:20:00.000Z",
"sleepStage": "AWAKE"
},
{
"type": "SLEEP_DURATION",
"value": 395,
"unit": "minutes",
"measuredAt": "2026-06-03T07:00:00.000Z",
"fieldTag": "users/me/dataTypes/sleep/dataPoints/sleep-0002:sleep:2026-06-03T00:25:00.000Z",
"sleepStage": "ASLEEP"
}
]
},
{
"index": 1,
"tz": "Europe/Berlin",
"detailed": {
"anchor": "users/me/dataTypes/sleep/dataPoints/sleep-0002",
"windowStart": "2026-06-02T21:50:00.000Z",
"windowEnd": "2026-06-03T05:00:00.000Z",
"rows": [
{
"type": "SLEEP_DURATION",
"value": 30,
"unit": "minutes",
"measuredAt": "2026-06-02T22:20:00.000Z",
"fieldTag": "users/me/dataTypes/sleep/dataPoints/sleep-0002:sleep:2026-06-02T21:50:00.000Z",
"sleepStage": "ASLEEP"
},
{
"type": "SLEEP_DURATION",
"value": 5,
"unit": "minutes",
"measuredAt": "2026-06-02T22:25:00.000Z",
"fieldTag": "users/me/dataTypes/sleep/dataPoints/sleep-0002:sleep:2026-06-02T22:20:00.000Z",
"sleepStage": "AWAKE"
},
{
"type": "SLEEP_DURATION",
"value": 395,
"unit": "minutes",
"measuredAt": "2026-06-03T05:00:00.000Z",
"fieldTag": "users/me/dataTypes/sleep/dataPoints/sleep-0002:sleep:2026-06-02T22:25:00.000Z",
"sleepStage": "ASLEEP"
}
],
"unknownStageSegments": 0,
"unknownStageLabels": []
},
"rows": [
{
"type": "SLEEP_DURATION",
"value": 30,
"unit": "minutes",
"measuredAt": "2026-06-02T22:20:00.000Z",
"fieldTag": "users/me/dataTypes/sleep/dataPoints/sleep-0002:sleep:2026-06-02T21:50:00.000Z",
"sleepStage": "ASLEEP"
},
{
"type": "SLEEP_DURATION",
"value": 5,
"unit": "minutes",
"measuredAt": "2026-06-02T22:25:00.000Z",
"fieldTag": "users/me/dataTypes/sleep/dataPoints/sleep-0002:sleep:2026-06-02T22:20:00.000Z",
"sleepStage": "AWAKE"
},
{
"type": "SLEEP_DURATION",
"value": 395,
"unit": "minutes",
"measuredAt": "2026-06-03T05:00:00.000Z",
"fieldTag": "users/me/dataTypes/sleep/dataPoints/sleep-0002:sleep:2026-06-02T22:25:00.000Z",
"sleepStage": "ASLEEP"
}
]
},
{
"index": 1,
"tz": "America/New_York",
"detailed": {
"anchor": "users/me/dataTypes/sleep/dataPoints/sleep-0002",
"windowStart": "2026-06-03T03:50:00.000Z",
"windowEnd": "2026-06-03T11:00:00.000Z",
"rows": [
{
"type": "SLEEP_DURATION",
"value": 30,
"unit": "minutes",
"measuredAt": "2026-06-03T04:20:00.000Z",
"fieldTag": "users/me/dataTypes/sleep/dataPoints/sleep-0002:sleep:2026-06-03T03:50:00.000Z",
"sleepStage": "ASLEEP"
},
{
"type": "SLEEP_DURATION",
"value": 5,
"unit": "minutes",
"measuredAt": "2026-06-03T04:25:00.000Z",
"fieldTag": "users/me/dataTypes/sleep/dataPoints/sleep-0002:sleep:2026-06-03T04:20:00.000Z",
"sleepStage": "AWAKE"
},
{
"type": "SLEEP_DURATION",
"value": 395,
"unit": "minutes",
"measuredAt": "2026-06-03T11:00:00.000Z",
"fieldTag": "users/me/dataTypes/sleep/dataPoints/sleep-0002:sleep:2026-06-03T04:25:00.000Z",
"sleepStage": "ASLEEP"
}
],
"unknownStageSegments": 0,
"unknownStageLabels": []
},
"rows": [
{
"type": "SLEEP_DURATION",
"value": 30,
"unit": "minutes",
"measuredAt": "2026-06-03T04:20:00.000Z",
"fieldTag": "users/me/dataTypes/sleep/dataPoints/sleep-0002:sleep:2026-06-03T03:50:00.000Z",
"sleepStage": "ASLEEP"
},
{
"type": "SLEEP_DURATION",
"value": 5,
"unit": "minutes",
"measuredAt": "2026-06-03T04:25:00.000Z",
"fieldTag": "users/me/dataTypes/sleep/dataPoints/sleep-0002:sleep:2026-06-03T04:20:00.000Z",
"sleepStage": "AWAKE"
},
{
"type": "SLEEP_DURATION",
"value": 395,
"unit": "minutes",
"measuredAt": "2026-06-03T11:00:00.000Z",
"fieldTag": "users/me/dataTypes/sleep/dataPoints/sleep-0002:sleep:2026-06-03T04:25:00.000Z",
"sleepStage": "ASLEEP"
}
]
},
{
"index": 2,
"tz": null,
"detailed": {
"anchor": "2026-06-05T03:30:00.000Z",
"windowStart": "2026-06-04T20:00:00.000Z",
"windowEnd": "2026-06-04T23:00:00.000Z",
"rows": [
{
"type": "SLEEP_DURATION",
"value": 180,
"unit": "minutes",
"measuredAt": "2026-06-04T23:00:00.000Z",
"fieldTag": "2026-06-05T03:30:00.000Z:sleep:2026-06-04T20:00:00.000Z",
"sleepStage": "DEEP"
}
],
"unknownStageSegments": 1,
"unknownStageLabels": ["SLEEP_STAGE_TYPE_UNSPECIFIED"]
},
"rows": [
{
"type": "SLEEP_DURATION",
"value": 180,
"unit": "minutes",
"measuredAt": "2026-06-04T23:00:00.000Z",
"fieldTag": "2026-06-05T03:30:00.000Z:sleep:2026-06-04T20:00:00.000Z",
"sleepStage": "DEEP"
}
]
},
{
"index": 2,
"tz": "Europe/Berlin",
"detailed": {
"anchor": "2026-06-05T03:30:00.000Z",
"windowStart": "2026-06-04T20:00:00.000Z",
"windowEnd": "2026-06-04T23:00:00.000Z",
"rows": [
{
"type": "SLEEP_DURATION",
"value": 180,
"unit": "minutes",
"measuredAt": "2026-06-04T23:00:00.000Z",
"fieldTag": "2026-06-05T03:30:00.000Z:sleep:2026-06-04T20:00:00.000Z",
"sleepStage": "DEEP"
}
],
"unknownStageSegments": 1,
"unknownStageLabels": ["SLEEP_STAGE_TYPE_UNSPECIFIED"]
},
"rows": [
{
"type": "SLEEP_DURATION",
"value": 180,
"unit": "minutes",
"measuredAt": "2026-06-04T23:00:00.000Z",
"fieldTag": "2026-06-05T03:30:00.000Z:sleep:2026-06-04T20:00:00.000Z",
"sleepStage": "DEEP"
}
]
},
{
"index": 2,
"tz": "America/New_York",
"detailed": {
"anchor": "2026-06-05T03:30:00.000Z",
"windowStart": "2026-06-04T20:00:00.000Z",
"windowEnd": "2026-06-04T23:00:00.000Z",
"rows": [
{
"type": "SLEEP_DURATION",
"value": 180,
"unit": "minutes",
"measuredAt": "2026-06-04T23:00:00.000Z",
"fieldTag": "2026-06-05T03:30:00.000Z:sleep:2026-06-04T20:00:00.000Z",
"sleepStage": "DEEP"
}
],
"unknownStageSegments": 1,
"unknownStageLabels": ["SLEEP_STAGE_TYPE_UNSPECIFIED"]
},
"rows": [
{
"type": "SLEEP_DURATION",
"value": 180,
"unit": "minutes",
"measuredAt": "2026-06-04T23:00:00.000Z",
"fieldTag": "2026-06-05T03:30:00.000Z:sleep:2026-06-04T20:00:00.000Z",
"sleepStage": "DEEP"
}
]
},
{
"index": 3,
"tz": null,
"detailed": {
"anchor": "2026-06-06T23:00:00.000Z",
"windowStart": null,
"windowEnd": null,
"rows": [],
"unknownStageSegments": 0,
"unknownStageLabels": []
},
"rows": []
},
{
"index": 3,
"tz": "Europe/Berlin",
"detailed": {
"anchor": "2026-06-06T23:00:00.000Z",
"windowStart": null,
"windowEnd": null,
"rows": [],
"unknownStageSegments": 0,
"unknownStageLabels": []
},
"rows": []
},
{
"index": 3,
"tz": "America/New_York",
"detailed": {
"anchor": "2026-06-06T23:00:00.000Z",
"windowStart": null,
"windowEnd": null,
"rows": [],
"unknownStageSegments": 0,
"unknownStageLabels": []
},
"rows": []
},
{
"index": 4,
"tz": null,
"detailed": {
"anchor": "1970-01-01T00:00:00.000Z",
"windowStart": null,
"windowEnd": null,
"rows": [],
"unknownStageSegments": 0,
"unknownStageLabels": []
},
"rows": []
},
{
"index": 4,
"tz": "Europe/Berlin",
"detailed": {
"anchor": "1970-01-01T00:00:00.000Z",
"windowStart": null,
"windowEnd": null,
"rows": [],
"unknownStageSegments": 0,
"unknownStageLabels": []
},
"rows": []
},
{
"index": 4,
"tz": "America/New_York",
"detailed": {
"anchor": "1970-01-01T00:00:00.000Z",
"windowStart": null,
"windowEnd": null,
"rows": [],
"unknownStageSegments": 0,
"unknownStageLabels": []
},
"rows": []
},
{
"index": 5,
"tz": null,
"detailed": {
"anchor": "users/me/dataTypes/sleep/dataPoints/sleep-0006",
"windowStart": "2026-06-08T01:10:00.000Z",
"windowEnd": "2026-06-08T01:40:00.000Z",
"rows": [
{
"type": "SLEEP_DURATION",
"value": 30,
"unit": "minutes",
"measuredAt": "2026-06-08T01:40:00.000Z",
"fieldTag": "users/me/dataTypes/sleep/dataPoints/sleep-0006:sleep:2026-06-08T01:10:00.000Z",
"sleepStage": "CORE"
}
],
"unknownStageSegments": 7,
"unknownStageLabels": [
"NAP_STAGE_ALPHA",
"NAP_STAGE_BETA",
"NAP_STAGE_GAMMA",
"NAP_STAGE_DELTA",
"NAP_STAGE_EPSILON"
]
},
"rows": [
{
"type": "SLEEP_DURATION",
"value": 30,
"unit": "minutes",
"measuredAt": "2026-06-08T01:40:00.000Z",
"fieldTag": "users/me/dataTypes/sleep/dataPoints/sleep-0006:sleep:2026-06-08T01:10:00.000Z",
"sleepStage": "CORE"
}
]
},
{
"index": 5,
"tz": "Europe/Berlin",
"detailed": {
"anchor": "users/me/dataTypes/sleep/dataPoints/sleep-0006",
"windowStart": "2026-06-08T01:10:00.000Z",
"windowEnd": "2026-06-08T01:40:00.000Z",
"rows": [
{
"type": "SLEEP_DURATION",
"value": 30,
"unit": "minutes",
"measuredAt": "2026-06-08T01:40:00.000Z",
"fieldTag": "users/me/dataTypes/sleep/dataPoints/sleep-0006:sleep:2026-06-08T01:10:00.000Z",
"sleepStage": "CORE"
}
],
"unknownStageSegments": 7,
"unknownStageLabels": [
"NAP_STAGE_ALPHA",
"NAP_STAGE_BETA",
"NAP_STAGE_GAMMA",
"NAP_STAGE_DELTA",
"NAP_STAGE_EPSILON"
]
},
"rows": [
{
"type": "SLEEP_DURATION",
"value": 30,
"unit": "minutes",
"measuredAt": "2026-06-08T01:40:00.000Z",
"fieldTag": "users/me/dataTypes/sleep/dataPoints/sleep-0006:sleep:2026-06-08T01:10:00.000Z",
"sleepStage": "CORE"
}
]
},
{
"index": 5,
"tz": "America/New_York",
"detailed": {
"anchor": "users/me/dataTypes/sleep/dataPoints/sleep-0006",
"windowStart": "2026-06-08T01:10:00.000Z",
"windowEnd": "2026-06-08T01:40:00.000Z",
"rows": [
{
"type": "SLEEP_DURATION",
"value": 30,
"unit": "minutes",
"measuredAt": "2026-06-08T01:40:00.000Z",
"fieldTag": "users/me/dataTypes/sleep/dataPoints/sleep-0006:sleep:2026-06-08T01:10:00.000Z",
"sleepStage": "CORE"
}
],
"unknownStageSegments": 7,
"unknownStageLabels": [
"NAP_STAGE_ALPHA",
"NAP_STAGE_BETA",
"NAP_STAGE_GAMMA",
"NAP_STAGE_DELTA",
"NAP_STAGE_EPSILON"
]
},
"rows": [
{
"type": "SLEEP_DURATION",
"value": 30,
"unit": "minutes",
"measuredAt": "2026-06-08T01:40:00.000Z",
"fieldTag": "users/me/dataTypes/sleep/dataPoints/sleep-0006:sleep:2026-06-08T01:10:00.000Z",
"sleepStage": "CORE"
}
]
}
]
@@ -43,12 +43,12 @@ import {
googleHealthBackfillRetryPending,
readGoogleHealthBackfillProgress,
} from "../backfill-progress";
import { forEachDataPointPage } from "@/lib/google-health/data-points";
import {
forEachDataPointPage,
GOOGLE_HEALTH_DATA_TYPES,
noteGoogleHealthDeferred,
runWithGoogleHealthClientOutcome,
} from "../client";
} from "@/lib/google-health/budget";
import { GOOGLE_HEALTH_DATA_TYPES } from "@/lib/google-health/mappers";
const MINUTE = 60_000;
const T0 = Date.parse("2026-10-17T12:00:00Z");
@@ -32,7 +32,7 @@ import {
googleHealthSliceBudget,
runWithGoogleHealthClientOutcome,
runWithGoogleHealthStop,
} from "../client";
} from "@/lib/google-health/budget";
beforeEach(() => {
vi.clearAllMocks();
@@ -18,7 +18,8 @@ vi.mock("@/lib/safe-fetch", async (importOriginal) => {
return { ...actual, safeFetch: safeFetchMock };
});
import { GOOGLE_HEALTH_DATA_TYPES, fetchDataPoints } from "../client";
import { fetchDataPoints } from "@/lib/google-health/data-points";
import { GOOGLE_HEALTH_DATA_TYPES } from "@/lib/google-health/mappers";
import { WideEventBuilder } from "@/lib/logging/event-builder";
import { eventStorage } from "@/lib/logging/context";
import { GoogleHealthApiError } from "../response-classifier";
@@ -32,15 +32,19 @@ vi.mock("@/lib/logging/context", () => ({
}));
import {
GOOGLE_HEALTH_DATA_TYPES,
GOOGLE_HEALTH_SKIP_MS,
type GoogleHealthDataPoint,
forEachDataPointPage,
googleHealthSliceBudget,
refusesEdgeFilter,
} from "@/lib/google-health/data-points";
import {
googleHealthSliceBudget,
runWithGoogleHealthClientOutcome,
runWithGoogleHealthStop,
} from "../client";
} from "@/lib/google-health/budget";
import {
GOOGLE_HEALTH_DATA_TYPES,
type GoogleHealthDataPoint,
} from "@/lib/google-health/mappers";
import { GoogleHealthApiError } from "../response-classifier";
import { GOOGLE_HEALTH_NO_PROGRESS_PAGES } from "../backfill-bounds";
import {
@@ -0,0 +1,462 @@
/**
* What each Google Health mapper makes of a fixed set of payloads, pinned in
* `__golden__/map-*.json`.
*
* The fixtures under `__fixtures__/` are synthetic data points in the
* documented v4 wire shape (spot samples, daily summaries, dailyRollUp
* windows, sleep sessions, exercise sessions), chosen to walk every branch the
* mappers have: int64 values as strings, zeros and negatives, missing or
* unparseable anchors, offset-less local times, unknown stage and sport
* labels. Every mapper that takes a zone runs once without one and once each
* for `Europe/Berlin` and `America/New_York`. The pure request helpers from
* `client.ts` run over small input tables in the test itself.
*
* The goldens were written before the mappers moved out of `client.ts` and
* `mappers.ts` into their own modules, so a green run proves the move changed
* no output. The clock is pinned because a sample without a parseable anchor
* falls back to the current time.
*/
import { format } from "prettier";
import { afterAll, beforeAll, describe, expect, it, vi } from "vitest";
import {
GOOGLE_HEALTH_DATA_TYPES,
googleHealthSampleInstant,
mapActiveEnergy,
mapBloodGlucose,
mapBodyFat,
mapCoreBodyTemperature,
mapDistance,
mapFloors,
mapGoogleHealthSleepStage,
mapGoogleHealthSportType,
mapHeartRate,
mapHeartRateVariability,
mapHeight,
mapOxygenSaturation,
mapRespiratoryRate,
mapRestingHeartRate,
mapSleepSession,
mapSleepSessionDetailed,
mapSteps,
mapVo2Max,
mapWeight,
mapWorkout,
mapWristTemperature,
type GoogleHealthDataPoint,
type GoogleHealthDataType,
type GoogleHealthRollupPoint,
} from "../mappers";
import {
buildDailyRollUpBody,
chunkCivilRange,
civilDateInTz,
resolveGoogleHealthUserId,
} from "../client";
import { refusesEdgeFilter } from "@/lib/google-health/data-points";
import {
extractGoogleApiErrorDetail,
formatCivilBound,
googleHealthRetryDelayMs,
incrementalFilter,
} from "@/lib/google-health/request";
import dailySummariesJson from "./__fixtures__/daily-summaries.json";
import exerciseJson from "./__fixtures__/exercise.json";
import rollupsJson from "./__fixtures__/rollups.json";
import samplesJson from "./__fixtures__/samples.json";
import sleepJson from "./__fixtures__/sleep.json";
/** The golden is written in the repository's JSON format, so the format check passes on it unchanged. */
async function golden(value: unknown): Promise<string> {
return format(JSON.stringify(value), { parser: "json" });
}
/** Captures a thrown error as data, so a refusal is part of the golden too. */
function outcome<T>(fn: () => T): T | { threw: string } {
try {
return fn();
} catch (err) {
return { threw: err instanceof Error ? err.message : String(err) };
}
}
const samples: GoogleHealthDataPoint[] = samplesJson;
const dailySummaries: GoogleHealthDataPoint[] = dailySummariesJson;
const rollups: GoogleHealthRollupPoint[] = rollupsJson;
const sleepSessions: GoogleHealthDataPoint[] = sleepJson;
const exercises: GoogleHealthDataPoint[] = exerciseJson;
const ZONES = [undefined, "Europe/Berlin", "America/New_York"] as const;
type ZonedMapper<P, R> = (point: P, tz?: string) => R;
/** Each fixture point through the mapper for the union key it carries, in every zone. */
function mapByKey<P extends Record<string, unknown>, R>(
points: P[],
mappers: Record<string, ZonedMapper<P, R>>,
): Array<{ index: number; key: string; tz: string | null; output: R }> {
const out: Array<{
index: number;
key: string;
tz: string | null;
output: R;
}> = [];
points.forEach((point, index) => {
for (const [key, mapper] of Object.entries(mappers)) {
if (!(key in point)) continue;
for (const tz of ZONES) {
out.push({ index, key, tz: tz ?? null, output: mapper(point, tz) });
}
}
});
if (out.length !== points.length * ZONES.length) {
throw new Error("a fixture point matched no mapper, or more than one");
}
return out;
}
const FIXED_NOW = new Date("2026-07-01T09:00:00.000Z");
beforeAll(() => {
vi.useFakeTimers({ now: FIXED_NOW, toFake: ["Date"] });
});
afterAll(() => {
vi.useRealTimers();
});
describe("Google Health mapper goldens", () => {
it("maps spot samples", async () => {
const output = {
measurements: mapByKey<GoogleHealthDataPoint, unknown>(samples, {
weight: mapWeight,
bodyFat: mapBodyFat,
heartRate: mapHeartRate,
bloodGlucose: mapBloodGlucose,
coreBodyTemperature: mapCoreBodyTemperature,
height: (point) => mapHeight(point),
}),
sampleInstants: samples.flatMap((point, index) =>
Object.entries(GOOGLE_HEALTH_DATA_TYPES)
.filter(([, dt]) => dt.key in point)
.flatMap(([name, dt]) =>
ZONES.map((tz) => ({
index,
dataType: name,
tz: tz ?? null,
instant: googleHealthSampleInstant(point, dt, tz),
})),
),
),
sampleInstantOfDailyType: googleHealthSampleInstant(
dailySummaries[0]!,
GOOGLE_HEALTH_DATA_TYPES.oxygenSaturation,
),
};
await expect(await golden(output)).toMatchFileSnapshot(
"./__golden__/map-samples.json",
);
});
it("maps daily summaries", async () => {
const output = mapByKey(dailySummaries, {
dailyOxygenSaturation: mapOxygenSaturation,
dailyHeartRateVariability: mapHeartRateVariability,
dailyRestingHeartRate: mapRestingHeartRate,
dailyRespiratoryRate: mapRespiratoryRate,
dailySleepTemperatureDerivations: mapWristTemperature,
dailyVo2Max: mapVo2Max,
});
await expect(await golden(output)).toMatchFileSnapshot(
"./__golden__/map-daily-summaries.json",
);
});
it("maps dailyRollUp windows", async () => {
const output = mapByKey(rollups, {
steps: mapSteps,
distance: mapDistance,
activeEnergyBurned: mapActiveEnergy,
floors: mapFloors,
});
await expect(await golden(output)).toMatchFileSnapshot(
"./__golden__/map-rollups.json",
);
});
it("maps sleep sessions", async () => {
const output = sleepSessions.flatMap((point, index) =>
ZONES.map((tz) => ({
index,
tz: tz ?? null,
detailed: mapSleepSessionDetailed(point, tz),
rows: mapSleepSession(point, tz),
})),
);
await expect(await golden(output)).toMatchFileSnapshot(
"./__golden__/map-sleep.json",
);
});
it("maps exercise sessions", async () => {
const output = exercises.flatMap((point, index) =>
ZONES.map((tz) => ({
index,
tz: tz ?? null,
workout: mapWorkout(point, tz),
})),
);
await expect(await golden(output)).toMatchFileSnapshot(
"./__golden__/map-exercise.json",
);
});
it("resolves sleep stage and sport labels", async () => {
const stageLabels: unknown[] = [
"AWAKE",
"wake",
"LIGHT",
" core ",
"Deep",
"REM",
"ASLEEP",
"RESTLESS",
"in bed",
"IN-BED",
"inbed",
"SLEEP_STAGE_TYPE_UNSPECIFIED",
"",
42,
null,
];
const sportLabels: unknown[] = [
"RUNNING",
"TREADMILL_RUNNING",
"STRENGTH_TRAINING",
"strength training",
"Mountain-Biking",
"STAIR_CLIMBER",
"STAIRCLIMBER",
"HIGH INTENSITY INTERVAL TRAINING",
"FOOTBALL",
"PILATES",
"OTHER_WORKOUT",
"KITESURFING",
"",
" ",
null,
7,
];
const output = {
sleepStages: stageLabels.map((raw) => ({
raw,
stage: mapGoogleHealthSleepStage(raw),
})),
sportTypes: sportLabels.map((raw) => ({
raw,
sportType: mapGoogleHealthSportType(raw),
})),
};
await expect(await golden(output)).toMatchFileSnapshot(
"./__golden__/map-labels.json",
);
});
it("builds request values with the pure client helpers", async () => {
const instants = [
new Date("2026-06-01T21:59:59.000Z"),
new Date("2026-06-01T22:00:00.000Z"),
new Date("2026-03-29T00:30:00.000Z"),
new Date("2026-12-31T23:30:15.500Z"),
];
const listTypes: GoogleHealthDataType[] = [
GOOGLE_HEALTH_DATA_TYPES.weight,
GOOGLE_HEALTH_DATA_TYPES.heartRateVariability,
GOOGLE_HEALTH_DATA_TYPES.sleep,
GOOGLE_HEALTH_DATA_TYPES.exercise,
GOOGLE_HEALTH_DATA_TYPES.steps,
];
const errorBodies: unknown[] = [
{
error: {
code: 400,
status: "INVALID_ARGUMENT",
message:
"Invalid filter for https://health.googleapis.com/v4/users/me/dataTypes/weight/dataPoints?filter=weight.date",
details: [
{
"@type": "type.googleapis.com/google.rpc.BadRequest",
fieldViolations: [
{ field: "filter", description: "Unknown field weight.date" },
{ field: "page_size" },
{ description: "Authorization: Bearer abc.def.ghi rejected" },
{ field: "fourth", description: "dropped past the third" },
],
},
],
},
},
{ error: { status: "RESOURCE_EXHAUSTED" } },
{ error: { message: "x".repeat(260) } },
{ error: { details: [null, { fieldViolations: [{}, 5] }] } },
{ error: "invalid_grant" },
{ error: {} },
null,
"plain text",
];
const retryCases: Array<{
headers: Record<string, string>;
body: unknown;
}> = [
{ headers: { "retry-after": "30" }, body: null },
{ headers: { "retry-after": "1.5" }, body: null },
{
headers: { "retry-after": "Wed, 01 Jul 2026 09:00:45 GMT" },
body: null,
},
{ headers: { "retry-after": "-3" }, body: null },
{
headers: {},
body: {
error: {
details: [
{ "@type": "type.googleapis.com/google.rpc.ErrorInfo" },
{
"@type": "type.googleapis.com/google.rpc.RetryInfo",
retryDelay: "12.5s",
},
],
},
},
},
{
headers: {},
body: { error: { details: [{ retryDelay: "soon" }] } },
},
{ headers: {}, body: null },
];
const output = {
resolveGoogleHealthUserId: [
{ name: "users/1234567890" },
{ name: "users/", id: "id-0002" },
{ name: "plain-0003" },
{ id: "id-0004" },
{},
].map((profile) => ({
profile,
userId: resolveGoogleHealthUserId(profile),
})),
formatCivilBound: instants.flatMap((instant) =>
ZONES.map((tz) => ({
instant,
tz: tz ?? null,
bound: formatCivilBound(instant, tz),
})),
),
civilDateInTz: instants.flatMap((instant) =>
ZONES.map((tz) => ({
instant,
tz: tz ?? null,
date: civilDateInTz(instant, tz),
})),
),
incrementalFilter: listTypes.flatMap((dataType) =>
(["camel", "snake"] as const).flatMap((style) =>
ZONES.map((tz) => ({
dataType: dataType.path,
style,
tz: tz ?? null,
filter: outcome(() =>
incrementalFilter(dataType, instants[1]!, tz, style),
),
})),
),
),
incrementalFilterDefaultStyle: incrementalFilter(
GOOGLE_HEALTH_DATA_TYPES.restingHeartRate,
instants[0]!,
),
chunkCivilRange: [
{
start: { year: 2026, month: 1, day: 1 },
end: { year: 2026, month: 7, day: 1 },
},
{
start: { year: 2026, month: 2, day: 20 },
end: { year: 2026, month: 3, day: 10 },
maxDays: 14,
},
{
start: { year: 2026, month: 6, day: 1 },
end: { year: 2026, month: 6, day: 1 },
},
{
start: { year: 2026, month: 6, day: 2 },
end: { year: 2026, month: 6, day: 1 },
},
].map(({ start, end, maxDays }) => ({
start,
end,
maxDays: maxDays ?? null,
chunks: chunkCivilRange(start, end, maxDays),
})),
buildDailyRollUpBody: [
{
chunk: {
start: { year: 2026, month: 1, day: 1 },
end: { year: 2026, month: 4, day: 1 },
},
},
{
chunk: {
start: { year: 2026, month: 2, day: 20 },
end: { year: 2026, month: 3, day: 1 },
},
pageToken: "page-token-0002",
},
{
chunk: {
start: { year: 2026, month: 12, day: 31 },
end: { year: 2027, month: 1, day: 1 },
},
pageToken: "",
},
].map(({ chunk, pageToken }) => ({
chunk,
pageToken: pageToken ?? null,
body: buildDailyRollUpBody(chunk, pageToken),
})),
extractGoogleApiErrorDetail: errorBodies.map((body) => ({
body,
detail: extractGoogleApiErrorDetail(body) ?? null,
})),
refusesEdgeFilter: [
undefined,
"INVALID_ARGUMENT",
"INVALID_ARGUMENT: Invalid filter",
"INVALID_ARGUMENT: Invalid page token",
"INVALID_ARGUMENT: page_size out of range",
"INVALID_ARGUMENT: pageSize too large",
"UNAUTHENTICATED: access token expired",
"FAILED_PRECONDITION: something else",
"Bad filter and bad token",
].map((detail) => ({
detail: detail ?? null,
refuses: refusesEdgeFilter(detail),
})),
googleHealthRetryDelayMs: retryCases.map(({ headers, body }) => ({
headers,
body,
delayMs:
googleHealthRetryDelayMs(
new Response(null, { status: 429, headers }),
body,
FIXED_NOW.getTime(),
) ?? null,
})),
};
await expect(await golden(output)).toMatchFileSnapshot(
"./__golden__/map-client-helpers.json",
);
});
});
@@ -14,10 +14,13 @@
*/
import { describe, expect, it } from "vitest";
import { chunkCivilRange } from "../client";
import { mapVo2Max } from "@/lib/google-health/mappers";
import {
chunkCivilRange,
formatCivilBound,
incrementalFilter,
} from "@/lib/google-health/request";
import {
GOOGLE_HEALTH_DATA_TYPES,
mapActiveEnergy,
mapBloodGlucose,
@@ -36,11 +39,10 @@ import {
mapSleepSession,
mapSleepSessionDetailed,
mapSteps,
mapVo2Max,
mapWeight,
mapWorkout,
mapWristTemperature,
} from "../client";
} from "@/lib/google-health/mappers";
describe("mapGoogleHealthSportType", () => {
it("keeps Badminton as a first-class workout sport", () => {
@@ -21,12 +21,12 @@ vi.mock("@/lib/safe-fetch", async (importOriginal) => {
return { ...actual, safeFetch: safeFetchMock };
});
import { fetchDataPoints } from "@/lib/google-health/data-points";
import {
GOOGLE_HEALTH_DATA_TYPES,
GOOGLE_HEALTH_RATE_LIMIT,
fetchDataPoints,
googleHealthRetryDelayMs,
} from "../client";
} from "@/lib/google-health/request";
import { GOOGLE_HEALTH_DATA_TYPES } from "@/lib/google-health/mappers";
import { collapseNaturalKeyTwins } from "../sync-core";
import { GoogleHealthApiError } from "../response-classifier";
@@ -20,15 +20,15 @@ vi.mock("@/lib/safe-fetch", async (importOriginal) => {
});
import {
GOOGLE_HEALTH_DATA_TYPES,
GOOGLE_HEALTH_ROLLUP_FALLBACK_RANGE_DAYS,
GOOGLE_HEALTH_ROLLUP_RANGE_DAYS,
buildDailyRollUpBody,
extractGoogleApiErrorDetail,
fetchDailyRollUp,
runWithGoogleHealthClientOutcome,
type GoogleHealthRollupShape,
} from "../client";
import { extractGoogleApiErrorDetail } from "@/lib/google-health/request";
import { runWithGoogleHealthClientOutcome } from "@/lib/google-health/budget";
import { GOOGLE_HEALTH_DATA_TYPES } from "@/lib/google-health/mappers";
import { GoogleHealthApiError } from "../response-classifier";
function jsonResponse(status: number, body: unknown): Response {
@@ -4,7 +4,7 @@ import {
GOOGLE_HEALTH_CORE_SCOPES,
getGoogleHealthScopeString,
resolveGoogleHealthScopes,
} from "../client";
} from "@/lib/google-health/oauth";
describe("Google Health scope resolver", () => {
it("ships exactly the four core Restricted read scopes", () => {
@@ -18,7 +18,7 @@ import { readFileSync } from "node:fs";
import { join } from "node:path";
import { describe, expect, it } from "vitest";
import { GOOGLE_HEALTH_CORE_SCOPES } from "../client";
import { GOOGLE_HEALTH_CORE_SCOPES } from "@/lib/google-health/oauth";
import {
INTEGRATION_DOCS_BASE,
integrationDocsHref,
@@ -16,11 +16,11 @@ const { fetchDailyRollUpMock, fetchDataPointsMock, handleErrorMock } =
vi.mock("../client", async (importOriginal) => {
const actual = await importOriginal<typeof import("../client")>();
return {
...actual,
fetchDailyRollUp: fetchDailyRollUpMock,
fetchDataPoints: fetchDataPointsMock,
};
return { ...actual, fetchDailyRollUp: fetchDailyRollUpMock };
});
vi.mock("../data-points", async (importOriginal) => {
const actual = await importOriginal<typeof import("../data-points")>();
return { ...actual, fetchDataPoints: fetchDataPointsMock };
});
vi.mock("../sync-core", () => ({
getValidToken: vi.fn(async () => ({
@@ -30,7 +30,7 @@ vi.mock("@/lib/integrations/status", () => ({
recordSyncFailure: vi.fn(async () => {}),
recordSyncSuccess: vi.fn(async () => {}),
}));
vi.mock("@/lib/insights/comprehensive-generate", () => ({
vi.mock("@/lib/insights/status-invalidation", () => ({
invalidateStatusInsightsForTypes: vi.fn(async () => {}),
}));
vi.mock("@/lib/arrivals/measurement-emit", () => ({
@@ -76,7 +76,7 @@ vi.mock("@/lib/rollups/measurement-rollups", () => ({
recomputeBucketsForMeasurement: vi.fn(async () => {}),
recomputeUserRollups: vi.fn(async () => {}),
}));
vi.mock("@/lib/insights/comprehensive-generate", () => ({
vi.mock("@/lib/insights/status-invalidation", () => ({
invalidateStatusInsightsForTypes: vi.fn(async () => {}),
}));
vi.mock("@/lib/cache/invalidate", () => ({
@@ -85,9 +85,15 @@ vi.mock("@/lib/cache/invalidate", () => ({
vi.mock("../credentials", () => ({
getUserGoogleHealthCredentials: getUserGoogleHealthCredentialsMock,
}));
vi.mock("../client", async (importOriginal) => {
const actual = await importOriginal<typeof import("../client")>();
return { ...actual, refreshAccessToken: refreshAccessTokenMock };
vi.mock("../oauth", async (importOriginal) => {
const actual = await importOriginal<typeof import("../oauth")>();
return {
...actual,
googleHealthOAuth: {
...actual.googleHealthOAuth,
refresh: refreshAccessTokenMock,
},
};
});
vi.mock("../sync-metrics", () => ({ syncUserMetrics: resourceFake }));
@@ -66,7 +66,7 @@ vi.mock("@/lib/rollups/measurement-rollups", () => ({
collapseToTypeDayKeys: vi.fn(() => []),
recomputeUserRollups: vi.fn(async () => {}),
}));
vi.mock("@/lib/insights/comprehensive-generate", () => ({
vi.mock("@/lib/insights/status-invalidation", () => ({
invalidateStatusInsightsForTypes: vi.fn(async () => {}),
}));
vi.mock("../sync-workout", () => ({ syncUserWorkout: resources.workout }));
@@ -44,7 +44,7 @@ vi.mock("@/lib/rollups/measurement-rollups", () => ({
collapseToTypeDayKeys: vi.fn(() => []),
recomputeUserRollups: vi.fn(async () => {}),
}));
vi.mock("@/lib/insights/comprehensive-generate", () => ({
vi.mock("@/lib/insights/status-invalidation", () => ({
invalidateStatusInsightsForTypes: vi.fn(async () => {}),
}));
vi.mock("../sync-workout", () => ({ syncUserWorkout: vi.fn(async () => 0) }));
@@ -46,10 +46,8 @@ vi.mock("@/lib/tz/resolver", () => ({
resolveUserTimezone: vi.fn(async () => "UTC"),
}));
import {
GOOGLE_HEALTH_PAGE_SIZE,
runWithGoogleHealthClientOutcome,
} from "../client";
import { GOOGLE_HEALTH_PAGE_SIZE } from "@/lib/google-health/data-points";
import { runWithGoogleHealthClientOutcome } from "@/lib/google-health/budget";
import { syncUserMetrics } from "../sync-metrics";
const PAGES = 3;
@@ -22,8 +22,8 @@ const { fetchDataPointsMock, handleErrorMock, mapWeightMock } = vi.hoisted(
}),
);
vi.mock("../client", async (importOriginal) => {
const actual = await importOriginal<typeof import("../client")>();
vi.mock("../data-points", async (importOriginal) => {
const actual = await importOriginal<typeof import("../data-points")>();
return {
...actual,
fetchDataPoints: fetchDataPointsMock,
@@ -44,9 +44,12 @@ vi.mock("../client", async (importOriginal) => {
if (points.length > 0) await onPage(points);
},
),
mapWeight: mapWeightMock,
};
});
vi.mock("../mappers", async (importOriginal) => {
const actual = await importOriginal<typeof import("../mappers")>();
return { ...actual, mapWeight: mapWeightMock };
});
vi.mock("../sync-core", () => ({
getValidToken: vi.fn(async () => ({
@@ -142,7 +142,7 @@ vi.mock("@/lib/rollups/measurement-rollups", () => ({
collapseToTypeDayKeys: vi.fn(() => []),
recomputeUserRollups: vi.fn(async () => {}),
}));
vi.mock("@/lib/insights/comprehensive-generate", () => ({
vi.mock("@/lib/insights/status-invalidation", () => ({
invalidateStatusInsightsForTypes: vi.fn(async () => {}),
}));
vi.mock("../sync-core", () => ({
@@ -35,7 +35,7 @@ vi.mock("@/lib/rollups/measurement-rollups", () => ({
recomputeBucketsForMeasurement: vi.fn(async () => {}),
recomputeUserRollups: vi.fn(async () => {}),
}));
vi.mock("@/lib/insights/comprehensive-generate", () => ({
vi.mock("@/lib/insights/status-invalidation", () => ({
invalidateStatusInsightsForTypes: vi.fn(async () => {}),
}));
vi.mock("../credentials", () => ({
@@ -45,12 +45,18 @@ vi.mock("../client", async () => {
const actual = await vi.importActual<typeof import("../client")>("../client");
return {
...actual,
refreshAccessToken: vi.fn(),
noteGoogleHealthMapped: vi.fn(),
noteGoogleHealthWritten: vi.fn(),
noteGoogleHealthOutcomeFailure: vi.fn(),
};
});
vi.mock("../oauth", async (importOriginal) => {
const actual = await importOriginal<typeof import("../oauth")>();
return {
...actual,
googleHealthOAuth: { ...actual.googleHealthOAuth, refresh: vi.fn() },
};
});
import { eventStorage } from "@/lib/logging/context";
import { WideEventBuilder } from "@/lib/logging/event-builder";
@@ -26,10 +26,12 @@ const {
maybeEnqueueMorningRefreshMock: vi.fn(async () => {}),
}));
vi.mock("../client", () => ({
vi.mock("../data-points", () => ({
GOOGLE_HEALTH_ACTIVITY_PAGE_SIZE: 100,
GOOGLE_HEALTH_DATA_TYPES: { sleep: "sleep" },
fetchDataPoints: fetchDataPointsMock,
}));
vi.mock("../mappers", () => ({
GOOGLE_HEALTH_DATA_TYPES: { sleep: "sleep" },
mapSleepSessionDetailed: mapSleepSessionDetailedMock,
}));
@@ -26,13 +26,19 @@ vi.mock("@/lib/rollups/measurement-rollups", () => ({
recomputeBucketsForMeasurement: vi.fn(async () => {}),
recomputeUserRollups: vi.fn(async () => {}),
}));
vi.mock("@/lib/insights/comprehensive-generate", () => ({
vi.mock("@/lib/insights/status-invalidation", () => ({
invalidateStatusInsightsForTypes: vi.fn(async () => {}),
}));
vi.mock("../credentials", () => ({
getUserGoogleHealthCredentials: vi.fn(async () => null),
}));
vi.mock("../client", () => ({ refreshAccessToken: vi.fn() }));
vi.mock("../oauth", async (importOriginal) => {
const actual = await importOriginal<typeof import("../oauth")>();
return {
...actual,
googleHealthOAuth: { ...actual.googleHealthOAuth, refresh: vi.fn() },
};
});
import { replaceStaleGoogleHealthSleep } from "../sync-core";
@@ -46,7 +46,7 @@ vi.mock("@/lib/rollups/measurement-rollups", () => ({
collapseToTypeDayKeys: vi.fn(() => []),
recomputeUserRollups: vi.fn(async () => {}),
}));
vi.mock("@/lib/insights/comprehensive-generate", () => ({
vi.mock("@/lib/insights/status-invalidation", () => ({
invalidateStatusInsightsForTypes: vi.fn(async () => {}),
}));
vi.mock("../sync-workout", () => ({ syncUserWorkout: resources.workout }));
@@ -54,7 +54,7 @@ vi.mock("../sync-sleep", () => ({ syncUserSleep: resources.sleep }));
vi.mock("../sync-activity", () => ({ syncUserActivity: resources.activity }));
vi.mock("../sync-metrics", () => ({ syncUserMetrics: resources.metrics }));
import { googleHealthSliceExhausted } from "../client";
import { googleHealthSliceExhausted } from "@/lib/google-health/budget";
import { syncUserGoogleHealth } from "../sync";
beforeEach(() => {
@@ -39,15 +39,18 @@ vi.mock("@/lib/rollups/measurement-rollups", () => ({
recomputeBucketsForMeasurement: vi.fn(async () => {}),
recomputeUserRollups: vi.fn(async () => {}),
}));
vi.mock("@/lib/insights/comprehensive-generate", () => ({
vi.mock("@/lib/insights/status-invalidation", () => ({
invalidateStatusInsightsForTypes: vi.fn(async () => {}),
}));
vi.mock("../credentials", () => ({
getUserGoogleHealthCredentials: vi.fn(async () => null),
}));
vi.mock("../client", async (importOriginal) => {
const actual = await importOriginal<typeof import("../client")>();
return { ...actual, refreshAccessToken: vi.fn() };
vi.mock("../oauth", async (importOriginal) => {
const actual = await importOriginal<typeof import("../oauth")>();
return {
...actual,
googleHealthOAuth: { ...actual.googleHealthOAuth, refresh: vi.fn() },
};
});
import { upsertGoogleHealthMeasurements } from "../sync-core";
@@ -29,10 +29,12 @@ vi.mock("@/lib/logging/context", () => ({
vi.mock("@/lib/tz/resolver", () => ({
resolveUserTimezone: vi.fn(async () => "UTC"),
}));
vi.mock("../client", () => ({
vi.mock("../data-points", () => ({
GOOGLE_HEALTH_ACTIVITY_PAGE_SIZE: 100,
GOOGLE_HEALTH_DATA_TYPES: { exercise: "exercise" },
fetchDataPoints,
}));
vi.mock("../mappers", () => ({
GOOGLE_HEALTH_DATA_TYPES: { exercise: "exercise" },
mapWorkout,
}));
vi.mock("../sync-core", () => ({
Loaded 100 of 442 files, more files were not shown because too many files have changed in this diff. Show more