From 2bc706a4453dd845f7d370bda88bd3826e3519be Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marc-Andr=C3=A9=20Bombeck?= Date: Sat, 18 Jul 2026 00:08:30 +0200 Subject: [PATCH] perf(insights): read PULSE cards through the day-rollup tier MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit /api/insights/cards pulled every raw PULSE row from the last 90 days into JS to compute an average and an anomaly count — on a dense wearable account that's 100k+ rows the DAY rollups already summarise. WEIGHT and blood pressure stay on the bounded raw read (manual-entry volume, and blood pressure needs exact-timestamp pairing for the in-target percentage); PULSE now reads through the existing rollup read-swap helper, falling back to a bounded per-day-grouped live read on a coverage miss. --- .../insights/cards/__tests__/route.test.ts | 61 ++++++++++++ src/app/api/insights/cards/route.ts | 93 ++++++++++++------- 2 files changed, 120 insertions(+), 34 deletions(-) diff --git a/src/app/api/insights/cards/__tests__/route.test.ts b/src/app/api/insights/cards/__tests__/route.test.ts index 8b25d71d8..56d842b15 100644 --- a/src/app/api/insights/cards/__tests__/route.test.ts +++ b/src/app/api/insights/cards/__tests__/route.test.ts @@ -11,6 +11,12 @@ vi.mock("@/lib/db", () => ({ // back to the all-on default so existing assertions ride // through unchanged. appSettings: { findUnique: vi.fn().mockResolvedValue(null) }, + // v1.28 perf — PULSE now reads through the DAY-rollup read-swap + // (`probeRollupCoverage` + `readDayMeanSeries`). An empty coverage + // probe sends every test down the live-fallback branch, which itself + // reads via the already-mocked `measurement.findMany` — no rollup + // rows needed for these tests. + $queryRaw: vi.fn().mockResolvedValue([]), }, })); @@ -69,6 +75,10 @@ beforeEach(() => { aiProvider: null, } as never); vi.mocked(prisma.appSettings.findUnique).mockResolvedValue(null as never); + // v1.28 perf — empty coverage probe routes PULSE through the + // live-fallback branch of `readDayMeanSeries`, which reads via the + // already-mocked `measurement.findMany` above. + vi.mocked(prisma.$queryRaw).mockResolvedValue([] as never); }); const callGet = GET as unknown as (req: NextRequest) => Promise; @@ -165,6 +175,57 @@ describe("GET /api/insights/cards", () => { expect(body.data.some((c) => c.severity === "alert")).toBe(true); }); + it("reads PULSE through the day-rollup read-swap, never the manual raw-row query (perf)", async () => { + vi.mocked(getSession).mockResolvedValue(SESSION_OK as never); + + // 27 quiet days + 3 outlier days, one raw sample per day — a large + // enough quiet cluster that the 3 outliers still clear the z-score > 2 + // threshold once folded into day means by the live-fallback branch of + // `readDayMeanSeries` (a small n dilutes the mean/stddev too much for + // the outliers to stand out). + const pulseRows: Array<{ value: number; measuredAt: Date }> = []; + const now = new Date(); + for (let i = 0; i < 30; i++) { + const at = new Date(now.getTime() - i * 86_400_000); + pulseRows.push({ value: i < 3 ? 300 : 68, measuredAt: at }); + } + + vi.mocked(prisma.measurement.findMany).mockImplementation( + (async (args: { where?: { type?: unknown } }) => { + // `readDayMeanSeries`'s live-fallback probes a single `type` + // equality filter; the manual WEIGHT/BP query below probes an + // `{ in: [...] }` list — the two calls are distinguishable by shape. + if (args?.where?.type === "PULSE") return pulseRows; + return []; + }) as never, + ); + + const res = await callGet(makeReq()); + expect(res.status).toBe(200); + const body = (await res.json()) as { + data: Array<{ title: string; summary: string }>; + }; + expect( + body.data.some((c) => c.summary.includes("unusual pulse readings")), + ).toBe(true); + + // The bounded manual-measurements query (WEIGHT / BP) must never ask + // for PULSE — a dense wearable account's raw PULSE rows only ever + // reach this route through the day-mean rollup read. + const manualCall = vi + .mocked(prisma.measurement.findMany) + .mock.calls.find( + (call) => + typeof (call[0] as { where?: { type?: { in?: unknown[] } } }) + ?.where?.type === "object", + ); + expect(manualCall).toBeDefined(); + const typeIn = ( + manualCall?.[0] as { where: { type: { in: string[] } } } + ).where.type.in; + expect(typeIn).not.toContain("PULSE"); + }); + it("does NOT flag a fully-adherent weekly injectable as low compliance (#214 regression)", async () => { // Pre-fix this path used a naive `schedules.length × 7` denominator, so a // weekly Monday injectable with its one dose taken computed ~14% and fired diff --git a/src/app/api/insights/cards/route.ts b/src/app/api/insights/cards/route.ts index 6c592bf2d..6525c661f 100644 --- a/src/app/api/insights/cards/route.ts +++ b/src/app/api/insights/cards/route.ts @@ -13,6 +13,8 @@ import { apiSuccess } from "@/lib/api-response"; import { annotate } from "@/lib/logging/context"; import { prisma } from "@/lib/db"; import { summarize, type DataPoint } from "@/lib/analytics/trends"; +import { probeRollupCoverage } from "@/lib/rollups/measurement-coverage"; +import { readDayMeanSeries } from "@/lib/insights/derived/baseline"; import { getBpTargets } from "@/lib/analytics/bp-targets"; import { isBpReadingInTarget } from "@/lib/analytics/bp-in-target"; import { @@ -72,43 +74,66 @@ export const GET = apiHandler(async () => { await requireAssistantSurface("insightStatus"); annotate({ action: { name: "insights.cards" } }); - const ninetyDaysAgo = new Date(Date.now() - 90 * 86_400_000); + const NINETY_DAY_WINDOW = 90; + const ninetyDaysAgo = new Date(Date.now() - NINETY_DAY_WINDOW * 86_400_000); - const [dbUser, allMeasurements, medications] = await Promise.all([ - prisma.user.findUnique({ - where: { id: user.id }, - select: { heightCm: true, dateOfBirth: true, aiProvider: true }, - }), - prisma.measurement.findMany({ - where: { - userId: user.id, - type: { - in: ["WEIGHT", "BLOOD_PRESSURE_SYS", "BLOOD_PRESSURE_DIA", "PULSE"], + const [dbUser, manualMeasurements, medications, rollupCoverage] = + await Promise.all([ + prisma.user.findUnique({ + where: { id: user.id }, + select: { heightCm: true, dateOfBirth: true, aiProvider: true }, + }), + // WEIGHT + BP are manual entries (low volume, and BP needs exact- + // timestamp pairing below for `bpPctInTarget`), so a bounded raw read + // stays cheap. PULSE is excluded here — a continuously-synced wearable + // account can carry 100k+ raw rows in the window; it reads through the + // rollup tier below instead. + prisma.measurement.findMany({ + where: { + userId: user.id, + type: { in: ["WEIGHT", "BLOOD_PRESSURE_SYS", "BLOOD_PRESSURE_DIA"] }, + measuredAt: { gte: ninetyDaysAgo }, + deletedAt: null, }, - measuredAt: { gte: ninetyDaysAgo }, - deletedAt: null, - }, - orderBy: { measuredAt: "asc" }, - select: { type: true, value: true, measuredAt: true }, - }), - prisma.medication.findMany({ - // v1.16.11 — as-needed (PRN) medications never surface a compliance - // rate (no expected doses). - where: { userId: user.id, active: true, asNeeded: false }, - include: { - // Schedules through the shared compliance select so the configured - // per-dose windows reach the engine like every other surface. - schedules: { select: SCHEDULE_COMPLIANCE_SELECT }, - // v1.16.3 — archived schedule eras for era-aware expected counts. - scheduleRevisions: { orderBy: { validFrom: "asc" } }, - // v1.25 H-MED1 — pause eras so paused days drop out of the denominator. - pauseEras: { select: { pausedAt: true, resumedAt: true } }, - }, - }), - ]); + orderBy: { measuredAt: "asc" }, + select: { type: true, value: true, measuredAt: true }, + }), + prisma.medication.findMany({ + // v1.16.11 — as-needed (PRN) medications never surface a compliance + // rate (no expected doses). + where: { userId: user.id, active: true, asNeeded: false }, + include: { + // Schedules through the shared compliance select so the configured + // per-dose windows reach the engine like every other surface. + schedules: { select: SCHEDULE_COMPLIANCE_SELECT }, + // v1.16.3 — archived schedule eras for era-aware expected counts. + scheduleRevisions: { orderBy: { validFrom: "asc" } }, + // v1.25 H-MED1 — pause eras so paused days drop out of the denominator. + pauseEras: { select: { pausedAt: true, resumedAt: true } }, + }, + }), + probeRollupCoverage(user.id), + ]); + + // v1.28 perf — PULSE reads the DAY-rollup tier (read-swap; falls back to a + // bounded per-day-grouped live read on a coverage miss) instead of pulling + // every raw sample into JS. `pulseAvg30` / `pulseAnomalyCount` derive from + // the resulting day-mean series, so a dense wearable account's anomaly + // count reflects daily outliers rather than every noisy raw sample. + const { points: pulseDayMeans } = await readDayMeanSeries( + user.id, + "PULSE", + NINETY_DAY_WINDOW, + new Date(), + rollupCoverage, + ); + const pulseData: DataPoint[] = pulseDayMeans.map((p) => ({ + date: new Date(`${p.day}T00:00:00.000Z`), + value: p.mean, + })); const byType = (t: MeasurementType): DataPoint[] => - allMeasurements + manualMeasurements .filter((m) => m.type === t) .map((m) => ({ date: m.measuredAt, value: m.value })); @@ -119,11 +144,11 @@ export const GET = apiHandler(async () => { "WEIGHT", "BLOOD_PRESSURE_SYS", "BLOOD_PRESSURE_DIA", - "PULSE", ] as MeasurementType[]) { const data = byType(t); if (data.length > 0) summaries[t] = summarize(data); } + if (pulseData.length > 0) summaries.PULSE = summarize(pulseData); let bmi: number | null = null; if (dbUser?.heightCm && summaries.WEIGHT?.latest) {