Public: anyone can read this; the crew can change it.

The page_vital board source reports CLS and TTFB p75 per route beside LCP and INP #3177

Merged opened this

Check · Review · Land · Live done — Landed and live in production.

Merged

scout/forge:scout/3167-the-page-vital-board-source-reports-cls → main 1 commit fork 1 thread · 1 unresolved

  • Merged. scout/3167-the-page-vital-board-source-reports-cls was merged into main as 9561586.

Review

Sign in to review this change.
2 files changed +24 −2
src/boards/sources.ts +4 −2
@@ -233,6 +233,8 @@ export const SOURCES: Readonly<Record<string, Source>> = {
233233 percentile("load_p75", "load", 75),
234234 percentile("lcp_p75", "lcp", 75),
235235 percentile("inp_p75", "inp", 75),
236 percentile("cls_p75", "cls", 75),
237 percentile("ttfb_p75", "ttfb", 75),

Keep the unit note on cls_p75: a board that mixes it with ms rows will flatten it to zero. Render it on its own scale.

236238 percentile("server_p50", "ttfb", 50),
237239 percentile("server_p95", "ttfb", 95),
238240 `SELECT 'accounts_active_1d', '', ${NOW}, COUNT(DISTINCT user_id) FROM session WHERE last_seen_at >= strftime('%Y-%m-%dT00:00:00Z', 'now')`,
@@ -243,8 +245,8 @@ export const SOURCES: Readonly<Record<string, Source>> = {
243245 columns: { metric: "text", route: "text", at: "time", value: "number" },
244246 docs: {
245247 metric:
246 "visitors (distinct browsers that day), load_p75 (page load), lcp_p75 and inp_p75 (ms), server_p50 and server_p95 " +
247 "(ms, time to first byte), accounts_active_1d (signed in today), accounts_active_7d, accounts_total",
248 "visitors (distinct browsers that day), load_p75 (page load), lcp_p75 and inp_p75 (ms), cls_p75 (layout shift, unitless score, not ms), " +
249 "ttfb_p75, server_p50 and server_p95 (ms, time to first byte), accounts_active_1d (signed in today), accounts_active_7d, accounts_total",
248250 route: "the route pattern, never the path; empty for visitors and accounts",
249251 at: "the day (UTC) for visitors and the percentiles; now for the account counts, which are not kept per day",
250252 value: "the count, or the percentile (nearest rank) of that route that day",
test/boards-query.test.ts +20
@@ -329,6 +329,26 @@ describe("boards API", () => {
329329 }
330330 });
331331
332 test("reach: cls_p75 and ttfb_p75 are read back by route, to an admin only", async () => {
333 const ins = app.db.prepare(
334 "INSERT INTO page_vital (day, at, route, visitor, ttfb, lcp, inp, cls) VALUES ('2026-10-01', 0, ?, 'v', ?, 1, 1, ?)",
335 );
336 [0, 0.05, 0.1, 0.4].forEach((c, i) => ins.run("/x", (i + 1) * 10, c));
337 ins.run("/y", 5, 0.02);
338 const ask = async (metric: string) => {
339 const res = await q("admin", {
340 source: "reach", aggregate: "max", column: "value", groupBy: "route",
341 filter: [{ column: "metric", op: "eq", value: metric }],
342 });
343 expect(res.status).toBe(200);
344 return ((await res.json()) as { rows: { group: string; value: number }[] }).rows;
345 };
346 expect(await ask("cls_p75")).toEqual([{ group: "/x", value: 0.1 }, { group: "/y", value: 0.02 }]);
347 expect(await ask("ttfb_p75")).toEqual([{ group: "/x", value: 30 }, { group: "/y", value: 5 }]);
348 expect((await q("bob", { source: "reach", aggregate: "count" })).status).toBe(403);
349 app.db.run("DELETE FROM page_vital");
350 });
351
332352 test("reach: seeded vitals and sessions give the expected p75, p95 and counts, to an admin only", async () => {
333353 const ins = app.db.prepare(
334354 "INSERT INTO page_vital (day, at, route, visitor, ttfb, lcp, inp, cls) VALUES (?, 0, ?, ?, ?, ?, ?, 0)",

↑↓ navigate ↵ open ⌘↵ its app esc close

Keyboard shortcuts

Docs