Public: anyone can read this; the crew can change it.
/settings/cloud/audit, /organizations/new and /apps/open name their Server-Timing phases and answer under 50 ms #4326
Check · Review · Land · Live done — Landed and live in production.
scout/forge:scout/4322-settings-cloud-audit-organizations-new-a → main 5 commits fork 2 threads · 2 unresolved
5 files
changed +101
−9
src/web/pages/apps.ts +5
| @@ -699,6 +699,11 @@ export function previewCard(ctx: Ctx, repo: Repo, pull: Pull): Html { | ||
| 699 | 699 | * theirs, nothing else is. |
| 700 | 700 | */ |
| 701 | 701 | export function appsOpen(ctx: Ctx): Response { |
| 702 | notePhase(ctx.request, "session", takeSessionTime(ctx.request)); | |
| 703 | return timePhase(ctx.request, "render", () => appsOpenResponse(ctx)); | |
| 704 | } | |
| 705 | ||
| 706 | function appsOpenResponse(ctx: Ctx): Response { | |
| 702 | 707 | const runtime = appsRuntime(ctx.app); |
| 703 | 708 | const host = (ctx.url.searchParams.get("host") ?? "").trim(); |
| 704 | 709 | const wanted = ctx.url.searchParams.get("to") ?? "/"; |
src/web/pages/cloud.ts +5 −4
| @@ -218,10 +218,11 @@ export async function cloudAudit(ctx: Ctx): Promise<Response> { | ||
| 218 | 218 | |
| 219 | 219 | // GET /settings/cloud/audit |
| 220 | 220 | export function cloudAuditPage(ctx: Ctx): Response { |
| 221 | notePhase(ctx.request, "session", takeSessionTime(ctx.request)); | |
| 221 | 222 | const denied = requireAdmin(ctx); |
| 222 | if (denied) return denied; | |
| 223 | const rows = auditToday(ctx.app.db); | |
| 224 | return page({ | |
| 223 | if (denied) return timePhase(ctx.request, "render", () => denied); | |
| 224 | const rows = timePhase(ctx.request, "audit", () => auditToday(ctx.app.db)); | |
| 225 | return timePhase(ctx.request, "render", () => page({ | |
| 225 | 226 | ctx, |
| 226 | 227 | title: "Cloud audit", |
| 227 | 228 | body: html`<div class="page-head"><h1>Cloud audit</h1></div> |
| @@ -244,7 +245,7 @@ export function cloudAuditPage(ctx: Ctx): Response { | ||
| 244 | 245 | ), |
| 245 | 246 | "Account settings", |
| 246 | 247 | )}`, |
| 247 | }); | |
| 248 | })); | |
| 248 | 249 | } |
| 249 | 250 | |
| 250 | 251 | // POST /settings/cloud/rotate |
src/web/pages/org.ts +4 −2
| @@ -443,8 +443,10 @@ function orgForm(ctx: Ctx, value: string, error?: string): Response { | ||
| 443 | 443 | |
| 444 | 444 | // GET /organizations/new |
| 445 | 445 | export function newOrgPage(ctx: Ctx): Response { |
| 446 | if (!ctx.auth.user) return signInFirst(ctx); | |
| 447 | return orgForm(ctx, ""); | |
| 446 | notePhase(ctx.request, "session", takeSessionTime(ctx.request)); | |
| 447 | return timePhase(ctx.request, "render", () => | |
| 448 | ctx.auth.user ? orgForm(ctx, "") : signInFirst(ctx), | |
| 449 | ); | |
| 448 | 450 | } |
| 449 | 451 | |
| 450 | 452 | // POST /organizations/new |
test/cloud-org-apps-timing.test.ts added +87
| @@ -0,0 +1,87 @@ | ||
| 1 | /** `/settings/cloud/audit`, `/organizations/new` and `/apps/open` name their Server-Timing phases and answer under 50 ms (#4322). */ | |
| 2 | import { afterAll, beforeAll, expect, test } from "bun:test"; | |
| 3 | import { mkdtempSync, rmSync } from "node:fs"; | |
| 4 | import { tmpdir } from "node:os"; | |
| 5 | import { join } from "node:path"; | |
| 6 | import { createApp, type App } from "../src/app.ts"; | |
| 7 | import { createUser } from "../src/core/users.ts"; | |
| 8 | import { serve, type Servers } from "../src/http/server.ts"; | |
| 9 | import { proveAll } from "./helpers/verified.ts"; | |
| 10 | import { quietLogs } from "./helpers/globals.ts"; | |
| 11 | ||
| 12 | quietLogs(); | |
| 13 | ||
| 14 | let root = ""; | |
| 15 | let app: App; | |
| 16 | let servers: Servers; | |
| 17 | let cookie = ""; | |
| 18 | ||
| 19 | beforeAll(async () => { | |
| 20 | root = mkdtempSync(join(tmpdir(), "forge-cloud-timing-")); | |
| 21 | app = createApp({ dataDir: root, port: 0 }); | |
| 22 | const u = await createUser(app.db, { | |
| 23 | origin: "seed", | |
| 24 | trusted: true, | |
| 25 | username: "alice", | |
| 26 | email: "[email protected]", | |
| 27 | password: "a-good-passphrase", | |
| 28 | adminIfFirst: true, | |
| 29 | }); | |
| 30 | if (!u.ok) throw new Error(u.error); | |
| 31 | proveAll(app); | |
| 32 | servers = await serve(app); | |
| 33 | const signin = await fetch(`${servers.url}/signin`, { | |
| 34 | method: "POST", | |
| 35 | headers: { origin: servers.url }, | |
| 36 | body: new URLSearchParams({ username: "alice", password: "a-good-passphrase" }), | |
| 37 | redirect: "manual", | |
| 38 | }); | |
| 39 | cookie = (signin.headers.get("set-cookie") ?? "").split(";")[0]!; | |
| 40 | }); | |
| 41 | afterAll(() => { | |
| 42 | servers.stop(); | |
| 43 | app.close(); | |
| 44 | rmSync(root, { recursive: true, force: true }); | |
| 45 | }); | |
| 46 | ||
| 47 | // /apps/open with no host is the answer a stranger gets: session, then render. | |
| 48 | const PAGES: [string, string[]][] = [ | |
| 49 | ["/settings/cloud/audit", ["session", "audit", "render"]], | |
| 50 | ["/organizations/new", ["session", "render"]], | |
| 51 | ["/apps/open", ["session", "render"]], | |
| 52 | ]; | |
| 53 | ||
| 54 | for (const [path, names] of PAGES) | |
| 55 | test(`GET ${path} names its phases and answers under 50 ms`, async () => { | |
| 56 | const get = () => fetch(`${servers.url}${path}`, { headers: { cookie }, redirect: "manual" }); | |
| 57 | await (await get()).text(); // warm | |
| 58 | // Timers jitter on a busy machine: judge the best of several requests. | |
| 59 | let left = Infinity; | |
| 60 | let total = Infinity; | |
| 61 | for (let i = 0; i < 7; i++) { | |
| 62 | const res = await get(); | |
| 63 | await res.text(); | |
| 64 | const parts = (res.headers.get("server-timing") ?? "").split(", "); | |
| 65 | const dur = (n: string) => Number(parts.find((p) => p.startsWith(`${n};`))!.split("dur=")[1]); | |
| 66 | for (const name of ["app", ...names]) | |
| 67 | expect(parts.find((p) => p.startsWith(`${name};`))).toMatch(/^[a-z]+;dur=\d+\.\d$/); | |
| 68 | total = Math.min(total, dur("app")); | |
| 69 | // Phases name the whole request: what is left over is small. | |
| 70 | left = Math.min(left, Math.abs(dur("app") - names.reduce((sum, n) => sum + dur(n), 0))); | |
| 71 | } | |
| 72 | expect(total).toBeLessThan(50); | |
| 73 | expect(left).toBeLessThan(1.5); | |
| 74 | }); | |
| 75 | ||
| 76 | // Signed out, the redirect to sign-in is still a named phase. | |
| 77 | for (const path of ["/settings/cloud/audit", "/organizations/new"]) | |
| 78 | test(`signed-out GET ${path} names session and render`, async () => { | |
| 79 | const res = await fetch(`${servers.url}${path}`, { redirect: "manual" }); | |
| 80 | await res.text(); | |
| 81 | expect(res.status).toBe(303); | |
| 82 | const parts = (res.headers.get("server-timing") ?? "").split(", "); | |
| 83 | const dur = (n: string) => Number(parts.find((p) => p.startsWith(`${n};`))!.split("dur=")[1]); | |
| 84 | for (const name of ["session", "render"]) | |
| 85 | expect(parts.find((p) => p.startsWith(`${name};`))).toMatch(/^[a-z]+;dur=\d+\.\d$/); | |
| 86 | expect(Math.abs(dur("app") - dur("session") - dur("render"))).toBeLessThan(1.5); | |
| 87 | }); |
test/server-timing-sweep.test.ts −3
| @@ -30,13 +30,10 @@ const UNNAMED: Array<[path: string, issue: string]> = [ | ||
| 30 | 30 | ["/alice/forge/settings/labels", "#3390"], |
| 31 | 31 | ["/alice/forge/settings/prompts", "#3390"], |
| 32 | 32 | ["/alice/forge/tells", "#3322"], |
| 33 | ["/apps/open", "#3390"], | |
| 34 | 33 | ["/claim", "#3390"], |
| 35 | 34 | ["/live/apps", "#3390"], |
| 36 | 35 | ["/live/on-you", "#3390"], |
| 37 | ["/organizations/new", "#3390"], | |
| 38 | 36 | ["/recover", "#3390"], |
| 39 | ["/settings/cloud/audit", "#3390"], | |
| 40 | 37 | ["/signin/parent", "#3390"], |
| 41 | 38 | ["/signin/parent/callback", "#3390"], |
| 42 | 39 | ["/verify", "#3390"], |
new above
offline
Only the no-host path of /apps/open is exercised, so host, repo and link would not fail this test if they were dropped. Add a fixture with an app runtime and assert them.