perf(web): add dashboard hydration observability
This commit is contained in:
parent
b6de67f081
commit
328f6e00bf
5 changed files with 339 additions and 22 deletions
|
|
@ -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", () => {
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue