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

Merged opened this

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

Merged

scout/forge:scout/4322-settings-cloud-audit-organizations-new-a → main 5 commits fork 2 threads · 2 unresolved

  • Merged. scout/4322-settings-cloud-audit-organizations-new-a was merged into main as 9daf829.

Review

Sign in to review this change.
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 {
699699 * theirs, nothing else is.
700700 */
701701 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 {
702707 const runtime = appsRuntime(ctx.app);
703708 const host = (ctx.url.searchParams.get("host") ?? "").trim();
704709 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> {
218218
219219 // GET /settings/cloud/audit
220220 export function cloudAuditPage(ctx: Ctx): Response {
221 notePhase(ctx.request, "session", takeSessionTime(ctx.request));
221222 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({
225226 ctx,
226227 title: "Cloud audit",
227228 body: html`<div class="page-head"><h1>Cloud audit</h1></div>
@@ -244,7 +245,7 @@ export function cloudAuditPage(ctx: Ctx): Response {
244245 ),
245246 "Account settings",
246247 )}`,
247 });
248 }));
248249 }
249250
250251 // POST /settings/cloud/rotate
src/web/pages/org.ts +4 −2
@@ -443,8 +443,10 @@ function orgForm(ctx: Ctx, value: string, error?: string): Response {
443443
444444 // GET /organizations/new
445445 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 );
448450 }
449451
450452 // 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 ];

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.

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]> = [
3030 ["/alice/forge/settings/labels", "#3390"],
3131 ["/alice/forge/settings/prompts", "#3390"],
3232 ["/alice/forge/tells", "#3322"],
33 ["/apps/open", "#3390"],
3433 ["/claim", "#3390"],
3534 ["/live/apps", "#3390"],
3635 ["/live/on-you", "#3390"],
37 ["/organizations/new", "#3390"],
3836 ["/recover", "#3390"],
39 ["/settings/cloud/audit", "#3390"],
4037 ["/signin/parent", "#3390"],
4138 ["/signin/parent/callback", "#3390"],
4239 ["/verify", "#3390"],

↑↓ navigate ↵ open ⌘↵ its app esc close

Keyboard shortcuts

Docs