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

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

Merged opened this

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

Merged

scout/forge:scout/3426-the-accounts-page-s-server-timing-names → main 1 commit fork

  • Merged. scout/3426-the-accounts-page-s-server-timing-names was merged into main as acc4ce3.

Review

Sign in to review this change.
2 files changed +69 −13
src/web/pages/accounts.ts +24 −13
@@ -28,6 +28,7 @@ import {
2828 } from "../../core/orgs.ts";
2929 import { getUserByName } from "../../core/users.ts";
3030 import type { Ctx } from "../../http/context.ts";
31 import { notePhase, takeSessionTime, timePhase } from "../../core/route_timing.ts";
3132 import { notFoundPage, settingsLayout, signInFirst } from "../chrome.ts";
3233 import { field, readForm } from "../form.ts";
3334 import { html, type Html } from "../html.ts";
@@ -130,18 +131,23 @@ function row(a: AccountRow, sole: readonly string[]): Html {
130131
131132 // GET /settings/accounts
132133 export function accountsPage(ctx: Ctx): Response {
134 notePhase(ctx.request, "session", takeSessionTime(ctx.request));
133135 return accountsView(ctx);
134136 }
135137
136138 /** The page, with what an act needs said first (`notice`) when it did not finish. */
137139 function accountsView(ctx: Ctx, notice?: Html, status = 200): Response {
138140 if (!ctx.auth.user) return signInFirst(ctx);
139 const listed = listAccounts(ctx.app, ctx.auth.user);
141 const user = ctx.auth.user;
142 const listed = timePhase(ctx.request, "accounts", () =>
143 listAccounts(ctx.app, user),
144 );
140145 if (!listed.ok) return notFoundPage(ctx);
141146 const people = listed.value;
142147 // Without mail a newcomer cannot prove an address; the owner's word is
143148 // the only way in, and the page says so where it is given.
144 const noMail = mailConfigured(ctx.app)
149 const mail = timePhase(ctx.request, "mail", () => mailConfigured(ctx.app));
150 const noMail = mail
145151 ? null
146152 : card(
147153 html`<p>
@@ -153,7 +159,9 @@ function accountsView(ctx: Ctx, notice?: Html, status = 200): Response {
153159 );
154160 // An organization every owner of which is suspended or removed stays as
155161 // it is for its members; only the forge's owner can mend it.
156 const orphans = ownerlessOrgs(ctx.app.db);
162 const orphans = timePhase(ctx.request, "orphans", () =>
163 ownerlessOrgs(ctx.app.db),
164 );
157165 const ownerless =
158166 orphans.length === 0
159167 ? null
@@ -173,21 +181,22 @@ function accountsView(ctx: Ctx, notice?: Html, status = 200): Response {
173181 </p>`,
174182 { tone: "attn" },
175183 );
176 return shell(
184 const sole = timePhase(ctx.request, "owned", () =>
185 people.map((a) => {
186 const u = getUserByName(ctx.app.db, a.username);
187 return u === undefined
188 ? []
189 : soleOwnedOrgs(ctx.app.db, u.id).map((o) => o.username);
190 }),
191 );
192 const rendering = performance.now();
193 const response = shell(
177194 ctx,
178195 html`${notice ?? null}${ownerless}${noMail}${card(
179196 people.length === 0
180197 ? empty({ icon: "users", title: "Nobody has joined yet." })
181198 : html`<ul class="rows">
182 ${people.map((a) => {
183 const user = getUserByName(ctx.app.db, a.username);
184 return row(
185 a,
186 user === undefined
187 ? []
188 : soleOwnedOrgs(ctx.app.db, user.id).map((o) => o.username),
189 );
190 })}
199 ${people.map((a, i) => row(a, sole[i] ?? []))}
191200 </ul>`,
192201 {
193202 head: html`<h2>Who is on this forge</h2>
@@ -200,6 +209,8 @@ function accountsView(ctx: Ctx, notice?: Html, status = 200): Response {
200209 )}`,
201210 status,
202211 );
212 notePhase(ctx.request, "render", performance.now() - rendering);
213 return response;
203214 }
204215
205216 /** Where an organization's owners are added. */
test/accounts-timing.test.ts added +45
@@ -0,0 +1,45 @@
1 /** The accounts page names its phases in Server-Timing (#3426). */
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 { serve, type Servers } from "../src/http/server.ts";
8 import { seedAdmin } from "./helpers/admin.ts";
9
10 let dataDir = "";
11 let app: App, servers: Servers, bearer = "";
12
13 beforeAll(async () => {
14 dataDir = await mkdtemp(join(tmpdir(), "forge-acctime-"));
15 app = createApp({ dataDir, port: 0 });
16 servers = await serve(app);
17 const admin = await seedAdmin(app, { username: "alice", email: "[email protected]", password: "correct-horse-battery" });
18 if (!admin.ok) throw new Error(admin.error);
19 const minted = await fetch(`${servers.url}/api/v1/tokens`, {
20 method: "POST",
21 headers: { "content-type": "application/json" },
22 body: JSON.stringify({ adminConfirm: true, username: "alice", password: "correct-horse-battery", name: "cli", scope: "admin" }),
23 });
24 bearer = ((await minted.json()) as { token: string }).token;
25 });
26
27 afterAll(async () => {
28 servers.stop();
29 app.close();
30 await rm(dataDir, { recursive: true, force: true });
31 });
32
33 test("the accounts page names session, each read and render once each", async () => {
34 const res = await fetch(`${servers.url}/settings/accounts`, { headers: { authorization: `Bearer ${bearer}` } });
35 const page = await res.text();
36 expect(res.status).toBe(200);
37 expect(page).toContain("Who is on this forge");
38 const parts = (res.headers.get("server-timing") ?? "").split(", ");
39 const names = parts.map((p) => p.split(";")[0]!);
40 for (const n of ["session", "accounts", "mail", "orphans", "owned", "render"]) {
41 expect(names.filter((x) => x === n).length).toBe(1);
42 const part = parts.find((p) => p.startsWith(`${n};`))!;
43 expect(Number(/dur=([\d.]+)/.exec(part)?.[1])).not.toBeNaN();
44 }
45 });

↑↓ navigate ↵ open ⌘↵ its app esc close

Keyboard shortcuts

Docs