81 lines
3.0 KiB
JavaScript
81 lines
3.0 KiB
JavaScript
// Errored function invocations still bill at full duration, so high-volume 5xx is a cost issue, not just reliability.
|
|
import { withRouteShapeWarnings } from '../route-normalize.mjs';
|
|
|
|
const MIN_VOLUME_FOR_RATE_EMISSION = 1000;
|
|
|
|
export const metadata = {
|
|
id: 'route_errors',
|
|
threshold: `count > 250 OR (totalRequests >= ${MIN_VOLUME_FOR_RATE_EMISSION} AND errorRate > 0.01)`,
|
|
billingDimension: 'function-duration',
|
|
scope: 'route',
|
|
sourceCitation: 'vercel-optimize gate threshold',
|
|
description:
|
|
'Routes producing > 250 5xx errors over the window, or with > 1% error rate on at least 1,000 total requests. Errored function invocations still bill at full duration; high error rates also poison user experience.',
|
|
};
|
|
|
|
export function gate(signals) {
|
|
const errors = extractErrors(signals);
|
|
return errors
|
|
.filter((e) => e.count > 250 || (e.total >= MIN_VOLUME_FOR_RATE_EMISSION && (e.errorRate ?? 0) > 0.01))
|
|
.map((e) => withRouteShapeWarnings({
|
|
kind: metadata.id,
|
|
scope: 'route',
|
|
route: e.route,
|
|
files: [],
|
|
priority: e.count,
|
|
confidence: 0.93,
|
|
o11ySignal: e.errorRate != null
|
|
? `errs=${e.count},rate=${(e.errorRate * 100).toFixed(1)}%`
|
|
: `errs=${e.count}`,
|
|
reason: 'concentrated 5xx errors',
|
|
question: `Why does ${e.route} produce ${e.count} 5xx errors over the window, and what code path is failing?`,
|
|
evidence: { metric: e.metric, route: e.route, count: e.count, totalRequests: e.total, errorRate: e.errorRate },
|
|
}, signals));
|
|
}
|
|
|
|
function extractErrors(signals) {
|
|
const fnStatus = signals.metrics?.fnStatusByRoute;
|
|
if (Array.isArray(fnStatus?.rows)) return extractFromStatusRows(fnStatus.rows, 'fnStatusByRoute');
|
|
|
|
const m = signals.metrics?.requestsByRouteStatus;
|
|
const cache = signals.metrics?.requestsByRouteCache;
|
|
if (!m?.ok && !Array.isArray(m?.rows)) return [];
|
|
|
|
const errors = extractFromStatusRows(m?.rows ?? [], 'requestsByRouteStatus');
|
|
|
|
// cache rollup is summed across cache_result, giving per-route total request count.
|
|
const totalByRoute = new Map();
|
|
for (const row of (cache?.rows ?? [])) {
|
|
if (!row.route) continue;
|
|
totalByRoute.set(row.route, (totalByRoute.get(row.route) ?? 0) + (row.value ?? 0));
|
|
}
|
|
|
|
return errors.map((e) => {
|
|
const total = totalByRoute.get(e.route) ?? 0;
|
|
return {
|
|
...e,
|
|
total,
|
|
errorRate: total > 0 ? e.count / total : null,
|
|
};
|
|
});
|
|
}
|
|
|
|
function extractFromStatusRows(rows, metric) {
|
|
const errByRoute = new Map();
|
|
const totalByRoute = new Map();
|
|
for (const row of rows) {
|
|
const route = row.route;
|
|
if (!route) continue;
|
|
const v = row.value ?? 0;
|
|
const status = String(row.http_status ?? '');
|
|
if (/^5\d\d$/.test(status)) errByRoute.set(route, (errByRoute.get(route) ?? 0) + v);
|
|
totalByRoute.set(route, (totalByRoute.get(route) ?? 0) + v);
|
|
}
|
|
|
|
return [...errByRoute.entries()].map(([route, count]) => {
|
|
const total = totalByRoute.get(route) ?? 0;
|
|
const errorRate = total > 0 ? count / total : null;
|
|
return { route, count, total, errorRate, metric };
|
|
});
|
|
}
|