mirror of
https://github.com/MBombeck/HealthLog.git
synced 2026-11-13 22:32:04 +00:00
perf(analytics): fast-path gates per-type only
`isFullyCovered(coverage) && specificTypes` poisoned every fast- path with a single unrelated uncovered measurement type. v1.4.38.5 verified the discovery query was finding nothing; the slow cold- mount was the AND-stack collapsing the gate. - correlations-fast-path: gates on SYS + PULSE + WEIGHT only. - bp-in-target-fast-path: gates on SYS + DIA only. - health-score-fast-path: gates on WEIGHT only. Coverage-probe semantics unchanged; helpers now consult the per- type entries directly. Power-user cold-mount on /api/analytics estimated 30-75 s → ~3-5 s.
This commit is contained in:
1 parent
9cc5ececa7
commit
8a8150d2bf
5 files changed
+50
-20
No files matched your search
@@ -1,5 +1,34 @@
|
|||||||
# Changelog
|
# Changelog
|
||||||
|
|
||||||
|
## [1.4.38.8] — 2026-05-18 — Analytics fast-path gates per-type only
|
||||||
|
|
||||||
|
The v1.4.38.5–.7 chain confirmed the rollup-fast-path was bouncing
|
||||||
|
to the live SQL aggregator even when the helper's own types were
|
||||||
|
fully covered — every fast-path stacked `isFullyCovered(coverage)
|
||||||
|
&& specificTypes` and a single unrelated uncovered type (e.g. an
|
||||||
|
iOS-pushed brand-new ACTIVITY_FLIGHTS reading) flipped the AND
|
||||||
|
false and stranded the whole helper on live SQL across the full
|
||||||
|
347 k-row measurement table.
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
|
||||||
|
- **`correlations-fast-path` gates only on `BLOOD_PRESSURE_SYS +
|
||||||
|
PULSE + WEIGHT`** — the three types the helper actually reads.
|
||||||
|
`isFullyCovered(coverage)` AND-term dropped.
|
||||||
|
- **`bp-in-target-fast-path` gates only on `BLOOD_PRESSURE_SYS +
|
||||||
|
BLOOD_PRESSURE_DIA`**. Same fix.
|
||||||
|
- **`health-score-fast-path` gates only on `WEIGHT`**. Same fix.
|
||||||
|
|
||||||
|
Coverage-probe semantics unchanged. Each helper now consults the
|
||||||
|
per-type entries directly; a brand-new uncovered metric type no
|
||||||
|
longer poisons unrelated cards.
|
||||||
|
|
||||||
|
### Operator notes
|
||||||
|
|
||||||
|
- No new migration. No env-var change.
|
||||||
|
- Expected: cold-mount `/api/analytics` on Marc-sized accounts
|
||||||
|
drops from 30-75 s to ~3-5 s.
|
||||||
|
|
||||||
## [1.4.38.7] — 2026-05-18 — Rollup recompute observability + admin trigger
|
## [1.4.38.7] — 2026-05-18 — Rollup recompute observability + admin trigger
|
||||||
|
|
||||||
The v1.4.38.5 / v1.4.38.6 chain promised a fast-path recovery for
|
The v1.4.38.5 / v1.4.38.6 chain promised a fast-path recovery for
|
||||||
|
|||||||
+1
-1
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "healthlog",
|
"name": "healthlog",
|
||||||
"version": "1.4.38.7",
|
"version": "1.4.38.8",
|
||||||
"description": "Self-hosted personal-health-tracking PWA with Withings integration, AI insights, and doctor-report PDF export.",
|
"description": "Self-hosted personal-health-tracking PWA with Withings integration, AI insights, and doctor-report PDF export.",
|
||||||
"license": "AGPL-3.0-only",
|
"license": "AGPL-3.0-only",
|
||||||
"homepage": "https://healthlog.dev",
|
"homepage": "https://healthlog.dev",
|
||||||
|
|||||||
@@ -54,7 +54,6 @@ import { prisma } from "@/lib/db";
|
|||||||
import { annotate } from "@/lib/logging/context";
|
import { annotate } from "@/lib/logging/context";
|
||||||
import { readRollupBuckets } from "@/lib/measurements/rollups";
|
import { readRollupBuckets } from "@/lib/measurements/rollups";
|
||||||
import {
|
import {
|
||||||
isFullyCovered,
|
|
||||||
probeRollupCoverage,
|
probeRollupCoverage,
|
||||||
type RollupCoverageMap,
|
type RollupCoverageMap,
|
||||||
} from "@/lib/measurements/rollup-coverage";
|
} from "@/lib/measurements/rollup-coverage";
|
||||||
@@ -149,14 +148,16 @@ export async function computeBpInTargetFastPath(input: {
|
|||||||
? "near-utc"
|
? "near-utc"
|
||||||
: "non-utc-live-fallback";
|
: "non-utc-live-fallback";
|
||||||
|
|
||||||
// Belt-and-braces: only take the rollup path when EVERY logged type
|
// v1.4.38.8 — gate only on the types the helper actually reads.
|
||||||
// is covered AND both BP types are present in the map AND the
|
// The previous `isFullyCovered(coverage) && sysCovered && diaCovered`
|
||||||
// cross-tz guard cleared. A brand-new user without BP at all falls
|
// shape poisoned the BP fast-path with any unrelated brand-new
|
||||||
// into `isFullyCovered === false` (coverage map is empty) and the
|
// measurement type the user had logged (e.g. iOS pushed a new
|
||||||
// route's downstream code already skips the BP block when
|
// ACTIVITY_FLIGHTS reading without a rollup row yet) — even though
|
||||||
// `getBpTargets` returns null; we still call the live fallback here
|
// SYS + DIA are fully covered. The cold-mount then fanned out to the
|
||||||
// so the helper's contract stays single-shot.
|
// live aggregator over the full measurement table. Coverage probe is
|
||||||
if (userNearUtc && isFullyCovered(coverage) && sysCovered && diaCovered) {
|
// already keyed per type, so trusting the per-type entries directly
|
||||||
|
// is the correct gate.
|
||||||
|
if (userNearUtc && sysCovered && diaCovered) {
|
||||||
return computeFromRollups(userId, targets, now, tzGuard);
|
return computeFromRollups(userId, targets, now, tzGuard);
|
||||||
}
|
}
|
||||||
return computeFromLive(userId, targets, now, tzGuard);
|
return computeFromLive(userId, targets, now, tzGuard);
|
||||||
|
|||||||
@@ -47,7 +47,6 @@ import { prisma } from "@/lib/db";
|
|||||||
import { annotate } from "@/lib/logging/context";
|
import { annotate } from "@/lib/logging/context";
|
||||||
import { readRollupBuckets } from "@/lib/measurements/rollups";
|
import { readRollupBuckets } from "@/lib/measurements/rollups";
|
||||||
import {
|
import {
|
||||||
isFullyCovered,
|
|
||||||
probeRollupCoverage,
|
probeRollupCoverage,
|
||||||
type RollupCoverageMap,
|
type RollupCoverageMap,
|
||||||
} from "@/lib/measurements/rollup-coverage";
|
} from "@/lib/measurements/rollup-coverage";
|
||||||
@@ -140,14 +139,13 @@ export async function computeCorrelationHypothesesFastPath(
|
|||||||
// local-day buckets and this guard can come back down.
|
// local-day buckets and this guard can come back down.
|
||||||
const userNearUtc = isNearUtc(userTz, now);
|
const userNearUtc = isNearUtc(userTz, now);
|
||||||
|
|
||||||
// The three measurement-derived streams ride the rollup-fast-path
|
// v1.4.38.8 — gate only on the three types this helper actually
|
||||||
// only when EVERY type the user has logged is covered AND each of
|
// reads (SYS / PULSE / WEIGHT). The prior `isFullyCovered &&` AND
|
||||||
// SYS / PULSE / WEIGHT is in the coverage map. Partial coverage
|
// poisoned the correlations fast-path with any unrelated brand-new
|
||||||
// falls back to the live path so a brand-new metric type doesn't
|
// type the user had logged, even when SYS + PULSE + WEIGHT were
|
||||||
// make the correlation card vanish.
|
// fully covered. Per-type coverage is the correct gate.
|
||||||
const measurementsOnRollups =
|
const measurementsOnRollups =
|
||||||
userNearUtc &&
|
userNearUtc &&
|
||||||
isFullyCovered(coverage) &&
|
|
||||||
coverage.get("BLOOD_PRESSURE_SYS") === true &&
|
coverage.get("BLOOD_PRESSURE_SYS") === true &&
|
||||||
coverage.get("PULSE") === true &&
|
coverage.get("PULSE") === true &&
|
||||||
coverage.get("WEIGHT") === true;
|
coverage.get("WEIGHT") === true;
|
||||||
|
|||||||
@@ -42,7 +42,6 @@ import { prisma } from "@/lib/db";
|
|||||||
import { annotate } from "@/lib/logging/context";
|
import { annotate } from "@/lib/logging/context";
|
||||||
import { readRollupBuckets } from "@/lib/measurements/rollups";
|
import { readRollupBuckets } from "@/lib/measurements/rollups";
|
||||||
import {
|
import {
|
||||||
isFullyCovered,
|
|
||||||
probeRollupCoverage,
|
probeRollupCoverage,
|
||||||
type RollupCoverageMap,
|
type RollupCoverageMap,
|
||||||
} from "@/lib/measurements/rollup-coverage";
|
} from "@/lib/measurements/rollup-coverage";
|
||||||
@@ -146,8 +145,11 @@ export async function computeUserHealthScoreFastPath(
|
|||||||
const prevUntil = new Date(now.getTime() - 7 * DAY_MS);
|
const prevUntil = new Date(now.getTime() - 7 * DAY_MS);
|
||||||
|
|
||||||
const coverage = input.coverage ?? (await probeRollupCoverage(userId));
|
const coverage = input.coverage ?? (await probeRollupCoverage(userId));
|
||||||
const weightCovered =
|
// v1.4.38.8 — gate only on the WEIGHT type this helper reads. The
|
||||||
isFullyCovered(coverage) && coverage.get("WEIGHT") === true;
|
// prior `isFullyCovered &&` AND poisoned the score fast-path with
|
||||||
|
// any unrelated brand-new type that lacked rollup coverage. Per-
|
||||||
|
// type coverage is the correct gate.
|
||||||
|
const weightCovered = coverage.get("WEIGHT") === true;
|
||||||
|
|
||||||
// Weight series — rollup-fast-path when fully covered, raw findMany
|
// Weight series — rollup-fast-path when fully covered, raw findMany
|
||||||
// otherwise. The raw read selects only the columns the slope helper
|
// otherwise. The raw read selects only the columns the slope helper
|
||||||
|
|||||||
Reference in new issue
Block a user