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

The asks page's Server-Timing names its phases: session, top, asks, render #3773

Merged opened this

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

Merged

scout/forge:scout/3764-the-asks-page-s-server-timing-names-its → main 2 commits fork 1 thread · 1 unresolved

  • Merged. scout/3764-the-asks-page-s-server-timing-names-its was merged into main as d480f6f.

Review

Sign in to review this change.
2 files changed +111 −6
src/web/studio/asks_page.ts +14 −6
@@ -7,6 +7,7 @@
77 * (`actions/asks_bulk.ts`). `/asks` was its own screen; it lands here now.
88 */
99
10 import { notePhase, takeSessionTime, timePhase } from "../../core/route_timing.ts";
1011 import type { WaitingFor } from "../../core/waiting_for.ts";
1112 import {
1213 applyAsksBulk,
@@ -335,7 +336,8 @@ export interface AsksTop {
335336 /** GET /asks — what needs you as its own page: the home's section and its
336337 * live script; `?op=<id>` (a bulk action's receipt) rides along. */
337338 export function asksPage(ctx: Ctx): Response {
338 return renderAsks(ctx, homeTop(ctx));
339 notePhase(ctx.request, "session", takeSessionTime(ctx.request));
340 return renderAsks(ctx, timePhase(ctx.request, "top", () => homeTop(ctx)));
339341 }
340342
341343 /** The asks page, with a bulk step's preview or receipt on top. */
@@ -343,12 +345,15 @@ function renderAsks(ctx: Ctx, top: AsksTop | undefined): Response {
343345 const user = ctx.auth.user;
344346 if (!user) return signInFirst(ctx);
345347 const section = needsYouSection(ctx, user, top);
346 return page({
348 const rendering = performance.now();
349 const response = page({
347350 ctx,
348351 title: "Asks",
349352 body: html`<main class="stack"><h1 class="sr-only">Asks</h1>${onYouRegion(ctx, user, undefined, section.body)}</main>`,
350353 scriptJs: `${liveClientJs}\n${liveRegionsJs}\n${section.scriptJs}`,
351354 });
355 notePhase(ctx.request, "render", performance.now() - rendering);
356 return response;
352357 }
353358
354359 /** The receipt `?op=<id>` names on the home, when it is the viewer's own. */
@@ -393,10 +398,13 @@ export function needsYouSection(
393398 top?: AsksTop,
394399 ): { body: Html; scriptJs: string } {
395400 const picked = top?.picked ?? new Set<string>();
396 const decisions = decisionsFor(
397 ctx,
398 listReposVisibleTo(ctx.app.db, user, { limit: 200, withoutArchived: true }),
399 );
401 const read = () =>
402 decisionsFor(
403 ctx,
404 listReposVisibleTo(ctx.app.db, user, { limit: 200, withoutArchived: true }),
405 );
406 // Callers that build the section outside a request have none to time against.

Production code branching for a test stub. Pass the stub a Request so this is just timePhase(...).

407 const decisions = ctx.request instanceof Request ? timePhase(ctx.request, "asks", read) : read();
400408 const needs = forgeNeedsFor(ctx, user);
401409 const rows = askRows(decisions);
402410 const silenced = silencedRepos(ctx.app.db, user);
test/asks-timing.test.ts added +97
@@ -0,0 +1,97 @@
1 /** `/asks` names session, top, asks and render in Server-Timing (#3764). */
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 { addToCrew, createAgent } from "../src/actions/crew.ts";
8 import { assignIssue } from "../src/actions/issues.ts";
9 import { openPull } from "../src/actions/pulls.ts";
10 import { commitToBranch, createRepo } from "../src/actions/repos.ts";
11 import { getIssueById } from "../src/core/issues.ts";
12 import { claimRun, createRun, finishRun } from "../src/core/runs.ts";
13 import { createUser, getUserByName } from "../src/core/users.ts";
14 import { serve, type Servers } from "../src/http/server.ts";
15 import { quietLogs } from "./helpers/globals.ts";
16
17 quietLogs();
18
19 let root = "";
20 let app: App;
21 let servers: Servers;
22
23 beforeAll(async () => {
24 root = mkdtempSync(join(tmpdir(), "forge-asks-timing-"));
25 app = createApp({ dataDir: root, port: 0 });
26 const made = await createUser(app.db, {
27 origin: "seed",
28 trusted: true,
29 username: "alice",
30 email: "[email protected]",
31 password: "a-good-passphrase",
32 adminIfFirst: true,
33 });
34 if (!made.ok) throw new Error(made.error);
35 const alice = made.value;
36 await createAgent(app, alice, { username: "bot" });
37 const r = await createRepo(app, alice, { name: "work", visibility: "public" });
38 if (!r.ok) throw new Error(String(r.error));
39 const repo = r.value;
40 await commitToBranch(app, alice, repo, { message: "seed", files: [{ path: "README.md", content: "# w\n" }] });
41 addToCrew(app, alice, repo, "bot");
42 await commitToBranch(app, alice, repo, {
43 branch: "bot/1",
44 base: "main",
45 message: "Work",
46 files: [{ path: "F.md", content: "1\n" }],
47 });
48 const opened = await openPull(app, getUserByName(app.db, "bot")!, repo, {
49 title: "Work",
50 body: "",
51 baseBranch: "main",
52 headBranch: "bot/1",
53 });
54 if (!opened.ok) throw new Error(String(opened.error));
55 const run = createRun(app.db, {
56 repoId: repo.id,
57 kind: "check",
58 issueId: opened.value.id,
59 env: `pull-${opened.value.number}`,
60 commitOid: opened.value.headOid,
61 reason: "pull",
62 });
63 claimRun(app.db, run.id);
64 finishRun(app.db, run.id, { state: "ok", summary: "ok" });
65 assignIssue(app, alice, repo, getIssueById(app.db, opened.value.id)!, alice);
66 servers = await serve(app);
67 }, 30_000);
68 afterAll(() => {
69 servers.stop();
70 app.close();
71 rmSync(root, { recursive: true, force: true });
72 });
73
74 const names = (res: Response): string[] =>
75 (res.headers.get("server-timing") ?? "").split(", ").map((p) => p.split(";")[0]!);
76
77 test("GET /asks names its phases for someone with an ask", async () => {
78 const signin = await fetch(`${servers.url}/signin`, {
79 method: "POST",
80 headers: { origin: servers.url },
81 body: new URLSearchParams({ username: "alice", password: "a-good-passphrase" }),
82 redirect: "manual",
83 });
84 const cookie = (signin.headers.get("set-cookie") ?? "").split(";")[0]!;
85 const res = await fetch(`${servers.url}/asks`, { headers: { cookie } });
86 const body = await res.text();
87 expect(res.status).toBe(200);
88 expect(body).toContain("Work");
89 expect(names(res)).toEqual(expect.arrayContaining(["app", "session", "top", "asks", "render"]));
90 });
91
92 test("signed out, /asks still redirects and names session", async () => {
93 const res = await fetch(`${servers.url}/asks`, { redirect: "manual" });
94 expect(res.status).toBe(303);
95 expect(res.headers.get("location")).toContain("/signin");
96 expect(names(res)).toContain("session");
97 });

↑↓ navigate ↵ open ⌘↵ its app esc close

Keyboard shortcuts

Docs