mirror of
https://github.com/MBombeck/HealthLog.git
synced 2026-11-08 12:32:43 +00:00
perf(dashboard): split analytics fan-out across slim and thick queries
Pre-fix the dashboard fired a single `useAnalyticsQuery()` against the thick `/api/analytics` envelope. Every per-type tile on the dashboard strip waited on the heavy fan-out before paint. Mood and medication tiles arrived first (separate routes with their own dedicated 60 s LRU caches); every other tile then arrived as one delayed burst once the full slice landed. Marc reported the "blocked-then-burst" pattern as "etwas nervig" in the v1.4.39.1 post-deploy trace. The dashboard now mounts two `useAnalyticsQuery` calls in parallel: one for `?slice=summaries` (per-type tile strip + last-seen freshness map) and one for the thick envelope (BD-Zielbereich + glucose tiles). The tile strip paints as soon as the slim slice resolves; the BD-Zielbereich and glucose tiles stream in afterwards from the thick slice. TanStack's parallel-mounting keeps the network fan-out flat, and both queries share the `caches.analytics` 60 s LRU server-side so warm hits stay free. The `data` object the rest of the dashboard consumes stays shape- stable: a `useMemo` merges the two query results with slim winning on overlapping fields (slim and thick compute `summaries` / `lastSeenByType` from the same SQL helpers, so the values agree). The thick-only `bpInTargetPct*` and `glucoseByContext` fields read from the thick slice and surface as `null` / `undefined` until the thick query resolves; the BD-Zielbereich tile's data-floor gate already suppresses the tile cleanly in that interim state. A text-anchored test pins both query mounts so a future refactor cannot silently collapse the dashboard back onto the thick-only path.
This commit is contained in:
1 parent
f8a1319e94
commit
3126e4cff2
2 files changed
+102
-9
No files matched your search
@@ -0,0 +1,52 @@
|
||||
import { describe, it, expect } from "vitest";
|
||||
import { readFileSync } from "node:fs";
|
||||
import { join } from "node:path";
|
||||
|
||||
/**
|
||||
* v1.4.39.2 — dashboard analytics slim/thick split.
|
||||
*
|
||||
* Pre-fix the dashboard fired a single `useAnalyticsQuery()` against the
|
||||
* thick `/api/analytics` envelope; every per-type tile waited on the
|
||||
* heavy fan-out before paint. Mood and medication tiles arrived first
|
||||
* (separate routes) and then every other tile arrived as one burst,
|
||||
* which Marc reported as "etwas nervig" in the v1.4.39.1 post-deploy
|
||||
* trace.
|
||||
*
|
||||
* Post-fix the dashboard mounts TWO `useAnalyticsQuery` calls in
|
||||
* parallel: one for `?slice=summaries` (per-type tile strip) and one
|
||||
* for the thick envelope (BD-Zielbereich + glucose tiles). The tile
|
||||
* strip paints from the slim slice as soon as it lands; thick fields
|
||||
* stream in afterwards. Without this dual-mount the cold-mount UX
|
||||
* waterfall regresses to the v1.4.39.1 behaviour, so the test pins the
|
||||
* exact two call shapes.
|
||||
*/
|
||||
const ROOT = join(__dirname, "../../..");
|
||||
const PAGE_PATH = join(ROOT, "src/app/page.tsx");
|
||||
|
||||
function load(path: string): string {
|
||||
return readFileSync(path, "utf8");
|
||||
}
|
||||
|
||||
describe("v1.4.39.2 — dashboard analytics slim/thick split", () => {
|
||||
it("mounts both the slim slice and the thick slice in parallel", () => {
|
||||
const src = load(PAGE_PATH);
|
||||
// Slim slice — `?slice=summaries` paints the per-type tile strip.
|
||||
expect(src).toMatch(/useAnalyticsQuery\(\{\s*slice:\s*"summaries"\s*\}\)/);
|
||||
// Thick slice — the bare `useAnalyticsQuery()` (no slice) feeds
|
||||
// the BD-Zielbereich + glucose tiles. Both must coexist.
|
||||
expect(src).toMatch(/useAnalyticsQuery\(\)/);
|
||||
});
|
||||
|
||||
it("merges slim and thick results so call-sites stay shape-stable", () => {
|
||||
const src = load(PAGE_PATH);
|
||||
// The merge contract: slim wins on overlapping fields, thick fills
|
||||
// in `bpInTargetPct*` and `glucoseByContext`. The text anchors
|
||||
// protect against a future refactor accidentally dropping one of
|
||||
// the queries — the `data` object that downstream tile rendering
|
||||
// consumes must stay shape-stable across both query states.
|
||||
expect(src).toMatch(/analyticsSlimQuery/);
|
||||
expect(src).toMatch(/analyticsThickQuery/);
|
||||
expect(src).toMatch(/bpInTargetPct/);
|
||||
expect(src).toMatch(/glucoseByContext/);
|
||||
});
|
||||
});
|
||||
+50
-9
@@ -1,6 +1,6 @@
|
||||
"use client";
|
||||
|
||||
import React, { useState } from "react";
|
||||
import React, { useMemo, useState } from "react";
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
import { useAuth } from "@/hooks/use-auth";
|
||||
import {
|
||||
@@ -222,14 +222,55 @@ export default function DashboardPage() {
|
||||
refetchOnWindowFocus: false,
|
||||
} as const;
|
||||
|
||||
// v1.4.33 IW2 — the dashboard tile-strip reads `bpInTargetPct*` +
|
||||
// `glucoseByContext` (thick-only fields) so it stays on the default
|
||||
// thick slice. The shared hook still centralises the cache settings
|
||||
// (60s staleTime, no refetch on mount / window focus) so the
|
||||
// tile-strip dedups with the Insights mother page on the same
|
||||
// queryKey + slice combination.
|
||||
const analyticsQuery = useAnalyticsQuery();
|
||||
const data = analyticsQuery.data as AnalyticsData | undefined;
|
||||
// v1.4.39.2 — split the dashboard's analytics consumption so the
|
||||
// tile-strip paints from the slim slice and the BD-Zielbereich +
|
||||
// glucose tiles stream in from the thick slice afterwards.
|
||||
//
|
||||
// Pre-fix: a single `useAnalyticsQuery()` against `/api/analytics`
|
||||
// (thick slice) blocked every per-type tile until the heavy fan-out
|
||||
// resolved. Mood and medication tiles paint from their own dedicated
|
||||
// endpoints and arrived first; the per-type measurement tiles then
|
||||
// arrived as one burst once the full slice landed — Marc reported
|
||||
// this "blocked-then-burst" pattern as "etwas nervig".
|
||||
//
|
||||
// Post-fix: two parallel queries. The slim slice (`?slice=summaries`)
|
||||
// resolves the per-type DataSummary headlines + `lastSeenByType` and
|
||||
// typically returns in well under a second once the rollup tier is
|
||||
// warm; that paints every per-type tile in the strip. The thick
|
||||
// slice resolves `bpInTargetPct*` + `glucoseByContext` for the
|
||||
// BD-Zielbereich and glucose tiles, which stream in independently.
|
||||
// Both queries share `caches.analytics` server-side so warm hits
|
||||
// stay free, and TanStack's parallel-mounting keeps the network fan-
|
||||
// out flat.
|
||||
const analyticsSlimQuery = useAnalyticsQuery({ slice: "summaries" });
|
||||
const analyticsThickQuery = useAnalyticsQuery();
|
||||
const data = useMemo<AnalyticsData | undefined>(() => {
|
||||
const slim = analyticsSlimQuery.data;
|
||||
const thick = analyticsThickQuery.data;
|
||||
if (!slim && !thick) return undefined;
|
||||
// Slim wins on overlapping fields when both have resolved — slim
|
||||
// and thick compute `summaries` / `lastSeenByType` from the same
|
||||
// SQL helpers so the values agree, and reading from whichever
|
||||
// resolves first keeps the tile strip painting as soon as either
|
||||
// query lands.
|
||||
const summaries =
|
||||
slim?.summaries ?? thick?.summaries ?? ({} as Record<string, DataSummary>);
|
||||
const lastSeenByType =
|
||||
slim?.lastSeenByType ?? thick?.lastSeenByType;
|
||||
return {
|
||||
summaries,
|
||||
lastSeenByType,
|
||||
bpInTargetPct: (thick?.bpInTargetPct ?? null) as number | null,
|
||||
bpInTargetPct7d: thick?.bpInTargetPct7d ?? null,
|
||||
bpInTargetPct30d: thick?.bpInTargetPct30d ?? null,
|
||||
bpInTargetPctAllTime: thick?.bpInTargetPctAllTime ?? null,
|
||||
bpInTargetPctPriorMonth: thick?.bpInTargetPctPriorMonth ?? null,
|
||||
bpInTargetPctPriorYear: thick?.bpInTargetPctPriorYear ?? null,
|
||||
glucoseByContext: thick?.glucoseByContext as
|
||||
| Record<string, DataSummaryType>
|
||||
| undefined,
|
||||
};
|
||||
}, [analyticsSlimQuery.data, analyticsThickQuery.data]);
|
||||
|
||||
const { data: layoutData } = useQuery({
|
||||
queryKey: queryKeys.dashboardWidgets(),
|
||||
|
||||
Reference in new issue
Block a user