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

A person's page names its Server-Timing phases: session, each read, render #5756

Merged opened this

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

Merged

scout/forge:scout/5673-a-person-s-page-names-its-server-timing → main 1 commit fork 2 threads · 2 unresolved

  • Merged. scout/5673-a-person-s-page-names-its-server-timing was merged into main as ad4c68e.

Review

Sign in to review this change.
2 files changed +74 −11
src/web/pages/org.ts +24 −11
@@ -33,7 +33,7 @@ import { LIST_LIMIT_MAX } from "../../core/issues.ts";
3333 import { listReposByOwner, type Repo } from "../../core/repos.ts";
3434 import { getUserByName, hiddenFrom, type User } from "../../core/users.ts";
3535 import type { Ctx } from "../../http/context.ts";
36 import { notePhase, takeSessionTime, timePhase } from "../../core/route_timing.ts";
36 import { notePhase, takeSessionTime, timePhase, timePhaseAsync } from "../../core/route_timing.ts";
3737 import { notFoundPage, pageHead, signInFirst } from "../chrome.ts";
3838 import { field, readForm, type Form } from "../form.ts";
3939 import { html, when, type Html } from "../html.ts";
@@ -301,17 +301,26 @@ function teamsSection(ctx: Ctx, org: User, canManage: boolean): Html {
301301 // drawn by the same shelf the home draws its *Your apps* card from, then
302302 // their other repositories, then — for an org — its members and teams.
303303 export async function ownerProfile(ctx: Ctx): Promise<Response> {
304 const owner = getUserByName(ctx.app.db, ctx.params.owner ?? "");
304 notePhase(ctx.request, "session", takeSessionTime(ctx.request));
305 const owner = timePhase(ctx.request, "owner", () =>
306 getUserByName(ctx.app.db, ctx.params.owner ?? ""),
307 );
305308 if (!owner || hiddenFrom(owner, ctx.auth.user)) return notFoundPage(ctx);
306309
307310 const isOrg = owner.kind === "org";
308 const access = isOrg
309 ? orgAccess(ctx.app.db, owner.id, ctx.auth.user)
310 : "none";
311 const access = timePhase(ctx.request, "owner", () =>
312 isOrg ? orgAccess(ctx.app.db, owner.id, ctx.auth.user) : "none",
313 );
311314 const canManage = isOrg && accessAllows(access, "admin");
312315 const isSelf =
313316 ctx.auth.user !== undefined && (ctx.auth.user.id === owner.id || canManage);

Second owner phase for the same request duplicates the name in Server-Timing; use access or merge with the lookup above.

314 const apps = await appsSection(ctx, owner, isSelf);
317 const apps = await timePhaseAsync(ctx.request, "apps", () =>
318 appsSection(ctx, owner, isSelf),
319 );
320 const repos = timePhase(ctx.request, "repos", () =>
321 repoSection(ctx, owner, isSelf, apps.count > 0),
322 );
323 const rendering = performance.now();
315324 // Said to the people it concerns: its members and the forge's owner.
316325 const ownerless =
317326 isOrg && access !== "none" && personOwners(ctx.app.db, owner.id).length === 0;
@@ -346,7 +355,7 @@ export async function ownerProfile(ctx: Ctx): Promise<Response> {
346355 )
347356 : null}
348357 ${apps.html}
349 ${repoSection(ctx, owner, isSelf, apps.count > 0)}
358 ${repos}
350359 ${when(isOrg && accessAllows(access, "read"), () =>
351360 teamsSection(ctx, owner, canManage),
352361 )}
@@ -372,10 +381,12 @@ export async function ownerProfile(ctx: Ctx): Promise<Response> {
372381 )}
373382 ${isOrg ? orgSecrets(ctx, owner.username) : null}`;
374383
375 const listed = listReposByOwner(ctx.app.db, owner.username, undefined, {
376 limit: LIST_LIMIT_MAX,
377 });
378 return page({
384 const listed = timePhase(ctx.request, "listed", () =>
385 listReposByOwner(ctx.app.db, owner.username, undefined, {
386 limit: LIST_LIMIT_MAX,
387 }),
388 );
389 const res = page({
379390 ctx,
380391 title: owner.username,
381392 description: ownerLine({
@@ -386,6 +397,8 @@ export async function ownerProfile(ctx: Ctx): Promise<Response> {
386397 }),
387398 body,
388399 });
400 notePhase(ctx.request, "render", performance.now() - rendering);
401 return res;
389402 }
390403
391404 function orgSecrets(ctx: Ctx, name: string): Html | null {
test/person-page-timing.test.ts added +50
@@ -0,0 +1,50 @@
1 /** A person's page and an organization's page name their Server-Timing phases. */
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-ptime-"));
15 app = createApp({ dataDir, port: 0 });
16 servers = await serve(app);
17 await seedAdmin(app, { username: "alice", email: "[email protected]", password: "correct-horse-battery" });
18 const minted = await fetch(`${servers.url}/api/v1/tokens`, {
19 method: "POST",
20 headers: { "content-type": "application/json" },
21 body: JSON.stringify({ adminConfirm: true, username: "alice", password: "correct-horse-battery", name: "cli", scope: "admin" }),
22 });
23 bearer = ((await minted.json()) as { token: string }).token;
24 const org = await fetch(`${servers.url}/api/v1/orgs`, {
25 method: "POST",
26 headers: { authorization: `Bearer ${bearer}`, "content-type": "application/json" },
27 body: JSON.stringify({ name: "acme" }),
28 });
29 expect(org.status).toBe(201);
30 });
31
32 afterAll(async () => {
33 servers.stop();
34 app.close();
35 await rm(dataDir, { recursive: true, force: true });
36 });
37
38 async function phases(path: string): Promise<string[]> {
39 const res = await fetch(`${servers.url}${path}`);
40 await res.arrayBuffer();
41 expect(res.status).toBe(200);
42 return (res.headers.get("server-timing") ?? "").split(", ").map((p) => p.split(";")[0]!);
43 }
44
45 for (const [what, path] of [["a person's page", "/alice"], ["an organization's page", "/acme"]] as const) {
46 test(`${what} names session, owner, apps, repos, render`, async () => {
47 const got = await phases(path);
48 for (const name of ["session", "owner", "apps", "repos", "render"]) expect(got).toContain(name);

Add "listed" to this list; it is a phase this change introduces and nothing would fail if it vanished.

49 });
50 }

↑↓ navigate ↵ open ⌘↵ its app esc close

Keyboard shortcuts

Docs