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

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

Merged opened this

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

Merged

scout/forge:scout/3516-an-app-s-state-fragment-names-its-server → main 1 commit fork

  • Merged. scout/3516-an-app-s-state-fragment-names-its-server was merged into main as 2ddfab8.

Review

Sign in to review this change.
2 files changed +71 −10
src/web/studio/previews.ts +20 −10
@@ -7,6 +7,7 @@
77 * deploy event refreshes (`appStateFragment`) share.
88 */
99
10 import { notePhase, takeSessionTime, timePhase } from "../../core/route_timing.ts";
1011 import { audienceFacts, audienceOf } from "../../core/audience.ts";
1112 import { sealedScope } from "../../core/sealed.ts";
1213 import { terminalFor } from "../../actions/apps.ts";
@@ -357,16 +358,25 @@ export function appPanels(
357358 * the studio page: a viewer who cannot see the app gets the same 404.
358359 */
359360 export function appStateFragment(ctx: Ctx): Response {
360 const repo = loadRepo(ctx);
361 const req = ctx.request;
362 notePhase(req, "session", takeSessionTime(req));
363 const repo = timePhase(req, "repo", () => loadRepo(ctx));
361364 if (!repo) return fragment(html``, { status: 404 });
362 const record = getAppByRepo(ctx.app.db, repo.id);
365 const record = timePhase(req, "record", () => getAppByRepo(ctx.app.db, repo.id));
363366 if (!record) return fragment(html``, { status: 404 });
364 const openPulls = listPulls(ctx.app.db, repo.id, {
365 state: "open",
366 limit: 10,
367 ...sealedScope(ctx.app.db, repo, ctx.auth.user),
368 });
369 const { head, previews } = appPanels(ctx, repo, record, openPulls);
370 return fragment(html`<div id="app-head">${head}</div>
371 <div id="app-previews">${previews}</div>`, { request: ctx.request });
367 const openPulls = timePhase(req, "pulls", () =>
368 listPulls(ctx.app.db, repo.id, {
369 state: "open",
370 limit: 10,
371 ...sealedScope(ctx.app.db, repo, ctx.auth.user),
372 }),
373 );
374 const { head, previews } = timePhase(req, "panels", () =>
375 appPanels(ctx, repo, record, openPulls),
376 );
377 const rendering = performance.now();
378 const res = fragment(html`<div id="app-head">${head}</div>
379 <div id="app-previews">${previews}</div>`, { request: req });
380 notePhase(req, "render", performance.now() - rendering);
381 return res;
372382 }
test/app-state-timing.test.ts added +51
@@ -0,0 +1,51 @@
1 /** An app's state fragment names its phases in Server-Timing (#3516). */
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-astate-"));
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 app state fragment names session, each read, panels and render once each", async () => {
40 const res = await fetch(`${servers.url}/alice/notes/app/state`, { headers: { authorization: `Bearer ${bearer}` } });
41 const body = await res.text();
42 expect(res.status).toBe(200);
43 expect(body).toContain('id="app-head"');
44 const parts = (res.headers.get("server-timing") ?? "").split(", ");
45 const names = parts.map((p) => p.split(";")[0]!);
46 for (const n of ["session", "repo", "record", "pulls", "panels", "render"]) {
47 expect(names.filter((x) => x === n).length).toBe(1);
48 const part = parts.find((p) => p.startsWith(`${n};`))!;
49 expect(Number(/dur=([\d.]+)/.exec(part)?.[1])).not.toBeNaN();
50 }
51 });

↑↓ navigate ↵ open ⌘↵ its app esc close

Keyboard shortcuts

Docs