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

The API's board reads name their Server-Timing phases: session, boards, render #3949

Merged opened this

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

Merged

scout/forge:scout/3936-the-api-s-board-reads-name-their-server → main 3 commits fork

  • Merged. scout/3936-the-api-s-board-reads-name-their-server was merged into main as 61aae08.

Review

Sign in to review this change.
2 files changed +83 −8
src/http/api.ts +14 −8
@@ -1505,21 +1505,25 @@ export function apiRoutes(router: Router<Handler>): void {
15051505 };
15061506
15071507 router.add("GET", "/api/v1/boards", async (ctx) => {
1508 notePhase(ctx.request, "session", takeSessionTime(ctx.request));
15081509 const denied = boardsAdmin(ctx);
15091510 if (denied) return denied;
1510 const listed = await listAllBoards(ctx.app, ctx.auth.user);
1511 return listed.ok
1512 ? json({ boards: listed.value.map((b) => ({ slug: b.slug, ...b.board })) })
1513 : notFound();
1511 const listed = await timePhaseAsync(ctx.request, "boards", () => listAllBoards(ctx.app, ctx.auth.user));
1512 return timePhase(ctx.request, "render", () =>
1513 listed.ok
1514 ? json({ boards: listed.value.map((b) => ({ slug: b.slug, ...b.board })) })
1515 : notFound());
15141516 });
15151517
15161518 // The signed-in admin's pins for home: GET lists them in order, POST
15171519 // {op: pin|unpin|move, slug, title, by?} changes them. Above `:slug`.
15181520 router.add("GET", "/api/v1/boards/pins", async (ctx) => {
1521 notePhase(ctx.request, "session", takeSessionTime(ctx.request));
15191522 const denied = boardsAdmin(ctx);
15201523 if (denied) return denied;
1521 const listed = await listPinnedPanels(ctx.app, ctx.auth.user);
1522 return listed.ok ? json({ pins: listed.value.map((p) => ({ slug: p.slug, title: p.title, panel: p.panel })) }) : notFound();
1524 const listed = await timePhaseAsync(ctx.request, "boards", () => listPinnedPanels(ctx.app, ctx.auth.user));
1525 return timePhase(ctx.request, "render", () =>
1526 listed.ok ? json({ pins: listed.value.map((p) => ({ slug: p.slug, title: p.title, panel: p.panel })) }) : notFound());
15231527 });
15241528
15251529 router.add("POST", "/api/v1/boards/pins", async (ctx) => {
@@ -1548,10 +1552,12 @@ export function apiRoutes(router: Router<Handler>): void {
15481552 });
15491553
15501554 router.add("GET", "/api/v1/boards/:slug", async (ctx) => {
1555 notePhase(ctx.request, "session", takeSessionTime(ctx.request));
15511556 const denied = boardsAdmin(ctx);
15521557 if (denied) return denied;
1553 const shown = await showBoard(ctx.app, ctx.auth.user, ctx.params.slug!);
1554 return shown.ok ? json({ slug: ctx.params.slug, ...shown.value }) : notFound();
1558 const shown = await timePhaseAsync(ctx.request, "boards", () => showBoard(ctx.app, ctx.auth.user, ctx.params.slug!));
1559 return timePhase(ctx.request, "render", () =>
1560 shown.ok ? json({ slug: ctx.params.slug, ...shown.value }) : notFound());
15551561 });
15561562
15571563 // One panel's data (`boards/panel_data.ts`): `:id` is its place on the
test/api-boards-timing.test.ts added +69
@@ -0,0 +1,69 @@
1 /** The API's board reads name their Server-Timing phases (#3936). */
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 { writeBoard } from "../src/actions/boards.ts";
8 import { createSession } from "../src/core/sessions.ts";
9 import { createUser } from "../src/core/users.ts";
10 import { serve, type Servers } from "../src/http/server.ts";
11 import { quietLogs } from "./helpers/globals.ts";
12
13 quietLogs();
14
15 let dir: string;
16 let app: App;
17 let servers: Servers;
18 let cookie = "";
19
20 beforeAll(async () => {
21 dir = await mkdtemp(join(tmpdir(), "forge-api-boards-timing-"));
22 app = createApp({ dataDir: dir, port: 0 });
23 servers = await serve(app);
24 const made = await createUser(app.db, {
25 origin: "seed", trusted: true, username: "alice", email: "[email protected]",
26 password: "a-good-passphrase", adminIfFirst: true,
27 });
28 if (!made.ok) throw new Error(String(made.error));
29 cookie = `forge_session=${createSession(app.db, made.value.id).token}`;
30 const w = await writeBoard(app, made.value, "ops", {
31 title: "Ops", range: "all", refresh: "off",
32 panels: [{ title: "Runs", chart: "line", query: { source: "runs", aggregate: "count", groupBy: "kind" }, w: 1, h: 1 }],
33 });
34 if (!w.ok) throw new Error(JSON.stringify(w.error));
35 });
36
37 afterAll(async () => {
38 await servers.stop();
39 app.close();
40 await rm(dir, { recursive: true, force: true });
41 });
42
43 async function phases(path: string, withCookie = true): Promise<{ status: number; names: string[]; body: string }> {
44 const res = await fetch(`${servers.url}${path}`, { headers: withCookie ? { cookie } : {} });
45 const body = await res.text();
46 const names = (res.headers.get("server-timing") ?? "").split(", ").map((p) => p.split(";")[0]!);
47 return { status: res.status, names: names.slice(names.indexOf("app") + 1), body };
48 }
49
50 test("each board read names session, boards, render in order", async () => {
51 for (const path of ["/api/v1/boards", "/api/v1/boards/pins", "/api/v1/boards/ops"]) {
52 const got = await phases(path);
53 expect(got.status).toBe(200);
54 expect(got.names).toEqual(["session", "boards", "render"]);
55 }
56 expect(JSON.parse((await phases("/api/v1/boards")).body).boards[0].slug).toBe("ops");
57 });
58
59 test("a missing board still names its phases", async () => {
60 const got = await phases("/api/v1/boards/nope");
61 expect(got.status).toBe(404);
62 expect(got.names).toEqual(["session", "boards", "render"]);
63 });
64
65 test("a refusal keeps its status and names session", async () => {
66 const got = await phases("/api/v1/boards", false);
67 expect(got.status).toBe(401);
68 expect(got.names).toEqual(["session"]);
69 });

↑↓ navigate ↵ open ⌘↵ its app esc close

Keyboard shortcuts

Docs