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

An app's answers fragment names its Server-Timing phases: session, each read, render #3532

Merged opened this

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

Merged

scout/forge:scout/3515-an-app-s-answers-fragment-names-its-serv → main 1 commit fork

  • Merged. scout/3515-an-app-s-answers-fragment-names-its-serv was merged into main as d008739.

Review

Sign in to review this change.
2 files changed +66 −5
src/web/studio/answers.ts +16 −5
@@ -13,6 +13,7 @@ import { getAppByRepo } from "../../core/apps.ts";
1313 import { listComments, listIssues, type Issue } from "../../core/issues.ts";
1414 import { canSeeOwnerFacts, type Repo } from "../../core/repos.ts";
1515 import { listRuns } from "../../core/runs.ts";
16 import { notePhase, takeSessionTime, timePhase } from "../../core/route_timing.ts";
1617 import { getUsersByIds, type User } from "../../core/users.ts";
1718 import type { Ctx } from "../../http/context.ts";
1819 import { loadRepo, repoBase } from "../chrome.ts";
@@ -75,9 +76,14 @@ export function canSeeAnswers(db: Database, repo: Repo, user: User | undefined):
7576 }
7677
7778 /** The region itself; empty (but present, so it can fill) with no tells yet. */
78 export function answersRegion(ctx: Ctx, repo: Repo, user: User): Html {
79 export function answersRegion(
80 ctx: Ctx,
81 repo: Repo,
82 user: User,
83 answers: readonly Answer[] = answersFor(ctx.app.db, repo, user),
84 ): Html {
7985 const base = repoBase(repo);
80 const rows = answersFor(ctx.app.db, repo, user).map(({ issue, said, working }) => {
86 const rows = answers.map(({ issue, said, working }) => {
8187 const text =
8288 said === undefined
8389 ? null
@@ -106,9 +112,14 @@ export function answersRegion(ctx: Ctx, repo: Repo, user: User): Html {
106112
107113 // GET /:owner/:name/app/answers — the region alone, for the page that shows it.
108114 export function appAnswersFragment(ctx: Ctx): Response {
109 const repo = loadRepo(ctx);
115 notePhase(ctx.request, "session", takeSessionTime(ctx.request));
116 const repo = timePhase(ctx.request, "repo", () => loadRepo(ctx));
110117 const user = ctx.auth.user;
111 if (!repo || !canSeeAnswers(ctx.app.db, repo, user))
118 if (!repo || !timePhase(ctx.request, "access", () => canSeeAnswers(ctx.app.db, repo, user)))
112119 return fragment(html``, { status: 404 });
113 return fragment(answersRegion(ctx, repo, user), { request: ctx.request });
120 const answers = timePhase(ctx.request, "answers", () => answersFor(ctx.app.db, repo, user!));
121 const rendering = performance.now();
122 const region = answersRegion(ctx, repo, user!, answers);
123 notePhase(ctx.request, "render", performance.now() - rendering);
124 return fragment(region, { request: ctx.request });
114125 }
test/app-answers-timing.test.ts added +50
@@ -0,0 +1,50 @@
1 /** An app's answers fragment names its phases in Server-Timing (#3515). */
2 import { afterAll, beforeAll, expect, test } from "bun:test";
3 import { mkdtemp, rm } from "node:fs/promises";
4 import { tmpdir } from "node:os";
5 import { join } from "node:path";
6 import { createApp, type App } from "../src/app.ts";
7 import { createRepo } from "../src/actions/repos.ts";
8 import { createApp as insertApp } from "../src/core/apps.ts";
9 import { serve, type Servers } from "../src/http/server.ts";
10 import { seedAdmin } from "./helpers/admin.ts";
11
12 let dataDir = "";
13 let app: App, servers: Servers, bearer = "";
14
15 beforeAll(async () => {
16 dataDir = await mkdtemp(join(tmpdir(), "forge-anstime-"));
17 app = createApp({ dataDir, port: 0 });
18 servers = await serve(app);
19 const admin = await seedAdmin(app, { username: "alice", email: "[email protected]", password: "correct-horse-battery" });
20 if (!admin.ok) throw new Error(admin.error);
21 const made = await createRepo(app, admin.value, { name: "notes", visibility: "public", description: "" });
22 if (!made.ok) throw new Error(String(made.error));
23 const row = insertApp(app.db, { repoId: made.value.id, slug: "notes", access: "public" });
24 if (!row.ok) throw new Error(row.error);
25 const minted = await fetch(`${servers.url}/api/v1/tokens`, {
26 method: "POST",
27 headers: { "content-type": "application/json" },
28 body: JSON.stringify({ adminConfirm: true, username: "alice", password: "correct-horse-battery", name: "cli", scope: "admin" }),
29 });
30 bearer = ((await minted.json()) as { token: string }).token;
31 });
32
33 afterAll(async () => {
34 servers.stop();
35 app.close();
36 await rm(dataDir, { recursive: true, force: true });
37 });
38
39 test("the answers fragment names session, repo, access, answers and render once each", async () => {
40 const res = await fetch(`${servers.url}/alice/notes/app/answers`, { headers: { authorization: `Bearer ${bearer}` } });
41 expect(res.status).toBe(200);
42 expect(await res.text()).toContain("live-answers");
43 const parts = (res.headers.get("server-timing") ?? "").split(", ");
44 const names = parts.map((p) => p.split(";")[0]!);
45 for (const n of ["session", "repo", "access", "answers", "render"]) {
46 expect(names.filter((x) => x === n).length).toBe(1);
47 const part = parts.find((p) => p.startsWith(`${n};`))!;
48 expect(Number(/dur=([\d.]+)/.exec(part)?.[1])).not.toBeNaN();
49 }
50 });

↑↓ navigate ↵ open ⌘↵ its app esc close

Keyboard shortcuts

Docs