fix(traffic): name an account's traffic limit instead of a daily budget

A failed flow tile now carries the server's reason. A hosted deployment that limits each account answers USAGE_LIMIT_REACHED or RATE_LIMITED, which the traffic panel labels as the monthly allowance or a rate limit; a 503 with another reason reads as the traffic service being unavailable, not a missing key. The local proxy's no_key and daily budget labels are unchanged.
This commit is contained in:
Sameh Khamis committed 2026-11-05 21:21:26 -08:00
1 parent ad009ea1eb
commit 58adbb11c7
15 files changed
+119 -1

No files matched your search

+22
View File
@@ -30,6 +30,28 @@ test('a superseded flow fetch is not an outage', () => {
assert.equal(deriveTrafficFlowError(undefined), null);
});
test('hosted account limits and service failures name their own reason', () => {
const failure = (status, code) =>
deriveTrafficFlowError(
Object.assign(new Error(`TomTom flow unavailable (HTTP ${status})`), {
status,
code,
}),
);
assert.equal(
failure(429, 'USAGE_LIMIT_REACHED'),
'Monthly traffic allowance used',
);
assert.equal(failure(429, 'RATE_LIMITED'), 'Traffic rate limit reached');
assert.equal(failure(429, 'budget'), 'TomTom daily budget reached');
assert.equal(failure(503, 'no_key'), 'TomTom key unavailable');
assert.equal(failure(503, null), 'TomTom key unavailable');
assert.equal(
failure(503, 'USAGE_UNAVAILABLE'),
'Traffic service unavailable',
);
});
test('flow failures map onto short, specific reasons', () => {
const reason = (message) => deriveTrafficFlowError(new Error(message));
assert.equal(reason('flow tile 12/1/1: HTTP 503'), 'TomTom key unavailable');
+3
View File
@@ -1749,6 +1749,9 @@
"Hybrid": "هجين",
"TomTom key unavailable": "مفتاح TomTom غير متاح",
"TomTom daily budget reached": "بلغت الحصة اليومية لـ TomTom",
"Monthly traffic allowance used": "تم استهلاك حصة حركة المرور الشهرية",
"Traffic rate limit reached": "تم بلوغ حد طلبات حركة المرور",
"Traffic service unavailable": "خدمة حركة المرور غير متاحة",
"TomTom upstream unreachable": "تعذّر الوصول إلى خادم TomTom",
"TomTom flow unavailable": "تدفق TomTom غير متاح",
"SIMULATED — {reason}": "محاكى — {reason}",
+3
View File
@@ -1667,6 +1667,9 @@
"Hybrid": "Hybrid",
"TomTom key unavailable": "TomTom-Schlüssel nicht verfügbar",
"TomTom daily budget reached": "TomTom-Tagesbudget erreicht",
"Monthly traffic allowance used": "Monatliches Verkehrskontingent aufgebraucht",
"Traffic rate limit reached": "Verkehrs-Anfragelimit erreicht",
"Traffic service unavailable": "Verkehrsdienst nicht verfügbar",
"TomTom upstream unreachable": "TomTom-Upstream nicht erreichbar",
"TomTom flow unavailable": "TomTom-Verkehrsfluss nicht verfügbar",
"SIMULATED — {reason}": "SIMULIERT – {reason}",
+3
View File
@@ -2274,6 +2274,9 @@
"SIMULATED — {reason}": "SIMULADO — {reason}",
"TomTom key unavailable": "Clave de TomTom no disponible",
"TomTom daily budget reached": "Se alcanzó el límite diario de TomTom",
"Monthly traffic allowance used": "Cupo mensual de tráfico agotado",
"Traffic rate limit reached": "Límite de solicitudes de tráfico alcanzado",
"Traffic service unavailable": "Servicio de tráfico no disponible",
"TomTom upstream unreachable": "Servidor de TomTom inaccesible",
"TomTom flow unavailable": "Flujo de TomTom no disponible",
"API key rejected — check AISSTREAM_API_KEY": "Clave de API rechazada — revisa AISSTREAM_API_KEY",
+3
View File
@@ -1105,6 +1105,9 @@
"Hybrid": "Hybride",
"TomTom key unavailable": "Clé TomTom indisponible",
"TomTom daily budget reached": "Quota quotidien TomTom atteint",
"Monthly traffic allowance used": "Quota mensuel de trafic épuisé",
"Traffic rate limit reached": "Limite de requêtes de trafic atteinte",
"Traffic service unavailable": "Service de trafic indisponible",
"TomTom upstream unreachable": "Serveur TomTom injoignable",
"TomTom flow unavailable": "Flux TomTom indisponible",
"SIMULATED — {reason}": "SIMULÉ — {reason}",
+3
View File
@@ -1105,6 +1105,9 @@
"Hybrid": "Ibrido",
"TomTom key unavailable": "Chiave TomTom non disponibile",
"TomTom daily budget reached": "Budget giornaliero TomTom esaurito",
"Monthly traffic allowance used": "Quota mensile del traffico esaurita",
"Traffic rate limit reached": "Limite di richieste del traffico raggiunto",
"Traffic service unavailable": "Servizio traffico non disponibile",
"TomTom upstream unreachable": "Servizio TomTom irraggiungibile",
"TomTom flow unavailable": "Flusso TomTom non disponibile",
"SIMULATED — {reason}": "SIMULATO: {reason}",
+3
View File
@@ -1073,6 +1073,9 @@
"Hybrid": "ハイブリッド",
"TomTom key unavailable": "TomTomキーを利用できません",
"TomTom daily budget reached": "TomTomの1日の上限に達しました",
"Monthly traffic allowance used": "今月の交通情報の上限に達しました",
"Traffic rate limit reached": "交通情報のリクエスト上限に達しました",
"Traffic service unavailable": "交通情報サービスを利用できません",
"TomTom upstream unreachable": "TomTomのサービスに接続できません",
"TomTom flow unavailable": "TomTomの交通流を利用できません",
"SIMULATED — {reason}": "シミュレーション:{reason}",
+3
View File
@@ -1103,6 +1103,9 @@
"Hybrid": "Híbrido",
"TomTom key unavailable": "Chave do TomTom indisponível",
"TomTom daily budget reached": "Limite diário do TomTom atingido",
"Monthly traffic allowance used": "Cota mensal de trânsito esgotada",
"Traffic rate limit reached": "Limite de solicitações de trânsito atingido",
"Traffic service unavailable": "Serviço de trânsito indisponível",
"TomTom upstream unreachable": "Servidor do TomTom inacessível",
"TomTom flow unavailable": "Fluxo do TomTom indisponível",
"SIMULATED — {reason}": "SIMULADO — {reason}",
+3
View File
@@ -2313,6 +2313,9 @@
"SIMULATED — {reason}": "СИМУЛЯЦИЯ — {reason}",
"TomTom key unavailable": "Ключ TomTom недоступен",
"TomTom daily budget reached": "Исчерпан дневной лимит TomTom",
"Monthly traffic allowance used": "Месячный лимит данных о трафике исчерпан",
"Traffic rate limit reached": "Достигнут лимит запросов данных о трафике",
"Traffic service unavailable": "Сервис данных о трафике недоступен",
"TomTom upstream unreachable": "Сервер TomTom не отвечает",
"TomTom flow unavailable": "Поток TomTom недоступен",
"API key rejected — check AISSTREAM_API_KEY": "Ключ API отклонён — проверьте AISSTREAM_API_KEY",
+3
View File
@@ -1120,6 +1120,9 @@
"Hybrid": "Гібрид",
"TomTom key unavailable": "Ключ TomTom недоступний",
"TomTom daily budget reached": "Вичерпано денний ліміт TomTom",
"Monthly traffic allowance used": "Місячний ліміт даних про трафік вичерпано",
"Traffic rate limit reached": "Досягнуто ліміту запитів даних про трафік",
"Traffic service unavailable": "Сервіс даних про трафік недоступний",
"TomTom upstream unreachable": "Сервер TomTom не відповідає",
"TomTom flow unavailable": "Потік TomTom недоступний",
"SIMULATED — {reason}": "ІМІТАЦІЯ — {reason}",
+3
View File
@@ -1049,6 +1049,9 @@
"Hybrid": "混合",
"TomTom key unavailable": "TomTom 密钥不可用",
"TomTom daily budget reached": "已达 TomTom 每日配额",
"Monthly traffic allowance used": "本月交通流量额度已用完",
"Traffic rate limit reached": "已达到交通数据请求频率上限",
"Traffic service unavailable": "交通服务不可用",
"TomTom upstream unreachable": "TomTom 上游服务无法连接",
"TomTom flow unavailable": "TomTom 路况不可用",
"SIMULATED — {reason}": "模拟——{reason}",
+3
View File
@@ -1044,6 +1044,9 @@
"Hybrid": "混合",
"TomTom key unavailable": "TomTom 密鑰不可用",
"TomTom daily budget reached": "已達 TomTom 每日配額",
"Monthly traffic allowance used": "本月交通流量額度已用完",
"Traffic rate limit reached": "已達交通資料請求頻率上限",
"Traffic service unavailable": "交通服務無法使用",
"TomTom upstream unreachable": "TomTom 上游服務無法連接",
"TomTom flow unavailable": "TomTom 路況不可用",
"SIMULATED — {reason}": "模擬——{reason}",
+8
View File
@@ -38,7 +38,15 @@ export function createFlow({ state: layerState, services, parts, source }) {
const status = Number.isFinite(error.status)
? error.status
: Number(message.match(/HTTP (\d{3})/)?.[1]);
const code = typeof error.code === 'string' ? error.code : null;
// English in state; trafficFeedPresentation translates it for the panel.
// A hosted deployment limits each account (code from the server); the
// local proxy has one daily budget (`budget`) and answers `no_key`.
if (code === 'USAGE_LIMIT_REACHED')
return msg('Monthly traffic allowance used');
if (code === 'RATE_LIMITED') return msg('Traffic rate limit reached');
if (status === 503 && code && code !== 'no_key')
return msg('Traffic service unavailable');
if (status === 503) return msg('TomTom key unavailable');
if (status === 429) return msg('TomTom daily budget reached');
if (status === 502 || status === 504)
+30 -1
View File
@@ -11,6 +11,26 @@ const FLOW_TILE_PATH = (z, x, y) => `/api/tomtom/flow/${z}/${x}/${y}.pbf`;
* (STALE-BUDGET once the daily budget is spent, STALE-ERROR when TomTom
* fails), and when the proxy fetched it.
*/
/**
* The machine-readable reason a failed tile carries: `code` (hosted usage,
* e.g. USAGE_LIMIT_REACHED or RATE_LIMITED) or the local proxy's `error`
* (`no_key`, `budget`). Only short identifiers are kept; prose is ignored.
*/
async function failureCode(response) {
if (!/json/i.test(response.headers?.get?.('content-type') || '')) return null;
try {
const text = await response.text();
if (text.length > 4096) return null;
const body = JSON.parse(text);
const code = body?.code ?? body?.error;
return typeof code === 'string' && /^[A-Za-z_]{1,40}$/.test(code)
? code
: null;
} catch {
return null;
}
}
function tileFreshness(response) {
const status = response.headers?.get?.('x-tomtom-cache') || '';
const fetchedAt = Number(response.headers?.get?.('x-tomtom-fetched-at'));
@@ -60,7 +80,16 @@ export function createFlowTileSource({
},
fetchImpl: async (url, init) => {
const response = await fetchImpl(url, init);
if (response.ok && !init.signal.aborted) {
if (!response.ok) {
const code = await failureCode(response);
await response.body?.cancel().catch(() => {});
throw Object.assign(
// Classified for display by deriveTrafficFlowError.
new Error(`TomTom flow unavailable (HTTP ${response.status})`),
{ status: response.status, code },
);
}
if (!init.signal.aborted) {
const path = String(url);
const entry = {
mark: tileFreshness(response),
+26
View File
@@ -1050,3 +1050,29 @@ test('cancelling a flow body releases its freshness before a new request', async
assert.equal(getEventListeners(requestSignal, 'abort').length, 0);
assert.equal(source.getFlowSessionStats().cacheEntries, 0);
});
test('a failed flow tile carries the server reason for its label', async () => {
for (const [status, body, code] of [
[
429,
{ error: 'Monthly usage limit reached', code: 'USAGE_LIMIT_REACHED' },
'USAGE_LIMIT_REACHED',
],
[
429,
{ error: 'Request rate limit reached', code: 'RATE_LIMITED' },
'RATE_LIMITED',
],
[503, { error: 'no_key' }, 'no_key'],
[503, { error: 'Usage enforcement is temporarily unavailable' }, null],
]) {
const source = createFlowTileSource({
fetchImpl: async () => Response.json(body, { status }),
});
await assert.rejects(source.fetchFlowForBounds(bounds), (error) => {
assert.equal(error.status, status);
assert.equal(error.code, code);
return true;
});
}
});