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.
This commit is contained in:
Marc-André Bombeck committed 2026-10-20 03:34:45 +02:00
1 parent bfbd88b30b
commit 7c23a49708
3 files changed
+42 -4

No files matched your search

+4 -1
View File
@@ -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 <instant>`.
- `meta["googleHealth.backfill.edgeFilterUnsupported"]` appears on the slice
@@ -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(
+17 -1
View File
@@ -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, {