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

A milestone's limb preview page's Server-Timing names its phases: session, each read, render #3540

Merged opened this

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

Merged

scout/forge:scout/3510-a-milestone-s-limb-preview-page-s-server → main 1 commit fork 1 thread · 1 unresolved

  • Merged. scout/3510-a-milestone-s-limb-preview-page-s-server was merged into main as 15f8375.

Review

Sign in to review this change.
2 files changed +92 −9
src/web/pages/limb.ts +19 −9
@@ -19,6 +19,7 @@ import {
1919 import type { FleetValue, Operation } from "../../core/fleet_ops.ts";
2020 import { getIssuesByIds } from "../../core/issues.ts";
2121 import { pillarsOf } from "../../core/pillars.ts";
22 import { notePhase, takeSessionTime, timePhase } from "../../core/route_timing.ts";
2223 import type { Ctx } from "../../http/context.ts";
2324 import {
2425 crumbsFor,
@@ -148,7 +149,8 @@ function requestOf(
148149
149150 /** GET /:owner/:name/milestones/:number/limb?action= — what it will do, before it does. */
150151 export function limbPreviewPage(ctx: Ctx): Response {
151 const repo = loadRepo(ctx);
152 notePhase(ctx.request, "session", takeSessionTime(ctx.request));
153 const repo = timePhase(ctx.request, "repo", () => loadRepo(ctx));
152154 if (!repo) return notFoundPage(ctx);
153155 if (!ctx.auth.user) return signInFirst(ctx);
154156 const number = Number(ctx.params.number);
@@ -158,12 +160,15 @@ export function limbPreviewPage(ctx: Ctx): Response {
158160 return notFoundPage(ctx);
159161 const request = requestOf(action, (n) => ctx.url.searchParams.get(n) ?? "");
160162 // A close is planned before its reason is typed: the rows do not depend on it.
161 const plan = previewLimb(ctx.app, ctx.auth.user, repo, number, {
162 ...request,
163 ...(action === "close" && request.reason === undefined
164 ? { reason: "…" }
165 : {}),
166 });
163 const user = ctx.auth.user;
164 const plan = timePhase(ctx.request, "plan", () =>
165 previewLimb(ctx.app, user, repo, number, {
166 ...request,
167 ...(action === "close" && request.reason === undefined
168 ? { reason: "…" }
169 : {}),
170 }),
171 );
167172 const base = repoBase(repo);
168173 const words = WORDS[action];
169174 if (!plan.ok) {
@@ -186,7 +191,10 @@ export function limbPreviewPage(ctx: Ctx): Response {
186191 const willWrite = p.counts.change > 0;
187192 const moving = action === "raise" || action === "lower";
188193 const pillars =
189 action === "resume" ? pillarsOf(ctx.app.db, repo).pillars : [];
194 action === "resume"
195 ? timePhase(ctx.request, "pillars", () => pillarsOf(ctx.app.db, repo).pillars)

This line is over 80 columns; wrap it like the surrounding code, e.g. put the arrow function body on its own lines.

196 : [];
197 const rendering = performance.now();
190198 const body = html`${repoHeader(ctx, repo, "map")}
191199 <div class="stack mt">
192200 <p class="small muted">
@@ -324,13 +332,15 @@ export function limbPreviewPage(ctx: Ctx): Response {
324332 Nothing here is yours to change.
325333 <a href="${milestoneHref(base, number)}">Back</a>
326334 </p>`}`;
327 return page({
335 const response = page({
328336 ctx,
329337 title: `${words.ask} ${p.limb.title} · ${repo.name}`,
330338 crumbs: crumbsFor(repo),
331339 palette: repoPalette(ctx, repo),
332340 body,
333341 });
342 notePhase(ctx.request, "render", performance.now() - rendering);
343 return response;
334344 }
335345
336346 /** POST /:owner/:name/milestones/:number/limb — apply, and show the receipt. */
test/limb-timing.test.ts added +73
@@ -0,0 +1,73 @@
1 /** The limb preview page names session, repo, plan, pillars and render in Server-Timing (#3510). */
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 { openIssue } from "../src/actions/issues.ts";
7 import { createRepo } from "../src/actions/repos.ts";
8 import { createApp, type App } from "../src/app.ts";
9 import { getRepoById } from "../src/core/repos.ts";
10 import { createUser } from "../src/core/users.ts";
11 import { serve, type Servers } from "../src/http/server.ts";
12 import { proveAll } from "./helpers/verified.ts";
13 import { quietLogs } from "./helpers/globals.ts";
14
15 quietLogs();
16
17 let root = "";
18 let app: App;
19 let servers: Servers;
20 let number = 0;
21
22 beforeAll(async () => {
23 root = mkdtempSync(join(tmpdir(), "forge-limb-timing-"));
24 app = createApp({ dataDir: root, port: 0, forgeRepo: "alice/forge" });
25 const u = await createUser(app.db, {
26 origin: "seed",
27 trusted: true,
28 username: "alice",
29 email: "[email protected]",
30 password: "a-good-passphrase",
31 adminIfFirst: true,
32 });
33 if (!u.ok) throw new Error(u.error);
34 proveAll(app);
35 const alice = { id: u.value.id, username: "alice" } as never;
36 const made = await createRepo(app, alice, { name: "forge" });
37 if (!made.ok) throw new Error(made.error);
38 const repo = getRepoById(app.db, made.value.id)!;
39 const opened = openIssue(app, u.value, repo, { title: "A limb" });
40 if (!opened.ok) throw new Error(String(opened.error));
41 number = opened.value.number;
42 servers = await serve(app);
43 });
44 afterAll(() => {
45 servers.stop();
46 app.close();
47 rmSync(root, { recursive: true, force: true });
48 });
49
50 test("GET /:owner/:name/milestones/:number/limb names its phases", async () => {
51 const signin = await fetch(`${servers.url}/signin`, {
52 method: "POST",
53 headers: { origin: servers.url },
54 body: new URLSearchParams({ username: "alice", password: "a-good-passphrase" }),
55 redirect: "manual",
56 });
57 const cookie = (signin.headers.get("set-cookie") ?? "").split(";")[0]!;
58 const phases = async (action: string) => {
59 const res = await fetch(
60 `${servers.url}/alice/forge/milestones/${number}/limb?action=${action}`,
61 { headers: { cookie } },
62 );
63 expect(res.status).toBe(200);
64 await res.text();
65 return (res.headers.get("server-timing") ?? "").split(", ");
66 };
67 const defer = await phases("defer");
68 for (const name of ["app", "session", "repo", "plan", "render"])
69 expect(defer.find((p) => p.startsWith(`${name};`))).toMatch(/^[a-z]+;dur=\d+\.\d$/);
70 expect(defer.find((p) => p.startsWith("pillars;"))).toBeUndefined();
71 const resume = await phases("resume");
72 expect(resume.find((p) => p.startsWith("pillars;"))).toMatch(/^pillars;dur=\d+\.\d$/);
73 });

↑↓ navigate ↵ open ⌘↵ its app esc close

Keyboard shortcuts

Docs