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

An app's settings page's Server-Timing names its phases: session, vitals, domains, render #3243

Merged opened this

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

Merged

scout/forge:scout/3240-an-app-s-settings-page-s-server-timing-n → main 1 commit fork 1 thread · 1 unresolved

  • Merged. scout/3240-an-app-s-settings-page-s-server-timing-n was merged into main as 44ace1a.

Review

Sign in to review this change.
2 files changed +66 −9
src/web/pages/settings.ts +14 −9
@@ -51,6 +51,7 @@ import {
5151 updateRepoSettings,
5252 } from "../../actions/repos.ts";
5353 import { appsRuntime } from "../../apps/runtime.ts";
54 import { notePhase, takeSessionTime, timePhase } from "../../core/route_timing.ts";
5455 import { appVitals, shiftText, speedText as ms, type AppVitals } from "../../apps/vitals.ts";
5556 import type { PreviewData } from "../../apps/data.ts";
5657 import {
@@ -581,10 +582,15 @@ function appSettingsView(
581582 },
582583 );
583584 if (record === undefined) return form;
584 const domains = listAppDomains(ctx.app, ctx.auth.user, repo);
585 const domains = timePhase(ctx.request, "domains", () =>
586 listAppDomains(ctx.app, ctx.auth.user, repo),
587 );
588 const vitals = timePhase(ctx.request, "vitals", () =>
589 appVitals(ctx.app.db, record.id),
590 );
585591 return html`${form}
586592 ${when(listed?.ok === true, () => html`<div class="mt">${appMembersView(base, values.access, named)}</div>`)}
587 <div class="mt">${appVisitorsView(appVitals(ctx.app.db, record.id))}</div>
593 <div class="mt">${appVisitorsView(vitals)}</div>
588594 <div class="mt">
589595 ${appEnvView(base, listAppEnvNames(ctx.app.db, record.id))}
590596 </div>
@@ -880,14 +886,13 @@ const appValues = (repo: Repo, record: AppRecord | undefined): AppValues => ({
880886 export function appSettingsPage(ctx: Ctx): Response {
881887 const repo = loadAdminRepo(ctx);
882888 if (!repo) return repoNotFound(ctx);
889 notePhase(ctx.request, "session", takeSessionTime(ctx.request));
883890 const record = getAppByRepo(ctx.app.db, repo.id);
884 return settingsPageResponse(
885 ctx,
886 repo,
887 "app",
888 "App",
889 appSettingsView(ctx, repo, record, appValues(repo, record)),
890 );
891 const view = appSettingsView(ctx, repo, record, appValues(repo, record));
892 const rendering = performance.now();
893 const response = settingsPageResponse(ctx, repo, "app", "App", view);
894 notePhase(ctx.request, "render", performance.now() - rendering);

render only times settingsPageResponse. The view is built before it, and the DB reads are timed on their own. If the view builder does other eager work, that time is not named. Fine for now, but it is worth a one-line comment.

895 return response;
891896 }
892897
893898 // POST /:owner/:name/settings/app — enable, or change.
test/app-settings-timing.test.ts added +52
@@ -0,0 +1,52 @@
1 /** An app's settings page names its phases in Server-Timing (#3240). */
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-atime-"));
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 settings page names session, vitals, domains and render once each", async () => {
40 const res = await fetch(`${servers.url}/alice/notes/settings/app`, { headers: { authorization: `Bearer ${bearer}` } });
41 const page = await res.text();
42 expect(res.status).toBe(200);
43 expect(page).toContain("Domains");
44 expect(res.headers.get("x-content-type-options")).toBe("nosniff");
45 const parts = (res.headers.get("server-timing") ?? "").split(", ");
46 const names = parts.map((p) => p.split(";")[0]!);
47 for (const n of ["session", "vitals", "domains", "render"]) {
48 expect(names.filter((x) => x === n).length).toBe(1);
49 const part = parts.find((p) => p.startsWith(`${n};`))!;
50 expect(Number(/dur=([\d.]+)/.exec(part)?.[1])).not.toBeNaN();
51 }
52 });

↑↓ navigate ↵ open ⌘↵ its app esc close

Keyboard shortcuts

Docs