feat(devops): separate public page server timing diagnostics
CI / check (push) Successful in 2m23s
CI / deploy (push) Successful in 1m13s
CI / publish-container (push) Successful in 44s

This commit is contained in:
Simo committed 2026-09-11 10:49:08 +02:00
1 parent d5091d1a73
commit baeb54aeb5
15 files changed
+315 -29

No files matched your search

+19
View File
@@ -85,3 +85,22 @@ it("does not change a completed snapshot when background work finishes", async (
await background;
expect(result.metrics.external.count).toBe(0);
});
it("reports timings on failure without replacing the application error", async () => {
const error = new Error("original");
let captured = 0;
await expect(
collectPerformance(
async () => {
await measureDependency("database", async () => {
throw error;
});
},
(metrics) => {
captured = metrics.database.errors;
throw Error("telemetry failure");
},
),
).rejects.toBe(error);
expect(captured).toBe(1);
});
+16 -5
View File
@@ -7,17 +7,28 @@ export type PerformanceMetrics = Record<Dependency, Measurement> & {
};
const context = new AsyncLocalStorage<PerformanceMetrics>();
export async function collectPerformance<T>(work: () => Promise<T>) {
export async function collectPerformance<T>(
work: () => Promise<T>,
onComplete?: (metrics: PerformanceMetrics) => void,
) {
const metrics: PerformanceMetrics = {
durationMs: 0,
database: { count: 0, durationMs: 0, errors: 0 },
external: { count: 0, durationMs: 0, errors: 0 },
};
const start = performance.now();
const value = await context.run(metrics, work);
metrics.durationMs = performance.now() - start;
// Background work may keep the async context: return a detached snapshot.
return { value, metrics: structuredClone(metrics) };
try {
const value = await context.run(metrics, work);
metrics.durationMs = performance.now() - start;
return { value, metrics: structuredClone(metrics) };
} finally {
metrics.durationMs = performance.now() - start;
try {
onComplete?.(structuredClone(metrics));
} catch {
/* Diagnostics must not change application outcomes. */
}
}
}
export async function measureDependency<T>(
+17
View File
@@ -68,3 +68,20 @@ it("ignores expired and malformed shared records", async () => {
expect(result.shared).toBe(true);
expect(result.samples).toEqual([sample]);
});
it("keeps public traffic separate from retained HK operations", async () => {
mock.status = "end";
recordPerformance({ ...sample, operationId: "hk-retained" });
for (let n = 0; n < 230; n++)
recordPerformance({
...sample,
kind: "public-page",
outcome: "completed",
operationId: `public-${n}`,
});
const admin = await readPerformance();
const publicPages = await readPerformance("public-page");
expect(admin.samples[0].operationId).toBe("hk-retained");
expect(publicPages.samples).toHaveLength(200);
expect(publicPages.samples[0].operationId).toBe("public-229");
});
+28 -9
View File
@@ -11,6 +11,8 @@ const measurement = z.object({
errors: z.number().nonnegative(),
});
const schema = z.object({
kind: z.enum(["admin-api", "public-page"]).optional(),
outcome: z.enum(["completed", "failed", "redirect", "not-found"]).optional(),
operationId: z.string().max(100),
route: z.string().max(300),
method: z.string().max(10),
@@ -24,6 +26,8 @@ const schema = z.object({
}),
});
export interface PerformanceSample {
kind?: "admin-api" | "public-page";
outcome?: "completed" | "failed" | "redirect" | "not-found";
operationId: string;
route: string;
method: string;
@@ -33,23 +37,33 @@ export interface PerformanceSample {
metrics: PerformanceMetrics;
}
const state = globalThis as typeof globalThis & {
cmsPerformance?: { samples: PerformanceSample[]; pending: number };
cmsPerformance?: {
samples: PerformanceSample[];
publicSamples?: PerformanceSample[];
pending: number;
};
};
const buffer = state.cmsPerformance ?? { samples: [], pending: 0 };
state.cmsPerformance = buffer;
/** Best effort: no connection wait, no unbounded offline queue, no request failure. */
export function recordPerformance(sample: PerformanceSample): void {
buffer.samples.unshift(sample);
buffer.samples.length = Math.min(buffer.samples.length, LIMIT);
const samples =
sample.kind === "public-page"
? (buffer.publicSamples ?? [])
: buffer.samples;
if (sample.kind === "public-page") buffer.publicSamples = samples;
samples.unshift(sample);
samples.length = Math.min(samples.length, LIMIT);
const key = sample.kind === "public-page" ? `${KEY}:public` : KEY;
if (redis?.status !== "ready" || buffer.pending >= 4) return;
buffer.pending++;
try {
void redis
.multi()
.lpush(KEY, JSON.stringify(sample))
.ltrim(KEY, 0, LIMIT - 1)
.expire(KEY, 3600)
.lpush(key, JSON.stringify(sample))
.ltrim(key, 0, LIMIT - 1)
.expire(key, 3600)
.exec()
.catch(() => undefined)
.finally(() => {
@@ -60,16 +74,21 @@ export function recordPerformance(sample: PerformanceSample): void {
}
}
export async function readPerformance(): Promise<{
export async function readPerformance(
kind: "admin-api" | "public-page" = "admin-api",
): Promise<{
samples: PerformanceSample[];
shared: boolean;
}> {
const cutoff = Date.now() - RETENTION_MS;
const key = kind === "public-page" ? `${KEY}:public` : KEY;
const local =
kind === "public-page" ? (buffer.publicSamples ?? []) : buffer.samples;
if (redis?.status === "ready") {
let timer: ReturnType<typeof setTimeout> | undefined;
try {
const rows = await Promise.race([
redis.lrange(KEY, 0, LIMIT - 1),
redis.lrange(key, 0, LIMIT - 1),
new Promise<never>((_, reject) => {
timer = setTimeout(
() => reject(new Error("Diagnostic storage timeout")),
@@ -95,7 +114,7 @@ export async function readPerformance(): Promise<{
}
}
return {
samples: buffer.samples.filter((sample) => sample.at >= cutoff),
samples: local.filter((sample) => sample.at >= cutoff),
shared: false,
};
}
+76
View File
@@ -0,0 +1,76 @@
import { beforeEach, expect, it, vi } from "vitest";
import { measureDependency } from "./performance-diagnostics";
const mocks = vi.hoisted(() => ({ record: vi.fn() }));
vi.mock("./performance-store", () => ({ recordPerformance: mocks.record }));
import { withPublicPagePerformance } from "./public-page-performance";
beforeEach(() => {
mocks.record.mockClear();
});
it("measures concurrent pages with static labels and never stores props", async () => {
const render = withPublicPagePerformance(
"/u/[username]",
async (props: { username: string }) => {
await measureDependency("database", async () => 1);
return props.username;
},
);
expect(
await Promise.all([
render({ username: "PrivateName" }),
render({ username: "AnotherName" }),
]),
).toEqual(["PrivateName", "AnotherName"]);
const records = mocks.record.mock.calls.map(([record]) => record);
expect(records).toHaveLength(2);
expect(records[0].metrics.database.count).toBe(1);
expect(records[0].operationId).not.toBe(records[1].operationId);
expect(JSON.stringify(records)).not.toMatch(/PrivateName|AnotherName/);
expect(records[0]).toMatchObject({
kind: "public-page",
route: "/u/[username]",
outcome: "completed",
});
});
it("preserves framework redirects and labels them separately from failures", async () => {
const error = Object.assign(new Error("private redirect"), {
digest: "NEXT_REDIRECT;replace;/login;307;",
});
const render = withPublicPagePerformance("/me", async () => {
throw error;
});
await expect(render(undefined)).rejects.toBe(error);
expect(mocks.record).toHaveBeenCalledWith(
expect.objectContaining({ outcome: "redirect" }),
);
expect(JSON.stringify(mocks.record.mock.calls)).not.toContain("/login");
});
it("keeps missing pages distinct and records dependency failures", async () => {
const error = Object.assign(new Error("private"), {
digest: "NEXT_HTTP_ERROR_FALLBACK;404",
});
await expect(
withPublicPagePerformance("/news/[slug]", async () => {
throw error;
})(undefined),
).rejects.toBe(error);
expect(mocks.record).toHaveBeenCalledWith(
expect.objectContaining({ outcome: "not-found" }),
);
});
it("excludes static build invocations without changing their result", async () => {
vi.stubEnv("NEXT_PHASE", "phase-production-build");
try {
expect(
await withPublicPagePerformance(
"/news",
async () => "content",
)(undefined),
).toBe("content");
expect(mocks.record).not.toHaveBeenCalled();
} finally {
vi.unstubAllEnvs();
}
});
+61
View File
@@ -0,0 +1,61 @@
import { createStore, runWithStore } from "./foundation/request-context";
import type { IpAddress } from "./foundation/types";
import { collectPerformance } from "./performance-diagnostics";
import { type PerformanceSample, recordPerformance } from "./performance-store";
type PublicRoute =
| "/me"
| "/news"
| "/news/[slug]"
| "/u/[username]"
| "/events"
| "/search";
/** Measures this server component invocation, not child rendering, streaming or browser load. */
export function withPublicPagePerformance<Props, Result>(
route: PublicRoute,
render: (props: Props) => Promise<Result>,
) {
return async (props: Props): Promise<Result> => {
if (
process.env.NEXT_PHASE === "phase-production-build" ||
process.env.NEXT_PHASE === "phase-production-compile"
)
return render(props);
const store = createStore("unknown" as IpAddress);
let outcome: PerformanceSample["outcome"] = "completed";
return runWithStore(store, async () => {
const result = await collectPerformance(
async () => {
try {
return await render(props);
} catch (error) {
const digest =
error && typeof error === "object" && "digest" in error
? String(error.digest)
: "";
outcome = digest.startsWith("NEXT_REDIRECT;")
? "redirect"
: digest === "NEXT_HTTP_ERROR_FALLBACK;404"
? "not-found"
: "failed";
throw error;
}
},
(metrics) =>
recordPerformance({
kind: "public-page",
outcome,
operationId: store.requestId,
route,
method: "PAGE",
status:
outcome === "failed" ? 500 : outcome === "not-found" ? 404 : 200,
at: Date.now(),
streaming: false,
metrics,
}),
);
return result.value;
});
};
}