Public: anyone can read this; the crew can change it.
A person's page names its Server-Timing phases: session, each read, render #5756
Check · Review · Land · Live done — Landed and live in production.
scout/forge:scout/5673-a-person-s-page-names-its-server-timing → main 1 commit fork 2 threads · 2 unresolved
2 files
changed +74
−11
src/web/pages/org.ts +24 −11
| @@ -33,7 +33,7 @@ import { LIST_LIMIT_MAX } from "../../core/issues.ts"; | ||
| 33 | 33 | import { listReposByOwner, type Repo } from "../../core/repos.ts"; |
| 34 | 34 | import { getUserByName, hiddenFrom, type User } from "../../core/users.ts"; |
| 35 | 35 | 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"; | |
| 37 | 37 | import { notFoundPage, pageHead, signInFirst } from "../chrome.ts"; |
| 38 | 38 | import { field, readForm, type Form } from "../form.ts"; |
| 39 | 39 | import { html, when, type Html } from "../html.ts"; |
| @@ -301,17 +301,26 @@ function teamsSection(ctx: Ctx, org: User, canManage: boolean): Html { | ||
| 301 | 301 | // drawn by the same shelf the home draws its *Your apps* card from, then |
| 302 | 302 | // their other repositories, then — for an org — its members and teams. |
| 303 | 303 | 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 | ); | |
| 305 | 308 | if (!owner || hiddenFrom(owner, ctx.auth.user)) return notFoundPage(ctx); |
| 306 | 309 | |
| 307 | 310 | 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 | ); | |
| 311 | 314 | const canManage = isOrg && accessAllows(access, "admin"); |
| 312 | 315 | const isSelf = |
| 313 | 316 | ctx.auth.user !== undefined && (ctx.auth.user.id === owner.id || canManage); |
| 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(); | |
| 315 | 324 | // Said to the people it concerns: its members and the forge's owner. |
| 316 | 325 | const ownerless = |
| 317 | 326 | isOrg && access !== "none" && personOwners(ctx.app.db, owner.id).length === 0; |
| @@ -346,7 +355,7 @@ export async function ownerProfile(ctx: Ctx): Promise<Response> { | ||
| 346 | 355 | ) |
| 347 | 356 | : null} |
| 348 | 357 | ${apps.html} |
| 349 | ${repoSection(ctx, owner, isSelf, apps.count > 0)} | |
| 358 | ${repos} | |
| 350 | 359 | ${when(isOrg && accessAllows(access, "read"), () => |
| 351 | 360 | teamsSection(ctx, owner, canManage), |
| 352 | 361 | )} |
| @@ -372,10 +381,12 @@ export async function ownerProfile(ctx: Ctx): Promise<Response> { | ||
| 372 | 381 | )} |
| 373 | 382 | ${isOrg ? orgSecrets(ctx, owner.username) : null}`; |
| 374 | 383 | |
| 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({ | |
| 379 | 390 | ctx, |
| 380 | 391 | title: owner.username, |
| 381 | 392 | description: ownerLine({ |
| @@ -386,6 +397,8 @@ export async function ownerProfile(ctx: Ctx): Promise<Response> { | ||
| 386 | 397 | }), |
| 387 | 398 | body, |
| 388 | 399 | }); |
| 400 | notePhase(ctx.request, "render", performance.now() - rendering); | |
| 401 | return res; | |
| 389 | 402 | } |
| 390 | 403 | |
| 391 | 404 | 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 | } |
new above
offline
Second
ownerphase for the same request duplicates the name in Server-Timing; useaccessor merge with the lookup above.