观察
#5352 / #5367 处理的是 rest-server.ts 里那串「按 message 措辞决定 HTTP 码」的正则。packages/services/service-analytics/src/analytics-service.ts 里还有第二个同类嗅探器 ,而且它的后果比选错状态码更重:命中即把错误换成一个空结果。
// analytics-service.ts:83
function isMissingSourceError(err: unknown): boolean {
const msg = String((err as { message?: unknown })?.message ?? err ?? '').toLowerCase();
return (
msg.includes('no such table') ||
(msg.includes('relation') && msg.includes('does not exist')) ||
msg.includes("doesn't exist") ||
msg.includes('not registered') ||
msg.includes('unknown object') ||
msg.includes('is not a registered object')
);
}
命中后 queryDataset 的 catch(analytics-service.ts:700)会 logger.warn 一句然后
return { rows: [], fields: [], totals: [] } —— 也就是 widget 画一个自信的空图,而不是报错。
这个降级本身是刻意的(对象没在本 kernel 挂载时 widget 应当显示「无数据」而非 500),#5033 已经把跨库那一支拆出来单独响亮报错。问题在于判据是措辞 。
已经存在的一次命中(今天无害)
dataset-compiler.ts:260 的拒收原文:
[dataset-compiler] dataset "X" includes relationship "R" which does not exist on object "O".
它同时含 relation(在 relationship 里)和 does not exist → isMissingSourceError 返回 true 。
今天不出事的唯一原因是抛点在 try 之外 :queryDataset 先 const compiled = this.registerDataset(dataset)(第 626 行,compileDataset 在这里抛),而那个 try/catch 在第 697 行才开始。也就是说这是一条已经装好但没接电的雷:
属于 observation 类:今天没有用户会撞到,grep 也不会告诉你两条措辞正在互相纠缠。
可能的方向(不预判,留给 triage)
A. 判据换成结构而非措辞 :让「源缺失」由驱动侧带 code(例如 ERR_DATASOURCE_UNAVAILABLE 已在 ERROR_CODE_LEDGER 里)或由 isRegisteredObject 探针回答,isMissingSourceError 退化为兼容层并排到 code 判据之后。
B. 最小防守 :降级路径只对「不带 ADR-0112 信封的错误」生效 —— 一个已经自称 4xx 的拒收(analytics dataset 路由的 message 正则兜底没有退休时间表:六族拒收仍靠措辞分类,改一个字就换一个 HTTP 码 #5367 之后 dataset-compiler 的两处就是)永远不该被降级成空结果。这一条无论 A 做不做都成立,且能直接把上面那条雷拆掉。
C. 收紧 relation/does not exist 这一支 ,让它只匹配 Postgres 的实际措辞(relation "x" does not exist,带引号的关系名),而不是任意同时含两个词的句子。
参考
观察
#5352 / #5367 处理的是
rest-server.ts里那串「按 message 措辞决定 HTTP 码」的正则。packages/services/service-analytics/src/analytics-service.ts里还有第二个同类嗅探器,而且它的后果比选错状态码更重:命中即把错误换成一个空结果。命中后
queryDataset的 catch(analytics-service.ts:700)会logger.warn一句然后return { rows: [], fields: [], totals: [] }—— 也就是 widget 画一个自信的空图,而不是报错。这个降级本身是刻意的(对象没在本 kernel 挂载时 widget 应当显示「无数据」而非 500),#5033 已经把跨库那一支拆出来单独响亮报错。问题在于判据是措辞。
已经存在的一次命中(今天无害)
dataset-compiler.ts:260的拒收原文:它同时含
relation(在relationship里)和does not exist→isMissingSourceError返回 true。今天不出事的唯一原因是抛点在 try 之外:
queryDataset先const compiled = this.registerDataset(dataset)(第 626 行,compileDataset在这里抛),而那个 try/catch 在第 697 行才开始。也就是说这是一条已经装好但没接电的雷:属于 observation 类:今天没有用户会撞到,
grep也不会告诉你两条措辞正在互相纠缠。可能的方向(不预判,留给 triage)
ERR_DATASOURCE_UNAVAILABLE已在ERROR_CODE_LEDGER里)或由isRegisteredObject探针回答,isMissingSourceError退化为兼容层并排到 code 判据之后。dataset-compiler的两处就是)永远不该被降级成空结果。这一条无论 A 做不做都成立,且能直接把上面那条雷拆掉。relation/does not exist这一支,让它只匹配 Postgres 的实际措辞(relation "x" does not exist,带引号的关系名),而不是任意同时含两个词的句子。参考
packages/services/service-analytics/src/analytics-service.ts:83(isMissingSourceError)、:112(missingSourceRelation)、:626(编译点)、:697-:730(降级 catch)packages/services/service-analytics/src/dataset-compiler.ts:260(命中的那条措辞)