diff --git a/src/Web/autopilot-monitor-web/lib/__tests__/platformStats.test.ts b/src/Web/autopilot-monitor-web/lib/__tests__/platformStats.test.ts index 54f98598..40047dc4 100644 --- a/src/Web/autopilot-monitor-web/lib/__tests__/platformStats.test.ts +++ b/src/Web/autopilot-monitor-web/lib/__tests__/platformStats.test.ts @@ -8,10 +8,10 @@ import { const CONTAINER = "https://stats.example.test/publicstats"; const MANIFEST_URL = `${CONTAINER}/platform-stats.json`; -const PAYLOAD_URL = `${CONTAINER}/platform-stats.2026-10-02.json`; +const PAYLOAD_URL = `${CONTAINER}/platform-stats.2026-10-02T140005Z.json`; /** What the backend publishes (MaintenanceService.TryPublishPlatformStatsJsonAsync). */ -const MANIFEST = { latest: "platform-stats.2026-10-02.json", generatedAtUtc: "2026-10-02T14:00:05.1234567Z" }; +const MANIFEST = { latest: "platform-stats.2026-10-02T140005Z.json", generatedAtUtc: "2026-10-02T14:00:05.1234567Z" }; const PAYLOAD = { totalEnrollments: 12481, totalUsers: 310, @@ -91,13 +91,13 @@ describe("toStatItems", () => { }); describe("fetchLivePlatformStats", () => { - it("reads the manifest, then the file it names, revalidating both with the blob", async () => { + it("revalidates the manifest and takes the immutable file it names from the browser cache", async () => { const fetchMock = serve(BOTH_FILES); expect(await fetchLivePlatformStats(MANIFEST_URL)).toEqual(PAYLOAD); expect(fetchMock.mock.calls).toEqual([ [MANIFEST_URL, { cache: "no-cache" }], - [PAYLOAD_URL, { cache: "no-cache" }], + [PAYLOAD_URL, { cache: "default" }], ]); }); @@ -106,7 +106,7 @@ describe("fetchLivePlatformStats", () => { Object.defineProperty(redirected, "url", { value: "https://stats.example.test/v2/platform-stats.json" }); const fetchMock = serve({ [MANIFEST_URL]: () => redirected, - "https://stats.example.test/v2/platform-stats.2026-10-02.json": () => json(PAYLOAD), + "https://stats.example.test/v2/platform-stats.2026-10-02T140005Z.json": () => json(PAYLOAD), }); expect(await fetchLivePlatformStats(MANIFEST_URL)).toEqual(PAYLOAD); @@ -161,7 +161,7 @@ describe("loadPlatformStatsSnapshot", () => { expect(init?.signal).toBeInstanceOf(AbortSignal); } expect(inits[0]?.signal).toBe(inits[1]?.signal); - expect(log).toHaveBeenCalledWith(expect.stringContaining(PAYLOAD.lastUpdated)); + expect(log).toHaveBeenCalledWith(expect.stringContaining(`from ${MANIFEST.latest} (as of ${PAYLOAD.lastUpdated})`)); }); it("fails soft with the reason in the build log when the blob does not answer", async () => { diff --git a/src/Web/autopilot-monitor-web/lib/platformStats.ts b/src/Web/autopilot-monitor-web/lib/platformStats.ts index 23b698c3..b0f5652d 100644 --- a/src/Web/autopilot-monitor-web/lib/platformStats.ts +++ b/src/Web/autopilot-monitor-web/lib/platformStats.ts @@ -95,9 +95,23 @@ export function toStatItems(payload: PlatformStatsPayload): StatItem[] { }); } -/** Manifest, then the file it names. Throws with the reason; the callers decide what to report. */ -async function readPlatformStats(manifestUrl: string, init: RequestInit): Promise { - const manifestResponse = await fetch(manifestUrl, init); +/** One read: the numbers and the versioned file they came from (named in the build log). */ +interface PlatformStatsRead { + payload: PlatformStatsPayload; + file: string; +} + +/** + * Manifest, then the file it names. Throws with the reason; the callers decide what to report. + * Each hop takes its own request options: the manifest changes with every publish, a versioned + * file never does. + */ +async function readPlatformStats( + manifestUrl: string, + manifestInit: RequestInit, + payloadInit: RequestInit = manifestInit, +): Promise { + const manifestResponse = await fetch(manifestUrl, manifestInit); if (!manifestResponse.ok) throw new Error(`${manifestUrl} answered HTTP ${manifestResponse.status}`); const manifest = (await manifestResponse.json()) as Partial | null; if (typeof manifest?.latest !== "string" || !manifest.latest) { @@ -111,23 +125,23 @@ async function readPlatformStats(manifestUrl: string, init: RequestInit): Promis throw new Error(`${manifestUrl} names a file on another origin`); } - const payloadResponse = await fetch(payloadUrl.toString(), init); + const payloadResponse = await fetch(payloadUrl.toString(), payloadInit); if (!payloadResponse.ok) throw new Error(`${payloadUrl} answered HTTP ${payloadResponse.status}`); const payload: unknown = await payloadResponse.json(); if (typeof payload !== "object" || payload === null) throw new Error(`${payloadUrl} holds no stats object`); - return payload as PlatformStatsPayload; + return { payload: payload as PlatformStatsPayload, file: manifest.latest }; } /** - * The browser's read. `no-cache` revalidates both files with the blob: the versioned file is - * named per day but rewritten every 2 hours under `Cache-Control: immutable`, so a copy cached - * earlier that day would replace the newer baked numbers with older ones. + * The browser's read. The manifest is revalidated on every load; the versioned file it names + * comes from the browser cache once fetched: the backend writes a new name per publish and serves + * it immutable (MaintenanceService.PlatformStatsVersionedFileName). */ export async function fetchLivePlatformStats( manifestUrl: string = PLATFORM_STATS_MANIFEST_URL, ): Promise { try { - return await readPlatformStats(manifestUrl, { cache: "no-cache" }); + return (await readPlatformStats(manifestUrl, { cache: "no-cache" }, { cache: "default" })).payload; } catch { // The band is decorative: a failed read only means no live numbers. return null; @@ -147,14 +161,14 @@ export async function loadPlatformStatsSnapshot( ): Promise { if (!/^https?:\/\//i.test(manifestUrl)) return null; try { - const payload = await readPlatformStats(manifestUrl, { + const { payload, file } = await readPlatformStats(manifestUrl, { cache: "no-store", signal: AbortSignal.timeout(BUILD_FETCH_TIMEOUT_MS), }); const items = toStatItems(payload); - if (items.length === 0) throw new Error("the stats file holds no positive figure"); + if (items.length === 0) throw new Error(`${file} holds no positive figure`); const asOf = typeof payload.lastUpdated === "string" ? payload.lastUpdated : null; - console.log(`Platform stats baked into the landing page (as of ${asOf ?? "unknown"}).`); + console.log(`Platform stats baked into the landing page from ${file} (as of ${asOf ?? "unknown"}).`); return { items, asOf }; } catch (err) { // Node's fetch reports every network failure as "fetch failed" and keeps the reason in `cause`.