Instrument API timing and reduce detail fallbacks

This commit is contained in:
2569718930@qq.com
2026-05-31 19:01:07 +08:00
parent 668f4d9bd3
commit 372d4366a8
18 changed files with 833 additions and 136 deletions
+55 -15
View File
@@ -5,6 +5,10 @@ import {
buildBackendRequestHeaders,
} from "@/lib/backend-auth";
import { buildCachedJsonResponse } from "@/lib/http-cache";
import {
finishProxyTimedResponse,
type ProxyTimer,
} from "@/lib/proxy-timing";
const PASSTHROUGH_UPSTREAM_STATUSES = new Set([
400,
@@ -91,31 +95,59 @@ export async function proxyBackendJsonGet(
signal?: AbortSignal;
statusOnException?: number;
timeoutPublicMessage?: string;
timing?: ProxyTimer;
url: string;
},
) {
let auth: Awaited<ReturnType<typeof buildBackendRequestHeaders>> | null = null;
const timing = options.timing;
try {
auth = await buildBackendRequestHeaders(req, {
includeSupabaseIdentity: options.includeSupabaseIdentity ?? false,
});
const res = await fetch(options.url, {
headers: auth.headers,
...(options.fetchCache
? { cache: options.fetchCache }
: { next: { revalidate: options.revalidateSeconds ?? 30 } }),
signal: options.signal,
});
auth = await (timing
? timing.measure("auth_headers", () =>
buildBackendRequestHeaders(req, {
includeSupabaseIdentity: options.includeSupabaseIdentity ?? false,
}),
)
: buildBackendRequestHeaders(req, {
includeSupabaseIdentity: options.includeSupabaseIdentity ?? false,
}));
const res = await (timing
? timing.measure("backend_fetch", () =>
fetch(options.url, {
headers: auth!.headers,
...(options.fetchCache
? { cache: options.fetchCache }
: { next: { revalidate: options.revalidateSeconds ?? 30 } }),
signal: options.signal,
}),
)
: fetch(options.url, {
headers: auth.headers,
...(options.fetchCache
? { cache: options.fetchCache }
: { next: { revalidate: options.revalidateSeconds ?? 30 } }),
signal: options.signal,
}));
const backendServerTiming = res.headers.get("server-timing") || "";
if (!res.ok) {
const raw = await res.text();
const raw = await (timing
? timing.measure("backend_read", () => res.text())
: res.text());
const response = buildUpstreamErrorResponse(res.status, raw, {
detailLimit: options.detailLimit,
error: options.error,
});
return applyAuthResponseCookies(response, auth.response);
const withCookies = applyAuthResponseCookies(response, auth.response);
return timing
? finishProxyTimedResponse(withCookies, timing, `upstream_${res.status}`, {
backendServerTiming,
})
: withCookies;
}
const data = await res.json();
const data = await (timing
? timing.measure("backend_read", () => res.json())
: res.json());
const response =
options.cacheControl && options.conditionalResponse !== false
? buildCachedJsonResponse(req, data, options.cacheControl)
@@ -124,7 +156,12 @@ export async function proxyBackendJsonGet(
? { "Cache-Control": options.cacheControl }
: undefined,
});
return applyAuthResponseCookies(response, auth.response);
const withCookies = applyAuthResponseCookies(response, auth.response);
return timing
? finishProxyTimedResponse(withCookies, timing, "ok", {
backendServerTiming,
})
: withCookies;
} catch (error) {
const timedOut = options.signal?.aborted === true;
const response = buildProxyExceptionResponse(error, {
@@ -134,6 +171,9 @@ export async function proxyBackendJsonGet(
: options.publicMessage,
status: timedOut ? 504 : options.statusOnException,
});
return auth ? applyAuthResponseCookies(response, auth.response) : response;
const withCookies = auth ? applyAuthResponseCookies(response, auth.response) : response;
return timing
? finishProxyTimedResponse(withCookies, timing, timedOut ? "timeout" : "exception")
: withCookies;
}
}
+1
View File
@@ -716,6 +716,7 @@ export interface AiAnalysisStructured {
}
export interface CityDetail {
city?: string;
name: string;
display_name: string;
detail_depth?: "panel" | "market" | "nearby" | "full";
+113
View File
@@ -0,0 +1,113 @@
import type { NextRequest, NextResponse } from "next/server";
export type ProxyTimingStage = {
durationMs: number;
name: string;
};
export type ProxyTimer = {
hasAuthorization: boolean;
hasSupabaseCookie: boolean;
measure<T>(name: string, action: () => Promise<T>): Promise<T>;
measureSync<T>(name: string, action: () => T): T;
route: string;
stages: ProxyTimingStage[];
totalMs(): number;
};
function proxyNowMs() {
return typeof performance !== "undefined" ? performance.now() : Date.now();
}
function hasRequestSupabaseSessionCookie(req: NextRequest) {
return req.cookies.getAll().some((cookie) => {
const name = cookie.name.toLowerCase();
const value = String(cookie.value || "").trim();
return Boolean(
value &&
(name === "supabase-auth-token" ||
(name.startsWith("sb-") && name.includes("-auth-token"))),
);
});
}
export function createProxyTimer(req: NextRequest, route: string): ProxyTimer {
const startedAt = proxyNowMs();
const stages: ProxyTimingStage[] = [];
const recordStage = (name: string, stageStartedAt: number) => {
stages.push({
durationMs: Math.round((proxyNowMs() - stageStartedAt) * 10) / 10,
name,
});
};
return {
hasAuthorization: Boolean(req.headers.get("authorization")),
hasSupabaseCookie: hasRequestSupabaseSessionCookie(req),
async measure<T>(name: string, action: () => Promise<T>) {
const stageStartedAt = proxyNowMs();
try {
return await action();
} finally {
recordStage(name, stageStartedAt);
}
},
measureSync<T>(name: string, action: () => T) {
const stageStartedAt = proxyNowMs();
try {
return action();
} finally {
recordStage(name, stageStartedAt);
}
},
route,
stages,
totalMs() {
return Math.round((proxyNowMs() - startedAt) * 10) / 10;
},
};
}
function formatServerTiming(stages: ProxyTimingStage[]) {
return stages
.map(({ durationMs, name }) => {
const safeName = name.replace(/[^A-Za-z0-9_-]/g, "_");
return `${safeName};dur=${Math.max(0, durationMs).toFixed(1)}`;
})
.join(", ");
}
export function finishProxyTimedResponse(
response: NextResponse,
timer: ProxyTimer,
outcome: string,
extra?: { backendServerTiming?: string },
) {
const total = timer.totalMs();
const ownServerTiming = formatServerTiming(
[...timer.stages, { durationMs: total, name: "total" }].map((stage) => ({
...stage,
name: `${timer.route}_${stage.name}`,
})),
);
const backendServerTiming = String(extra?.backendServerTiming || "").trim();
response.headers.set(
"Server-Timing",
backendServerTiming
? `${ownServerTiming}, ${backendServerTiming}`
: ownServerTiming,
);
console.info(
"[api-proxy-timing]",
JSON.stringify({
hasAuthorization: timer.hasAuthorization,
hasSupabaseCookie: timer.hasSupabaseCookie,
outcome,
route: timer.route,
stages: timer.stages,
status: response.status,
totalMs: total,
}),
);
return response;
}