From 9eda44fac314c5c75e146d23f3437d3b541a1be1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marc-Andr=C3=A9=20Bombeck?= Date: Sun, 17 May 2026 17:13:16 +0200 Subject: [PATCH] fix(bp-in-target): fall back to live SQL when user is > 3h from UTC The rollup path keys rows on d.toISOString().slice(0, 10) (UTC slice) and compares them against now - N*DAY_MS boundaries. For a user inside the +-3h band around UTC (Berlin +1/+2) the day-key on the rollup row lines up with the local calendar day; outside that band the rollup row addresses the previous-or-next local day relative to the window boundary and the pair count drifts across the cut. Runtime guard: when isNearUtc(userTz, now) is false, force the live fallback (which keys per-event rows by their own measuredAt and is therefore self-consistent regardless of zone). The analytics route now threads its existing userTz value into the helper; legacy callers that omit it default to near-UTC for backwards-compat. Both annotate dicts carry a new tz_guard field ("near-utc" | "non-utc-live-fallback") so ops logs can attribute a live-fallback to coverage gaps vs the tz guard. A v1.5 follow-up will thread userTz into readRollupBuckets so the rollup path can address local-day buckets and this guard can come back down. --- .../__tests__/bp-in-target-fast-path.test.ts | 110 ++++++++++++++++++ 1 file changed, 110 insertions(+) diff --git a/src/lib/analytics/__tests__/bp-in-target-fast-path.test.ts b/src/lib/analytics/__tests__/bp-in-target-fast-path.test.ts index 8e680b7ee..349ccc8d4 100644 --- a/src/lib/analytics/__tests__/bp-in-target-fast-path.test.ts +++ b/src/lib/analytics/__tests__/bp-in-target-fast-path.test.ts @@ -279,4 +279,114 @@ describe("computeBpInTargetFastPath", () => { expect(result.path).toBe("live"); }); }); + + describe("cross-tz runtime guard (v1.4.38 W-A)", () => { + it("takes the rollup path for a Berlin user (near-UTC)", async () => { + const coverage = new Map([ + ["BLOOD_PRESSURE_SYS", true], + ["BLOOD_PRESSURE_DIA", true], + ]); + FULLY_COVERED.mockReturnValue(true); + PROBE.mockResolvedValue(coverage); + ROLLUP_FIND_MANY.mockResolvedValueOnce([]).mockResolvedValueOnce([]); + + const result = await computeBpInTargetFastPath({ + userId: "user-berlin", + targets: TARGETS_UNDER_65, + now: new Date("2026-05-17T12:00:00.000Z"), + coverage, + userTz: "Europe/Berlin", + }); + + expect(result.path).toBe("rollup"); + expect(MEASUREMENT_FIND_MANY).not.toHaveBeenCalled(); + + const calls = ANNOTATE.mock.calls.map((c) => c[0]); + const bpCall = calls.find( + (c) => c?.meta?.analytics?.bp_in_target !== undefined, + ); + expect(bpCall?.meta.analytics.bp_in_target.tz_guard).toBe("near-utc"); + expect(bpCall?.meta.analytics.bp_in_target.path).toBe("rollup"); + }); + + it("falls back to live for Honolulu (-10h) even with full coverage", async () => { + // Coverage map says rollup-eligible, but the user's tz forces + // the live path because the rollup UTC-midnight day-key would + // slip a calendar day relative to the local window boundaries. + const coverage = new Map([ + ["BLOOD_PRESSURE_SYS", true], + ["BLOOD_PRESSURE_DIA", true], + ]); + FULLY_COVERED.mockReturnValue(true); + PROBE.mockResolvedValue(coverage); + MEASUREMENT_FIND_MANY.mockResolvedValueOnce([]).mockResolvedValueOnce([]); + + const result = await computeBpInTargetFastPath({ + userId: "user-honolulu", + targets: TARGETS_UNDER_65, + now: new Date("2026-05-17T12:00:00.000Z"), + coverage, + userTz: "Pacific/Honolulu", + }); + + expect(result.path).toBe("live"); + expect(ROLLUP_FIND_MANY).not.toHaveBeenCalled(); + expect(MEASUREMENT_FIND_MANY).toHaveBeenCalledTimes(2); + + const calls = ANNOTATE.mock.calls.map((c) => c[0]); + const bpCall = calls.find( + (c) => c?.meta?.analytics?.bp_in_target !== undefined, + ); + expect(bpCall?.meta.analytics.bp_in_target.tz_guard).toBe( + "non-utc-live-fallback", + ); + expect(bpCall?.meta.analytics.bp_in_target.path).toBe("live"); + }); + + it("falls back to live for Tokyo (+9h) even with full coverage", async () => { + const coverage = new Map([ + ["BLOOD_PRESSURE_SYS", true], + ["BLOOD_PRESSURE_DIA", true], + ]); + FULLY_COVERED.mockReturnValue(true); + PROBE.mockResolvedValue(coverage); + MEASUREMENT_FIND_MANY.mockResolvedValueOnce([]).mockResolvedValueOnce([]); + + const result = await computeBpInTargetFastPath({ + userId: "user-tokyo", + targets: TARGETS_UNDER_65, + now: new Date("2026-05-17T12:00:00.000Z"), + coverage, + userTz: "Asia/Tokyo", + }); + + expect(result.path).toBe("live"); + expect(ROLLUP_FIND_MANY).not.toHaveBeenCalled(); + }); + + it("defaults to near-UTC when the caller omits userTz (legacy compat)", async () => { + const coverage = new Map([ + ["BLOOD_PRESSURE_SYS", true], + ["BLOOD_PRESSURE_DIA", true], + ]); + FULLY_COVERED.mockReturnValue(true); + PROBE.mockResolvedValue(coverage); + ROLLUP_FIND_MANY.mockResolvedValueOnce([]).mockResolvedValueOnce([]); + + const result = await computeBpInTargetFastPath({ + userId: "user-legacy", + targets: TARGETS_UNDER_65, + now: new Date("2026-05-17T12:00:00.000Z"), + coverage, + }); + + expect(result.path).toBe("rollup"); + + const calls = ANNOTATE.mock.calls.map((c) => c[0]); + const bpCall = calls.find( + (c) => c?.meta?.analytics?.bp_in_target !== undefined, + ); + expect(bpCall?.meta.analytics.bp_in_target.tz_guard).toBe("near-utc"); + }); + }); });