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

Browsing an origin names its Server-Timing phases: session, origin, render #5264

Merged opened this

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

Merged

scout/forge:scout/5247-browsing-an-origin-names-its-server-timi → main 1 commit fork 1 thread · 1 unresolved

  • Merged. scout/5247-browsing-an-origin-names-its-server-timi was merged into main as 7d2245c.

Review

Sign in to review this change.
2 files changed +109 −17
src/http/api.ts +23 −17
@@ -4788,6 +4788,7 @@ export function apiRoutes(router: Router<Handler>): void {
47884788 // request spends their credential on another forge.
47894789
47904790 const browse = (ctx: Ctx): RepoSource | Response => {
4791 notePhase(ctx.request, "session", takeSessionTime(ctx.request));
47914792 const denied = requireAccount(ctx, { adminToken: true });
47924793 if (denied) return denied;
47934794 const source = sourceFor(ctx.app, ctx.auth.user, {
@@ -4805,8 +4806,10 @@ export function apiRoutes(router: Router<Handler>): void {
48054806 ): Promise<Response> => {
48064807 const source = browse(ctx);
48074808 if (source instanceof Response) return source;
4808 const result = await run(source);
4809 return result.ok ? shape(result.value) : sourceFailure(result.error);
4809 const result = await timePhaseAsync(ctx.request, "origin", () => run(source));
4810 return result.ok
4811 ? timePhase(ctx.request, "render", () => shape(result.value))
4812 : sourceFailure(result.error);
48104813 };
48114814
48124815 const BROWSE = "/api/v1/user/origins/:id/browse/:owner/:name";
@@ -4815,19 +4818,20 @@ export function apiRoutes(router: Router<Handler>): void {
48154818 router.add("GET", BROWSE, async (ctx) => {
48164819 const source = browse(ctx);
48174820 if (source instanceof Response) return source;
4818 const facts = await source.describe();
4821 const facts = await timePhaseAsync(ctx.request, "origin", () => source.describe());

Not blocking: this route and commits note origin twice (here for describe, then for the branches/readme pair), so the header can show two origin entries. Server-Timing allows that, but one summed entry would be easier to read. A later pull could time the whole origin stretch once.

48194822 if (!facts.ok) return sourceFailure(facts.error);
4820 const [branches, readme] = await Promise.all([
4821 source.branches(),
4822 source.readme(facts.value.defaultBranch),
4823 ]);
4824 return json({
4825 ...whereJson(source),
4826 capabilities: source.capabilities,
4827 facts: facts.value,
4828 branches: branches.ok ? branches.value : [],
4829 readme: readme.ok ? readme.value : null,
4830 });
4823 const [branches, readme] = await timePhaseAsync(ctx.request, "origin", () =>
4824 Promise.all([source.branches(), source.readme(facts.value.defaultBranch)]),
4825 );
4826 return timePhase(ctx.request, "render", () =>
4827 json({
4828 ...whereJson(source),
4829 capabilities: source.capabilities,
4830 facts: facts.value,
4831 branches: branches.ok ? branches.value : [],
4832 readme: readme.ok ? readme.value : null,
4833 }),
4834 );
48314835 });
48324836
48334837 router.add("GET", `${BROWSE}/branches`, (ctx) =>
@@ -4887,14 +4891,16 @@ export function apiRoutes(router: Router<Handler>): void {
48874891 // a live source that question is a request to another forge.
48884892 let ref = ctx.url.searchParams.get("ref") ?? "";
48894893 if (ref === "") {
4890 const facts = await source.describe();
4894 const facts = await timePhaseAsync(ctx.request, "origin", () => source.describe());
48914895 if (!facts.ok) return sourceFailure(facts.error);
48924896 ref = facts.value.defaultBranch || "HEAD";
48934897 }
48944898 const cursor = ctx.url.searchParams.get("cursor") ?? undefined;
4895 const page = await source.commits(ref, cursor);
4899 const page = await timePhaseAsync(ctx.request, "origin", () => source.commits(ref, cursor));
48964900 if (!page.ok) return sourceFailure(page.error);
4897 return json({ items: page.value.items, next_cursor: page.value.next });
4901 return timePhase(ctx.request, "render", () =>
4902 json({ items: page.value.items, next_cursor: page.value.next }),
4903 );
48984904 });
48994905
49004906 router.add("GET", `${BROWSE}/commits/:oid`, (ctx) => {
test/browse-phases.test.ts added +86
@@ -0,0 +1,86 @@
1 import { afterAll, beforeAll, describe, expect, test } from "bun:test";
2 import { mkdtemp, rm } from "node:fs/promises";
3 import { tmpdir } from "node:os";
4 import { join } from "node:path";
5 import { createApp, type App } from "../src/app.ts";
6 import { serve, type Servers } from "../src/http/server.ts";
7 import { forgetProviders } from "../src/origins/providers/index.ts";
8 import { clearViewCache } from "../src/view/cache.ts";
9 import { newFakeState, serveFakeForge, type FakeForge } from "./fake-origin.ts";
10 import { fastFirstFetch } from "./helpers/globals.ts";
11 import { seedAdmin } from "./helpers/admin.ts";
12
13 /** Browsing an origin names where the time went: session, origin, render. */
14
15 const TOKEN = "octo-origin-secret-4b81";
16 const PASSWORD = "alice-login-secret";
17 const auth = `Basic ${Buffer.from(`alice:${PASSWORD}`).toString("base64")}`;
18
19 fastFirstFetch();
20
21 describe("browsing an origin: Server-Timing", () => {
22 let dataDir: string;
23 let app: App;
24 let servers: Servers;
25 let fake: FakeForge;
26 let root = "";
27
28 beforeAll(async () => {
29 dataDir = await mkdtemp(join(tmpdir(), "forge-browse-timing-"));
30 const state = newFakeState("forgejo", "octo", TOKEN);
31 state.repos.push({
32 id: 41,
33 fullName: "octo/widgets",
34 cloneUrl: "http://127.0.0.1:1/octo/widgets.git",
35 defaultBranch: "main",
36 });
37 state.files.set("octo/widgets", new Map([["README.md", "# widgets\n"]]));
38 state.branches.set("octo/widgets", [{ name: "main", sha: "a".repeat(40), protected: false }]);
39 state.issues.set("octo/widgets", [
40 {
41 id: 900,
42 number: 7,
43 title: "The gear slips",
44 user: "mona",
45 createdAt: "2026-01-01T00:00:00.000Z",
46 updatedAt: "2026-01-02T00:00:00.000Z",
47 },
48 ]);
49 fake = serveFakeForge(state);
50 app = createApp({ dataDir, port: 0 });
51 servers = await serve(app);
52 await seedAdmin(app, { username: "alice", email: "[email protected]", password: PASSWORD });
53 const connected = await fetch(`${servers.url}/api/v1/user/origins`, {
54 method: "POST",
55 headers: { "content-type": "application/json", authorization: auth },
56 body: JSON.stringify({ host: fake.url, token: TOKEN }),
57 });
58 expect(connected.status).toBe(201);
59 const id = ((await connected.json()) as { id: string }).id;
60 root = `${servers.url}/api/v1/user/origins/${id}/browse/octo/widgets`;
61 clearViewCache();
62 });
63
64 afterAll(async () => {
65 fake.stop();
66 servers.stop();
67 app.close();
68 forgetProviders(dataDir);
69 await rm(dataDir, { recursive: true, force: true });
70 });
71
72 const phases = (response: Response): string[] =>
73 (response.headers.get("server-timing") ?? "")
74 .split(",")
75 .map((part) => part.trim().split(/[;=]/)[0]!)
76 .filter(Boolean);
77
78 for (const path of ["", "/branches", "/blob/main/README.md", "/issues"]) {
79 test(`GET …/browse/octo/widgets${path} names session, origin and render`, async () => {
80 const response = await fetch(`${root}${path}`, { headers: { authorization: auth } });
81 expect(response.status).toBe(200);
82 const names = phases(response);
83 for (const name of ["session", "origin", "render"]) expect(names).toContain(name);
84 });
85 }
86 });

↑↓ navigate ↵ open ⌘↵ its app esc close

Keyboard shortcuts

Docs