Instrument API timing and reduce detail fallbacks

This commit is contained in:
2569718930@qq.com
2026-05-31 18:58:57 +08:00
parent 668f4d9bd3
commit 5083b7c433
18 changed files with 832 additions and 135 deletions
@@ -12,6 +12,7 @@ import {
import {
MAX_HOURLY_DETAIL_CONCURRENT_REQUESTS,
HOURLY_CACHE_TTL_MS,
__resolveCityDetailFromBatchForTest,
__readHourlyCacheEntryForTest,
__resetHourlyDetailRequestQueueForTest,
__runQueuedHourlyDetailRequestForTest,
@@ -130,6 +131,19 @@ export async function runTests() {
__shouldFetchCityDetailForChartForTest({ city: "paris", documentHidden: true, isChartVisible: true }) === false,
"hidden browser tabs should not prefetch city detail",
);
const normalizedBatchDetail = __resolveCityDetailFromBatchForTest(
{
"hong kong": {
city: "hong kong",
timeseries: { hourly: { times: ["00:00"], temps: [32] } },
},
} as any,
"Hong Kong",
) as any;
assert(
normalizedBatchDetail?.city === "hong kong",
"frontend detail batch lookup should accept backend-normalized city keys before falling back to single-city requests",
);
__resetHourlyDetailRequestQueueForTest();
let activeRequests = 0;
@@ -1073,6 +1073,30 @@ function rejectBatchWaiters(
(waiters || []).forEach((waiter) => waiter.reject(reason));
}
function resolveCityDetailFromBatch(
details: Record<string, CityDetail | null | undefined> | undefined,
city: string,
) {
if (!details) return undefined;
const trimmed = String(city || "").trim();
const direct =
details[city] ||
details[trimmed] ||
details[trimmed.toLowerCase()] ||
details[normalizeCityKey(trimmed)];
if (direct) return direct;
const requestedKey = normalizeCityKey(trimmed);
if (!requestedKey) return undefined;
for (const [key, detail] of Object.entries(details)) {
if (!detail) continue;
if (normalizeCityKey(key) === requestedKey) return detail;
const detailCity = (detail as any).city || detail.name || detail.display_name;
if (normalizeCityKey(detailCity) === requestedKey) return detail;
}
return undefined;
}
async function flushCityDetailBatch(resolution: string) {
const queue = _cityDetailBatchQueues.get(resolution);
if (!queue) return;
@@ -1091,7 +1115,7 @@ async function flushCityDetailBatch(resolution: string) {
await Promise.all(
cities.map(async (city) => {
const waiters = queue.waiters.get(city);
const detail = details[city];
const detail = resolveCityDetailFromBatch(details, city);
const data = primeCityDetailCache(city, resolution, detail);
if (data) {
resolveBatchWaiters(waiters, data);
@@ -2426,6 +2450,7 @@ export {
HOURLY_CACHE_TTL_MS,
_hourlyCache,
__readHourlyCacheEntryForTest,
resolveCityDetailFromBatch as __resolveCityDetailFromBatchForTest,
__resetHourlyDetailRequestQueueForTest,
__runQueuedHourlyDetailRequestForTest,
buildChartDomain,
@@ -0,0 +1,60 @@
import assert from "node:assert/strict";
import fs from "node:fs";
import path from "node:path";
function readFrontend(...parts: string[]) {
return fs.readFileSync(path.join(process.cwd(), ...parts), "utf8");
}
export function runTests() {
const timingSource = readFrontend("lib", "proxy-timing.ts");
assert.match(
timingSource,
/createProxyTimer/,
"shared proxy timing helper should create timers for slow API proxies",
);
assert.match(
timingSource,
/Server-Timing/,
"shared proxy timing helper should write Server-Timing headers for HAR inspection",
);
assert.doesNotMatch(
timingSource,
/authUserId|authEmail|userId|email/,
"proxy timing logs must avoid raw user ids or emails",
);
const apiProxySource = readFrontend("lib", "api-proxy.ts");
assert.match(
apiProxySource,
/timing\?: ProxyTimer/,
"generic backend JSON proxy should accept an optional timer",
);
for (const stage of ["auth_headers", "backend_fetch", "backend_read"]) {
assert.match(
apiProxySource,
new RegExp(stage),
`generic backend JSON proxy should measure ${stage}`,
);
}
const detailBatchProxy = readFrontend("app", "api", "cities", "detail-batch", "route.ts");
assert.match(detailBatchProxy, /createProxyTimer\(req,\s*"city_detail_batch"\)/);
assert.match(detailBatchProxy, /timing:\s*timer/);
const scanTerminalProxy = readFrontend("app", "api", "scan", "terminal", "route.ts");
assert.match(scanTerminalProxy, /createProxyTimer\(req,\s*"scan_terminal"\)/);
assert.match(scanTerminalProxy, /timing:\s*timer/);
const cityDetailProxy = readFrontend("app", "api", "city", "[name]", "detail", "route.ts");
assert.match(cityDetailProxy, /createProxyTimer\(req,\s*"city_detail"\)/);
for (const stage of ["auth_headers", "backend_fetch", "backend_read"]) {
assert.match(cityDetailProxy, new RegExp(stage));
}
const onlineUsersProxy = readFrontend("app", "api", "ops", "online-users", "route.ts");
assert.match(onlineUsersProxy, /createProxyTimer\(req,\s*"ops_online_users"\)/);
for (const stage of ["auth_headers", "ops_auth", "backend_fetch", "backend_read"]) {
assert.match(onlineUsersProxy, new RegExp(stage));
}
}