fix(analytics): all-time extremes and range deltas count one source per day

The all-time weight, blood-pressure and pulse figures the insight features
carry averaged every raw row, so a device that samples densely outweighed the
readings the source ladder prefers on the same days. They now keep one source
per day, like the dashboard's all-time figures.

The period-over-period range delta averaged readings. For step-like totals
the current window still holds the last day and a half as many small
samples, so the average collapsed; it now compares average daily totals. For
sleep it averaged stage segments and in-bed envelopes; it now compares
average nights of time asleep. The API descriptions say which.
This commit is contained in:
Marc-André Bombeck committed 2026-09-26 16:07:45 +02:00
1 parent b2bb81cac3
commit 4e7beb797c
7 files changed
+396 -29

No files matched your search

@@ -20,6 +20,8 @@ import {
recomputeUserRollups,
} from "@/lib/rollups/measurement-rollups";
import { readLiveBuckets } from "@/lib/measurements/daily-series-read";
import { readAllTimeExtremes } from "@/lib/insights/feature-blocks";
import { computeSummariesSlice } from "@/lib/analytics/summaries-slice";
import type { MeasurementType } from "@/generated/prisma/client";
vi.mock("@/lib/db-compat", () => ({
@@ -283,4 +285,85 @@ describe("coarse buckets — canonical source per day", () => {
expect(rollupDay.mean).toBe(110);
expect(liveMonth.value).toBe(110);
});
it("all-time weight figures keep one source per day", async () => {
const user = await seedUser();
const prisma = getPrismaClient();
await prisma.measurement.createMany({
data: [
// Day A: the scale (WITHINGS leads the ladder) and a typed reading.
{
userId: user.id,
type: "WEIGHT",
value: 80,
unit: "kg",
source: "WITHINGS",
measuredAt: day(3, 7),
},
{
userId: user.id,
type: "WEIGHT",
value: 90,
unit: "kg",
source: "MANUAL",
measuredAt: day(3, 9),
},
// Day B: a typed reading only.
{
userId: user.id,
type: "WEIGHT",
value: 70,
unit: "kg",
source: "MANUAL",
measuredAt: day(4, 9),
},
],
});
const extremes = await readAllTimeExtremes(user.id, ["WEIGHT"]);
expect(extremes.get("WEIGHT")).toEqual({ mean: 75, min: 70, max: 80 });
});
it("rows older than the rollup window count one source per day in the all-time figures", async () => {
const user = await seedUser();
const prisma = getPrismaClient();
const now = Date.now();
const recent = new Date(now - 2 * DAY_MS);
const old = new Date(now - 6 * 365 * DAY_MS);
await prisma.measurement.createMany({
data: [
{
userId: user.id,
type: "WEIGHT",
value: 82,
unit: "kg",
source: "MANUAL",
measuredAt: recent,
},
// Six years ago, one day seen by two sources.
{
userId: user.id,
type: "WEIGHT",
value: 90,
unit: "kg",
source: "WITHINGS",
measuredAt: old,
},
{
userId: user.id,
type: "WEIGHT",
value: 100,
unit: "kg",
source: "MANUAL",
measuredAt: new Date(old.getTime() + 60_000),
},
],
});
await recomputeUserRollups(user.id, { granularities: ["DAY"] });
const slice = await computeSummariesSlice(user.id);
expect(slice.summaries.WEIGHT.count).toBe(2);
expect(slice.summaries.WEIGHT.max).toBe(90);
expect(slice.summaries.WEIGHT.mean).toBe(86);
});
});