perf(insights): persist timeout stub for blood-pressure and weight status

Mirror the bmi-status (v1.4.37) timeout-stub persist into the two
sibling endpoints. On a 20 s provider stall the route now writes a
sentinel auditLog row keyed to today's Berlin day so the next mount
short-circuits at the cache lookup instead of re-racing the same
provider call for the rest of the day.

This eliminates the ~14 s warm response on every iOS dashboard open
when the provider has stalled once that day. Response envelope
unchanged; iOS v0.5.4 contract preserved.

Tests cover both arms: the persist-on-timeout path (one create with
timeout:true, model:"timeout-stub", today's dateKey) and the
short-circuit-on-stub path (subsequent mount returns cached text,
provider never invoked).
This commit is contained in:
Marc-André Bombeck committed 2026-05-21 17:38:20 +02:00
1 parent 9d2901aaf9
commit 27f3bec1f4
5 files changed
+371 -4

No files matched your search

@@ -109,6 +109,104 @@ describe("generateBloodPressureStatusForUser — v1.4.6 bucketed payload", () =>
});
});
describe("generateBloodPressureStatusForUser — v1.4.41 timeout-stub persistence", () => {
it("persists a sentinel row keyed to today when the provider times out", async () => {
vi.mocked(prisma.user.findUnique).mockResolvedValue({
dateOfBirth: null,
} as never);
vi.mocked(prisma.auditLog.findFirst).mockResolvedValue(null);
vi.mocked(prisma.measurement.findMany).mockResolvedValue([
{ type: "BLOOD_PRESSURE_SYS", value: 132, measuredAt: new Date() },
{ type: "BLOOD_PRESSURE_DIA", value: 84, measuredAt: new Date() },
] as never);
vi.mocked(prisma.medication.findMany).mockResolvedValue([] as never);
vi.mocked(prisma.medicationIntakeEvent.findMany).mockResolvedValue(
[] as never,
);
vi.mocked(prisma.moodEntry.findMany).mockResolvedValue([] as never);
vi.mocked(prisma.auditLog.create).mockResolvedValue({
createdAt: new Date(),
} as never);
// Provider hangs past the 20 s timeout — withTimeout cuts the race.
vi.mocked(resolveProvider).mockResolvedValue({
type: "anthropic",
generateCompletion: vi.fn(
() =>
new Promise(() => {
/* never resolves */
}),
),
} as never);
vi.useFakeTimers();
const promise = generateBloodPressureStatusForUser("user-1", {
locale: "en",
});
await vi.advanceTimersByTimeAsync(25_000);
const result = await promise;
vi.useRealTimers();
expect(result.text).toBeTruthy();
expect(result.cached).toBe(true);
expect(result.updatedAt).toBeTruthy();
const createCalls = vi.mocked(prisma.auditLog.create).mock.calls;
expect(createCalls.length).toBe(1);
const details = (createCalls[0][0] as { data: { details: string } }).data
.details;
const parsed = JSON.parse(details) as {
dateKey?: string;
text?: string;
timeout?: boolean;
model?: string;
};
expect(parsed.text).toBeTruthy();
expect(parsed.timeout).toBe(true);
expect(parsed.model).toBe("timeout-stub");
expect(parsed.dateKey).toMatch(/^\d{4}-\d{2}-\d{2}$/);
});
it("subsequent mounts short-circuit at the cache lookup and skip the race", async () => {
const parts = new Intl.DateTimeFormat("en-US", {
timeZone: "Europe/Berlin",
year: "numeric",
month: "2-digit",
day: "2-digit",
}).formatToParts(new Date());
const y = parts.find((p) => p.type === "year")!.value;
const m = parts.find((p) => p.type === "month")!.value;
const d = parts.find((p) => p.type === "day")!.value;
const todayKey = `${y}-${m}-${d}`;
const stubRow = {
createdAt: new Date(),
details: JSON.stringify({
dateKey: todayKey,
locale: "en",
text: "Blood-pressure assessment temporarily unavailable.",
timeout: true,
}),
};
vi.mocked(prisma.auditLog.findFirst).mockResolvedValue(stubRow as never);
const providerCall = vi.fn();
vi.mocked(resolveProvider).mockResolvedValue({
type: "anthropic",
generateCompletion: providerCall,
} as never);
const result = await generateBloodPressureStatusForUser("user-1", {
locale: "en",
});
expect(providerCall).not.toHaveBeenCalled();
expect(result.text).toBe("Blood-pressure assessment temporarily unavailable.");
expect(result.cached).toBe(true);
expect(result.updatedAt).toBe(stubRow.createdAt.toISOString());
expect(prisma.auditLog.create).not.toHaveBeenCalled();
});
});
describe("generateBloodPressureStatusForUser — token-leak hardening (v1.4.27 F16)", () => {
it("strips metric: tokens out of the cached text before persisting", async () => {
vi.mocked(prisma.user.findUnique).mockResolvedValue({
@@ -82,6 +82,92 @@ describe("generateWeightStatusForUser — v1.4.6 bucketed payload", () => {
});
});
describe("generateWeightStatusForUser — v1.4.41 timeout-stub persistence", () => {
it("persists a sentinel row keyed to today when the provider times out", async () => {
vi.mocked(prisma.auditLog.findFirst).mockResolvedValue(null);
vi.mocked(prisma.measurement.findMany).mockResolvedValue([
{ type: "WEIGHT", value: 82, measuredAt: new Date() },
] as never);
vi.mocked(prisma.moodEntry.findMany).mockResolvedValue([] as never);
vi.mocked(prisma.auditLog.create).mockResolvedValue({
createdAt: new Date(),
} as never);
// Provider hangs past the 20 s timeout — withTimeout cuts the race.
vi.mocked(resolveProvider).mockResolvedValue({
type: "anthropic",
generateCompletion: vi.fn(
() =>
new Promise(() => {
/* never resolves */
}),
),
} as never);
vi.useFakeTimers();
const promise = generateWeightStatusForUser("user-1", { locale: "en" });
await vi.advanceTimersByTimeAsync(25_000);
const result = await promise;
vi.useRealTimers();
expect(result.text).toBeTruthy();
expect(result.cached).toBe(true);
expect(result.updatedAt).toBeTruthy();
const createCalls = vi.mocked(prisma.auditLog.create).mock.calls;
expect(createCalls.length).toBe(1);
const details = (createCalls[0][0] as { data: { details: string } }).data
.details;
const parsed = JSON.parse(details) as {
dateKey?: string;
text?: string;
timeout?: boolean;
model?: string;
};
expect(parsed.text).toBeTruthy();
expect(parsed.timeout).toBe(true);
expect(parsed.model).toBe("timeout-stub");
expect(parsed.dateKey).toMatch(/^\d{4}-\d{2}-\d{2}$/);
});
it("subsequent mounts short-circuit at the cache lookup and skip the race", async () => {
const parts = new Intl.DateTimeFormat("en-US", {
timeZone: "Europe/Berlin",
year: "numeric",
month: "2-digit",
day: "2-digit",
}).formatToParts(new Date());
const y = parts.find((p) => p.type === "year")!.value;
const m = parts.find((p) => p.type === "month")!.value;
const d = parts.find((p) => p.type === "day")!.value;
const todayKey = `${y}-${m}-${d}`;
const stubRow = {
createdAt: new Date(),
details: JSON.stringify({
dateKey: todayKey,
locale: "en",
text: "Weight assessment temporarily unavailable.",
timeout: true,
}),
};
vi.mocked(prisma.auditLog.findFirst).mockResolvedValue(stubRow as never);
const providerCall = vi.fn();
vi.mocked(resolveProvider).mockResolvedValue({
type: "anthropic",
generateCompletion: providerCall,
} as never);
const result = await generateWeightStatusForUser("user-1", { locale: "en" });
expect(providerCall).not.toHaveBeenCalled();
expect(result.text).toBe("Weight assessment temporarily unavailable.");
expect(result.cached).toBe(true);
expect(result.updatedAt).toBe(stubRow.createdAt.toISOString());
expect(prisma.auditLog.create).not.toHaveBeenCalled();
});
});
describe("generateWeightStatusForUser — token-leak hardening (v1.4.27 F16)", () => {
it("strips metric: tokens out of the cached text before persisting", async () => {
vi.mocked(prisma.auditLog.findFirst).mockResolvedValue(null);
+41 -2
View File
@@ -542,11 +542,50 @@ export async function generateBloodPressureStatusForUser(
);
if (raced.timedOut || raced.value === null) {
// v1.4.41 — parity with bmi-status: persist a sentinel row keyed
// to today so the next mount short-circuits at the cache lookup
// above instead of re-racing the same 20 s provider call on every
// cold visit. Without this, a single stall (provider hiccup,
// network blip, model warm-up) left iOS users staring at the
// loading state on every dashboard open for the rest of the day.
// The body is the same no-key fallback the caller returned
// before, so the user-facing UI is identical; only the re-fire
// frequency drops to zero until the daily pre-warm job overwrites
// the stub. `meta.timeout` is set so the pre-warm worker can
// recognise and replace it rather than respect it as a real
// assessment.
const stubText = getNoKeyBloodPressureStatusText(locale);
let stubUpdatedAt: string | null = null;
try {
const stub = await prisma.auditLog.create({
data: {
userId,
action: cacheAction,
details: JSON.stringify({
dateKey: todayKey,
locale,
text: stubText,
providerType: provider.type,
model: "timeout-stub",
tokensUsed: null,
timeout: true,
}),
},
select: { createdAt: true },
});
stubUpdatedAt = stub.createdAt.toISOString();
} catch {
// Best-effort persist — if the row write fails the caller still
// sees the deterministic fallback text and the next mount falls
// back to the (still expensive) race. Do not surface the error:
// the user does not care that the cache write missed, only that
// the page rendered.
}
return {
hasProvider: true,
text: getNoKeyBloodPressureStatusText(locale),
text: stubText,
cached: true,
updatedAt: null,
updatedAt: stubUpdatedAt,
};
}
+41 -2
View File
@@ -440,11 +440,50 @@ export async function generateWeightStatusForUser(
);
if (raced.timedOut || raced.value === null) {
// v1.4.41 — parity with bmi-status: persist a sentinel row keyed
// to today so the next mount short-circuits at the cache lookup
// above instead of re-racing the same 20 s provider call on every
// cold visit. Without this, a single stall (provider hiccup,
// network blip, model warm-up) left iOS users staring at the
// loading state on every dashboard open for the rest of the day.
// The body is the same no-key fallback the caller returned
// before, so the user-facing UI is identical; only the re-fire
// frequency drops to zero until the daily pre-warm job overwrites
// the stub. `meta.timeout` is set so the pre-warm worker can
// recognise and replace it rather than respect it as a real
// assessment.
const stubText = getNoKeyWeightStatusText(locale);
let stubUpdatedAt: string | null = null;
try {
const stub = await prisma.auditLog.create({
data: {
userId,
action: cacheAction,
details: JSON.stringify({
dateKey: todayKey,
locale,
text: stubText,
providerType: provider.type,
model: "timeout-stub",
tokensUsed: null,
timeout: true,
}),
},
select: { createdAt: true },
});
stubUpdatedAt = stub.createdAt.toISOString();
} catch {
// Best-effort persist — if the row write fails the caller still
// sees the deterministic fallback text and the next mount falls
// back to the (still expensive) race. Do not surface the error:
// the user does not care that the cache write missed, only that
// the page rendered.
}
return {
hasProvider: true,
text: getNoKeyWeightStatusText(locale),
text: stubText,
cached: true,
updatedAt: null,
updatedAt: stubUpdatedAt,
};
}