Public: anyone can read this; the crew can change it.
The asks page's Server-Timing names its phases: session, top, asks, render #3773
Check · Review · Land · Live done — Landed and live in production.
scout/forge:scout/3764-the-asks-page-s-server-timing-names-its → main 2 commits fork 1 thread · 1 unresolved
2 files
changed +111
−6
src/web/studio/asks_page.ts +14 −6
| @@ -7,6 +7,7 @@ | ||
| 7 | 7 | * (`actions/asks_bulk.ts`). `/asks` was its own screen; it lands here now. |
| 8 | 8 | */ |
| 9 | 9 | |
| 10 | import { notePhase, takeSessionTime, timePhase } from "../../core/route_timing.ts"; | |
| 10 | 11 | import type { WaitingFor } from "../../core/waiting_for.ts"; |
| 11 | 12 | import { |
| 12 | 13 | applyAsksBulk, |
| @@ -335,7 +336,8 @@ export interface AsksTop { | ||
| 335 | 336 | /** GET /asks — what needs you as its own page: the home's section and its |
| 336 | 337 | * live script; `?op=<id>` (a bulk action's receipt) rides along. */ |
| 337 | 338 | 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))); | |
| 339 | 341 | } |
| 340 | 342 | |
| 341 | 343 | /** 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 { | ||
| 343 | 345 | const user = ctx.auth.user; |
| 344 | 346 | if (!user) return signInFirst(ctx); |
| 345 | 347 | const section = needsYouSection(ctx, user, top); |
| 346 | return page({ | |
| 348 | const rendering = performance.now(); | |
| 349 | const response = page({ | |
| 347 | 350 | ctx, |
| 348 | 351 | title: "Asks", |
| 349 | 352 | body: html`<main class="stack"><h1 class="sr-only">Asks</h1>${onYouRegion(ctx, user, undefined, section.body)}</main>`, |
| 350 | 353 | scriptJs: `${liveClientJs}\n${liveRegionsJs}\n${section.scriptJs}`, |
| 351 | 354 | }); |
| 355 | notePhase(ctx.request, "render", performance.now() - rendering); | |
| 356 | return response; | |
| 352 | 357 | } |
| 353 | 358 | |
| 354 | 359 | /** The receipt `?op=<id>` names on the home, when it is the viewer's own. */ |
| @@ -393,10 +398,13 @@ export function needsYouSection( | ||
| 393 | 398 | top?: AsksTop, |
| 394 | 399 | ): { body: Html; scriptJs: string } { |
| 395 | 400 | 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. | |
| 407 | const decisions = ctx.request instanceof Request ? timePhase(ctx.request, "asks", read) : read(); | |
| 400 | 408 | const needs = forgeNeedsFor(ctx, user); |
| 401 | 409 | const rows = askRows(decisions); |
| 402 | 410 | 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 | }); |
new above
offline
Production code branching for a test stub. Pass the stub a Request so this is just timePhase(...).