perf(web): add dashboard hydration observability

This commit is contained in:
vince 2026-06-20 14:58:25 +02:00
parent b6de67f081
commit 328f6e00bf
5 changed files with 339 additions and 22 deletions

View file

@ -1,4 +1,4 @@
import { describe, expect, test, vi } from "vitest";
import { afterAll, afterEach, describe, expect, test, vi } from "vitest";
import { dimensionLabDashboardFixture } from "$lib/dashboard-seed/dimensionlab";
import {
createDashboardTileCache,
@ -6,9 +6,20 @@ import {
handleDashboardTileRoute,
loadDashboardResponse,
loadDashboardTileResponse,
type DashboardTileResolutionLogEvent,
} from "./dashboard";
describe("dashboard API route", () => {
const consoleInfo = vi.spyOn(console, "info").mockImplementation(() => undefined);
afterEach(() => {
consoleInfo.mockClear();
});
afterAll(() => {
consoleInfo.mockRestore();
});
test("returns the ready dashboard shell without hydrating live datasources", async () => {
const fetch = vi.spyOn(globalThis, "fetch").mockRejectedValue(
new Error("live datasource fetch should not run for the shell response"),
@ -154,12 +165,13 @@ describe("dashboard API route", () => {
test("caches ready tile responses until the tile ttl expires", async () => {
const cache = createDashboardTileCache();
let now = 1_000;
const fetch = vi.fn(async () =>
jsonResponse({
const fetch = vi.fn(async () => {
now += 7;
return jsonResponse({
status: "UP",
ping: 42,
}),
);
});
});
const tile = { kind: "service", groupId: "essentials", id: "vaultwarden" } as const;
const first = await loadDashboardTileResponse(tile, {
@ -190,6 +202,68 @@ describe("dashboard API route", () => {
expect(fetch).toHaveBeenCalledTimes(2);
});
test("logs tile duration and cache hit or miss metadata", async () => {
let now = 1_000;
const logs: DashboardTileResolutionLogEvent[] = [];
const tile = { kind: "service", groupId: "essentials", id: "vaultwarden" } as const;
const service = dimensionLabDashboardFixture.serviceGroups
.flatMap((group) => group.services)
.find((item) => item.id === tile.id);
if (!service) throw new Error("missing service fixture");
let cacheCalls = 0;
const tileCache = {
async resolve() {
cacheCalls += 1;
if (cacheCalls === 1) now += 7;
const cacheState = cacheCalls === 1 ? "miss" as const : "hit" as const;
return {
cache: cacheState,
coalesced: false,
response: {
state: "ready" as const,
tile,
item: service,
},
};
},
};
await loadDashboardTileResponse(tile, {
logTileResolution: (event) => logs.push(event),
now: () => now,
refreshSeedDocument: true,
seedIfEmpty: true,
tileCache,
});
now += 10;
await loadDashboardTileResponse(tile, {
logTileResolution: (event) => logs.push(event),
now: () => now,
refreshSeedDocument: true,
seedIfEmpty: true,
tileCache,
});
expect(logs).toEqual([
expect.objectContaining({
cache: "miss",
coalesced: false,
durationMs: 7,
status: "ready",
tileKey: dashboardTileCacheKey(tile),
}),
expect.objectContaining({
cache: "hit",
coalesced: false,
durationMs: 0,
status: "ready",
tileKey: dashboardTileCacheKey(tile),
}),
]);
});
test("keeps telemetry tiles cached for fifteen seconds", async () => {
const cache = createDashboardTileCache();
let now = 1_000;
@ -268,6 +342,7 @@ describe("dashboard API route", () => {
test("coalesces concurrent tile requests for the same cache key", async () => {
const cache = createDashboardTileCache();
const logs: DashboardTileResolutionLogEvent[] = [];
let resolveFetch: ((response: Response) => void) | undefined;
const fetch = vi.fn(() =>
new Promise<Response>((resolve) => {
@ -278,6 +353,7 @@ describe("dashboard API route", () => {
const first = loadDashboardTileResponse(tile, {
fetch,
logTileResolution: (event) => logs.push(event),
now: () => 1_000,
refreshSeedDocument: true,
seedIfEmpty: true,
@ -285,6 +361,7 @@ describe("dashboard API route", () => {
});
const second = loadDashboardTileResponse(tile, {
fetch,
logTileResolution: (event) => logs.push(event),
now: () => 1_000,
refreshSeedDocument: true,
seedIfEmpty: true,
@ -296,6 +373,48 @@ describe("dashboard API route", () => {
resolveFetch?.(jsonResponse({ status: "UP", ping: 42 }));
expect(await first).toEqual(await second);
expect(logs).toEqual([
expect.objectContaining({
cache: "miss",
coalesced: false,
status: "ready",
tileKey: dashboardTileCacheKey(tile),
}),
expect.objectContaining({
cache: "miss",
coalesced: true,
status: "ready",
tileKey: dashboardTileCacheKey(tile),
}),
]);
});
test("logs error categories for failed tile cache resolution", async () => {
const logs: DashboardTileResolutionLogEvent[] = [];
const tile = { kind: "service", groupId: "essentials", id: "vaultwarden" } as const;
await expect(
loadDashboardTileResponse(tile, {
logTileResolution: (event) => logs.push(event),
refreshSeedDocument: true,
seedIfEmpty: true,
tileCache: {
async resolve() {
throw new TypeError("cache failed");
},
},
}),
).rejects.toThrow("cache failed");
expect(logs).toEqual([
expect.objectContaining({
cache: "miss",
coalesced: false,
errorCategory: "TypeError",
status: "error",
tileKey: dashboardTileCacheKey(tile),
}),
]);
});
test("uses structured tile cache keys when identifiers contain delimiters", () => {