Public: anyone can read this; the crew can change it.
Browsing an origin names its Server-Timing phases: session, origin, render #5264
Check · Review · Land · Live done — Landed and live in production.
scout/forge:scout/5247-browsing-an-origin-names-its-server-timi → main 1 commit fork 1 thread · 1 unresolved
2 files
changed +109
−17
src/http/api.ts +23 −17
| @@ -4788,6 +4788,7 @@ export function apiRoutes(router: Router<Handler>): void { | ||
| 4788 | 4788 | // request spends their credential on another forge. |
| 4789 | 4789 | |
| 4790 | 4790 | const browse = (ctx: Ctx): RepoSource | Response => { |
| 4791 | notePhase(ctx.request, "session", takeSessionTime(ctx.request)); | |
| 4791 | 4792 | const denied = requireAccount(ctx, { adminToken: true }); |
| 4792 | 4793 | if (denied) return denied; |
| 4793 | 4794 | const source = sourceFor(ctx.app, ctx.auth.user, { |
| @@ -4805,8 +4806,10 @@ export function apiRoutes(router: Router<Handler>): void { | ||
| 4805 | 4806 | ): Promise<Response> => { |
| 4806 | 4807 | const source = browse(ctx); |
| 4807 | 4808 | 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); | |
| 4810 | 4813 | }; |
| 4811 | 4814 | |
| 4812 | 4815 | const BROWSE = "/api/v1/user/origins/:id/browse/:owner/:name"; |
| @@ -4815,19 +4818,20 @@ export function apiRoutes(router: Router<Handler>): void { | ||
| 4815 | 4818 | router.add("GET", BROWSE, async (ctx) => { |
| 4816 | 4819 | const source = browse(ctx); |
| 4817 | 4820 | if (source instanceof Response) return source; |
| 4818 | const facts = await source.describe(); | |
| 4821 | const facts = await timePhaseAsync(ctx.request, "origin", () => source.describe()); | |
| 4819 | 4822 | 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 | ); | |
| 4831 | 4835 | }); |
| 4832 | 4836 | |
| 4833 | 4837 | router.add("GET", `${BROWSE}/branches`, (ctx) => |
| @@ -4887,14 +4891,16 @@ export function apiRoutes(router: Router<Handler>): void { | ||
| 4887 | 4891 | // a live source that question is a request to another forge. |
| 4888 | 4892 | let ref = ctx.url.searchParams.get("ref") ?? ""; |
| 4889 | 4893 | if (ref === "") { |
| 4890 | const facts = await source.describe(); | |
| 4894 | const facts = await timePhaseAsync(ctx.request, "origin", () => source.describe()); | |
| 4891 | 4895 | if (!facts.ok) return sourceFailure(facts.error); |
| 4892 | 4896 | ref = facts.value.defaultBranch || "HEAD"; |
| 4893 | 4897 | } |
| 4894 | 4898 | 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)); | |
| 4896 | 4900 | 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 | ); | |
| 4898 | 4904 | }); |
| 4899 | 4905 | |
| 4900 | 4906 | 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 | }); |
new above
offline
Not blocking: this route and
commitsnoteorigintwice (here fordescribe, then for the branches/readme pair), so the header can show twooriginentries. Server-Timing allows that, but one summed entry would be easier to read. A later pull could time the whole origin stretch once.