mirror of
https://github.com/MBombeck/HealthLog.git
synced 2026-11-07 12:22:03 +00:00
perf(coach): bound buildCoachSnapshot to last 90 days
Every POST /api/insights/chat called extractFeatures(userId, false),
which ran prisma.measurement.findMany({ where: { userId } }) with no
date filter — fetching every measurement the user had ever recorded.
For power users with multi-year Withings imports that's tens of
thousands of rows on every Coach turn.
Add an optional sinceDays bound to extractFeatures (default unchanged
= full history; analytics dashboard + insights/generate keep their
existing behaviour) and have the Coach snapshot pass sinceDays: 90.
The Coach already only narrates the last7 / last30 / last90 windows,
so the 90-day floor preserves every metric the snapshot surfaces.
Fixes Senior-HIGH-3.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
1 parent
e632e26854
commit
249607d0b2
2 files changed
+26
-3
No files matched your search
@@ -25,11 +25,21 @@ export interface CoachSnapshotResult {
|
||||
* `includeRaw=false` because the Coach replies are conversational and
|
||||
* the user is asking the model — they should never depend on raw
|
||||
* measurement timestamps that the privacy mode controls.
|
||||
*
|
||||
* Bounded to the last 90 days. Coach narrates the same windows the
|
||||
* Daily Briefing renders (last7 / last30 / last90); fetching every
|
||||
* historical measurement on each turn was unbounded I/O for power
|
||||
* users with multi-year Withings imports. The 90-day floor is the
|
||||
* widest window any Coach metric currently consumes.
|
||||
*/
|
||||
const COACH_SNAPSHOT_WINDOW_DAYS = 90;
|
||||
|
||||
export async function buildCoachSnapshot(
|
||||
userId: string,
|
||||
): Promise<CoachSnapshotResult> {
|
||||
const features = await extractFeatures(userId, false);
|
||||
const features = await extractFeatures(userId, false, {
|
||||
sinceDays: COACH_SNAPSHOT_WINDOW_DAYS,
|
||||
});
|
||||
|
||||
// Trim down to the metrics the Coach narrates. extractFeatures
|
||||
// returns more (sleep, steps, etc.) — the Coach surface keeps the
|
||||
|
||||
@@ -256,6 +256,7 @@ function computeHistoricalComparison(
|
||||
export async function extractFeatures(
|
||||
userId: string,
|
||||
includeRaw: boolean,
|
||||
options: { sinceDays?: number } = {},
|
||||
): Promise<AggregatedFeatures | RawFeatures> {
|
||||
const user = await prisma.user.findUnique({
|
||||
where: { id: userId },
|
||||
@@ -268,9 +269,21 @@ export async function extractFeatures(
|
||||
|
||||
const now = Date.now();
|
||||
|
||||
// Fetch ALL measurements (not just 30 days) for full temporal context
|
||||
// Fetch measurements. Default = ALL (full temporal context for the
|
||||
// dashboard / insights generator). Callers that only consume the
|
||||
// ≤90-day windows (Coach snapshot per turn) pass `sinceDays: 90` so
|
||||
// the per-turn I/O stays bounded — `findMany({ where: { userId } })`
|
||||
// is unbounded by user-history size and gets paid once per Coach turn
|
||||
// for power users with multi-year Withings imports.
|
||||
const sinceDays = options.sinceDays;
|
||||
const sinceCutoff =
|
||||
typeof sinceDays === "number" && sinceDays > 0
|
||||
? new Date(now - sinceDays * 24 * 60 * 60 * 1000)
|
||||
: null;
|
||||
const measurements = await prisma.measurement.findMany({
|
||||
where: { userId },
|
||||
where: sinceCutoff
|
||||
? { userId, measuredAt: { gte: sinceCutoff } }
|
||||
: { userId },
|
||||
orderBy: { measuredAt: "asc" },
|
||||
});
|
||||
|
||||
|
||||
Reference in new issue
Block a user