perf(insights): read PULSE cards through the day-rollup tier

/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.
This commit is contained in:
Marc-André Bombeck committed 2026-07-18 00:37:24 +02:00
1 parent 2b89259cce
commit 2bc706a445
2 files changed
+120 -34

No files matched your search

@@ -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<Response>;
@@ -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
+59 -34
View File
@@ -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) {