From 7c23a497080d4d637ed39b49a529c3da29b9c8a6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marc-Andr=C3=A9=20Bombeck?= Date: Tue, 20 Oct 2026 03:34:45 +0200 Subject: [PATCH] fix(google-health): annotate an ended backfill in one fixed shape The ended note carried the whole map of ended collections, dates and all, which duplicated the slice report and gave dashboards no stable shape. It now names the collection that just ended, its type and reason, whether the end is final, and counts the ends so far with their types. It stays meta: the job names the event's action once the slice is over, which would replace an action set here. --- docs/integrations/google-health.md | 5 +++- .../__tests__/backfill-bounds.test.ts | 23 +++++++++++++++++-- src/lib/google-health/sync-core.ts | 18 ++++++++++++++- 3 files changed, 42 insertions(+), 4 deletions(-) diff --git a/docs/integrations/google-health.md b/docs/integrations/google-health.md index 50c8890d5..0b187be94 100644 --- a/docs/integrations/google-health.md +++ b/docs/integrations/google-health.md @@ -265,7 +265,10 @@ reading: nothing left but a data type waiting for its second attempt and queued the next slice for when it is due (`delaySeconds`). - `meta["googleHealth.backfill.ended"]` appears on the slice where a data - type ended short, with the same entries. A walk stopped for lack of + type ended short: the `collection`, its measurement `type`, the `reason`, + whether the end is `final`, how many data types have ended short so far + (`endedCount`) and of those for good (`finalCount`), and their `types`. + The full entries are in the slice report's `ended`. A walk stopped for lack of progress also leaves a warning: `fetchHeartRate: 20 pages in a row reached no further back than `. - `meta["googleHealth.backfill.edgeFilterUnsupported"]` appears on the slice diff --git a/src/lib/google-health/__tests__/backfill-bounds.test.ts b/src/lib/google-health/__tests__/backfill-bounds.test.ts index cc76b51ba..7bc28ad46 100644 --- a/src/lib/google-health/__tests__/backfill-bounds.test.ts +++ b/src/lib/google-health/__tests__/backfill-bounds.test.ts @@ -9,9 +9,10 @@ */ import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; -const { prismaMock, safeFetchMock } = vi.hoisted(() => ({ +const { prismaMock, safeFetchMock, annotateMock } = vi.hoisted(() => ({ prismaMock: { googleHealthConnection: { update: vi.fn() } }, safeFetchMock: vi.fn(), + annotateMock: vi.fn(), })); vi.mock("@/lib/db", () => ({ prisma: prismaMock })); vi.mock("@/lib/safe-fetch", async (importOriginal) => ({ @@ -19,7 +20,7 @@ vi.mock("@/lib/safe-fetch", async (importOriginal) => ({ safeFetch: safeFetchMock, })); vi.mock("@/lib/logging/context", () => ({ - annotate: () => {}, + annotate: annotateMock, getEvent: () => null, })); @@ -321,6 +322,24 @@ describe("a collection ended short", () => { expect(googleHealthBackfillProgressAfterCompletion(ended)).toBeNull(); }); + it("annotates an end in one fixed shape", async () => { + await slice(stalls, new Map([["fetchWeight", HORIZON_END]])); + const notes = annotateMock.mock.calls + .map(([fields]) => fields.meta?.["googleHealth.backfill.ended"]) + .filter(Boolean); + expect(notes).toEqual([ + { + collection: "fetchHeartRate", + type: "PULSE", + reason: "no_progress", + final: false, + endedCount: 2, + finalCount: 1, + types: ["PULSE", "WEIGHT"], + }, + ]); + }); + it("is not walked again before its day is up", async () => { const walk = vi.fn(async () => 1); const { skipped } = await slice( diff --git a/src/lib/google-health/sync-core.ts b/src/lib/google-health/sync-core.ts index 25cb8b885..542755bc8 100644 --- a/src/lib/google-health/sync-core.ts +++ b/src/lib/google-health/sync-core.ts @@ -612,8 +612,24 @@ export async function recordGoogleHealthBackfillEnd( } : end, ); + // A fixed shape, not the whole note: the slice report carries every end in + // full (`googleHealth.backfill.slice`). This names the one that just ended + // and counts the rest. It is meta, not the event's action, because the job + // names the action once the slice is over and that would replace it. + const recorded = ended.get(key)!; + const final = [...ended.values()].filter(googleHealthBackfillEndIsFinal); annotate({ - meta: { "googleHealth.backfill.ended": Object.fromEntries(ended) }, + meta: { + "googleHealth.backfill.ended": { + collection: key, + type: recorded.type, + reason: recorded.reason, + final: googleHealthBackfillEndIsFinal(recorded), + endedCount: ended.size, + finalCount: final.length, + types: [...new Set([...ended.values()].map((e) => e.type))].sort(), + }, + }, }); try { await persistBackfillProgress(resume, {