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

The audit page's Server-Timing names its phases: session, each read, render #3440

Merged opened this

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

Merged

scout/forge:scout/3405-the-audit-page-s-server-timing-names-its → main 1 commit fork

  • Merged. scout/3405-the-audit-page-s-server-timing-names-its was merged into main as d810c16.

Review

Sign in to review this change.
2 files changed +98 −20
src/web/pages/audit.ts +41 −20
@@ -17,6 +17,12 @@ import { appsRuntime } from "../../apps/runtime.ts";
1717 import { appsForRepos, getAppByRepo } from "../../core/apps.ts";
1818 import { canWrite, listReposVisibleTo, type Repo } from "../../core/repos.ts";
1919 import type { Ctx } from "../../http/context.ts";
20 import {
21 notePhase,
22 takeSessionTime,
23 timePhase,
24 timePhaseAsync,
25 } from "../../core/route_timing.ts";
2026 import { audit, type Audited } from "../../contract/audit.ts";
2127 import {
2228 contractChanges,
@@ -52,21 +58,27 @@ import {
5258 const AUDIT_LIMIT = 200;
5359
5460 export async function auditPage(ctx: Ctx): Promise<Response> {
55 const repos = listReposVisibleTo(ctx.app.db, ctx.auth.user, {
56 limit: AUDIT_LIMIT,
57 });
58 const records = appsForRepos(
59 ctx.app.db,
60 repos.map((r) => r.id),
61 const req = ctx.request;
62 notePhase(req, "session", takeSessionTime(req));
63 const repos = timePhase(req, "repos", () =>
64 listReposVisibleTo(ctx.app.db, ctx.auth.user, { limit: AUDIT_LIMIT }),
65 );
66 const records = timePhase(req, "apps", () =>
67 appsForRepos(
68 ctx.app.db,
69 repos.map((r) => r.id),
70 ),
6171 );
6272 const apps = repos.filter((r) => records.has(r.id));
63 const audited = await Promise.all(
64 apps.map((repo) => audit(ctx.app.reposRoot, repo)),
73 const audited = await timePhaseAsync(req, "contracts", () =>
74 Promise.all(apps.map((repo) => audit(ctx.app.reposRoot, repo))),
6575 );
6676 const sound = audited.filter((a) => a.standing.kind === "sound");
67 const probed = latestProbes(
68 ctx.app.db,
69 sound.map((a) => records.get(a.repo.id)!.id),
77 const probed = timePhase(req, "probes", () =>
78 latestProbes(
79 ctx.app.db,
80 sound.map((a) => records.get(a.repo.id)!.id),
81 ),
7082 );
7183 const disobeying = sound.filter(
7284 (a) => probed.get(records.get(a.repo.id)!.id)?.verdict === "mismatch",
@@ -80,21 +92,28 @@ export async function auditPage(ctx: Ctx): Promise<Response> {
8092 // The third lens: what moved. Contracts that changed, pulls at the wall,
8193 // apps whose doors started or stopped agreeing with their table.
8294 const changes = (
83 await Promise.all(
84 audited
85 .filter((a) => a.standing.kind !== "unread")
86 .map((a) => contractChanges(ctx.app.reposRoot, a.repo)),
95 await timePhaseAsync(req, "history", () =>
96 Promise.all(
97 audited
98 .filter((a) => a.standing.kind !== "unread")
99 .map((a) => contractChanges(ctx.app.reposRoot, a.repo)),
100 ),
87101 )
88102 )
89103 .flat()
90104 .sort((a, b) => (a.date < b.date ? 1 : a.date > b.date ? -1 : 0))
91105 .slice(0, 30);
92 const crossings = crossingsWaiting(ctx.app.db, apps);
93 const verdicts = verdictChanges(
94 ctx.app.db,
95 sound.map((a) => ({ repo: a.repo, appId: records.get(a.repo.id)!.id })),
106 const crossings = timePhase(req, "crossings", () =>
107 crossingsWaiting(ctx.app.db, apps),
96108 );
97 return page({
109 const verdicts = timePhase(req, "verdicts", () =>
110 verdictChanges(
111 ctx.app.db,
112 sound.map((a) => ({ repo: a.repo, appId: records.get(a.repo.id)!.id })),
113 ),
114 );
115 const rendering = performance.now();
116 const response = page({
98117 ctx,
99118 title: "Audit",
100119 description:
@@ -141,6 +160,8 @@ export async function auditPage(ctx: Ctx): Promise<Response> {
141160 ${when(unread.length > 0, () => unreadCard(unread))}
142161 ${changesSection(crossings, changes, verdicts)}`}`,
143162 });
163 notePhase(req, "render", performance.now() - rendering);
164 return response;
144165 }
145166
146167 const count = (n: number, one: string, many: string): string =>
test/audit-timing.test.ts added +57
@@ -0,0 +1,57 @@
1 /** `audit` names its phases in Server-Timing (#3405). */
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 { createRepo } from "../src/actions/repos.ts";
7 import { createApp, type App } from "../src/app.ts";
8 import { createUser } from "../src/core/users.ts";
9 import { serve, type Servers } from "../src/http/server.ts";
10 import { proveAll } from "./helpers/verified.ts";
11 import { quietLogs } from "./helpers/globals.ts";
12
13 quietLogs();
14
15 let root = "";
16 let app: App;
17 let servers: Servers;
18
19 beforeAll(async () => {
20 root = mkdtempSync(join(tmpdir(), "forge-audit-timing-"));
21 app = createApp({ dataDir: root, port: 0, forgeRepo: "alice/forge" });
22 const u = await createUser(app.db, {
23 origin: "seed",
24 trusted: true,
25 username: "alice",
26 email: "[email protected]",
27 password: "a-good-passphrase",
28 adminIfFirst: true,
29 });
30 if (!u.ok) throw new Error(u.error);
31 proveAll(app);
32 const made = await createRepo(app, { id: u.value.id, username: "alice" } as never, { name: "forge" });
33 if (!made.ok) throw new Error(made.error);
34 servers = await serve(app);
35 });
36 afterAll(() => {
37 servers.stop();
38 app.close();
39 rmSync(root, { recursive: true, force: true });
40 });
41
42 test("GET /audit names its phases", async () => {
43 const signin = await fetch(`${servers.url}/signin`, {
44 method: "POST",
45 headers: { origin: servers.url },
46 body: new URLSearchParams({ username: "alice", password: "a-good-passphrase" }),
47 redirect: "manual",
48 });
49 const cookie = (signin.headers.get("set-cookie") ?? "").split(";")[0]!;
50 const res = await fetch(`${servers.url}/audit`, { headers: { cookie } });
51 const body = await res.text();
52 expect(res.status).toBe(200);
53 expect(body).toContain("<h1>Audit</h1>");
54 const parts = (res.headers.get("server-timing") ?? "").split(", ");
55 for (const name of ["app", "session", "repos", "apps", "contracts", "probes", "history", "crossings", "verdicts", "render"])
56 expect(parts.find((p) => p.startsWith(`${name};`))).toMatch(/^[a-z]+;dur=\d+\.\d$/);
57 });

↑↓ navigate ↵ open ⌘↵ its app esc close

Keyboard shortcuts

Docs