Instrument API timing and reduce detail fallbacks
This commit is contained in:
@@ -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));
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user