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
Check · Review · Land · Live done — Landed and live in production.
scout/forge:scout/3167-the-page-vital-board-source-reports-cls → main 1 commit fork 1 thread · 1 unresolved
2 files
changed +24
−2
src/boards/sources.ts +4 −2
| @@ -233,6 +233,8 @@ export const SOURCES: Readonly<Record<string, Source>> = { | ||
| 233 | 233 | percentile("load_p75", "load", 75), |
| 234 | 234 | percentile("lcp_p75", "lcp", 75), |
| 235 | 235 | percentile("inp_p75", "inp", 75), |
| 236 | percentile("cls_p75", "cls", 75), | |
| 237 | percentile("ttfb_p75", "ttfb", 75), | |
| 236 | 238 | percentile("server_p50", "ttfb", 50), |
| 237 | 239 | percentile("server_p95", "ttfb", 95), |
| 238 | 240 | `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>> = { | ||
| 243 | 245 | columns: { metric: "text", route: "text", at: "time", value: "number" }, |
| 244 | 246 | docs: { |
| 245 | 247 | 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", | |
| 248 | 250 | route: "the route pattern, never the path; empty for visitors and accounts", |
| 249 | 251 | at: "the day (UTC) for visitors and the percentiles; now for the account counts, which are not kept per day", |
| 250 | 252 | 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", () => { | ||
| 329 | 329 | } |
| 330 | 330 | }); |
| 331 | 331 | |
| 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 | ||
| 332 | 352 | test("reach: seeded vitals and sessions give the expected p75, p95 and counts, to an admin only", async () => { |
| 333 | 353 | const ins = app.db.prepare( |
| 334 | 354 | "INSERT INTO page_vital (day, at, route, visitor, ttfb, lcp, inp, cls) VALUES (?, 0, ?, ?, ?, ?, ?, 0)", |
new above
offline
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.