mirror of
https://github.com/MBombeck/HealthLog.git
synced 2026-11-09 04:50:59 +00:00
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:
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
|
||||
|
||||
@@ -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) {
|
||||
|
||||
Reference in new issue
Block a user