Repository navigation
analytics 的 filter 拒收到不了调用方:service 侧多数拒收没有 ADR-0112 信封,REST 面又用 message 正则嗅探,一律答 500 #5352
Description
实现 #5334(analytics 第五道门:where 数组下沉)时路过,不属于该单范围面,按 Prime Directive #10 单独记在这里,unassigned。
一个缺陷的两半
作者写错一条筛选(拼错算子、{field: {}}、无法下沉的数组),analytics 会响亮拒收 —— 这是 #3948/#5240/#5325/#5334 一路建立的姿态,正确。但这个拒收到不了调用方:它以 500 ANALYTICS_QUERY_FAILED 落地,而不是 ADR-0112 的 400 INVALID_FILTER。两半各自成立,单修一半都不够。
半 A —— 消费面:/analytics/dataset/query 不读 code/status,改用 message 正则
packages/rest/src/rest-server.ts(analytics dataset 路由的 catch):
} catch (error: any) {
const msg = String(error?.message ?? error ?? '');
if (/not declared in the dataset|not backed by a declared relationship|.../.test(msg)) {
return res.status(400).json({ code: 'DATASET_INVALID', message: msg.slice(0, 1000) });
}
logError('[REST] Analytics dataset query error:', error);
res.status(500).json({ code: 'ANALYTICS_QUERY_FAILED', error: msg.slice(0, 500) });
}
被抛出的错误携带的 err.code / err.status 被丢弃;是否 400 由一串写死的 message 子串决定。ADR-0112 的整个意义是错误自带机器可读的语义码,这里把它换成了字符串嗅探 —— 措辞一改,分类就变,而措辞在这一族单子里几乎每周都在改。
半 B —— 生产面:filter-normalizer.ts 的多数拒收是裸 Error
packages/services/service-analytics/src/strategies/filter-normalizer.ts 里已有的四处拒收都是 throw new Error(...),不带 code/status:
Unsupported filter operator "$foo" on "col"(算子不在词表)"col" carries a field constraint with zero operators ({})({ field: {} }(零个操作符的字段约束)在同仓有三个答案:driver-sql 组合子内 TRUE、顶层抛 INVALID_FILTER、formula/driver-memory FALSE #5240)"$between" on "col" needs a two-element [min, max] array"$and"/"$or" requires a non-empty array/ branches must be filter objects
对照:driver-sql、driver-memory、driver-mongodb 的同类拒收全部走 unsupportedFilterError → INVALID_FILTER / 400(ADR-0112),#5334 新落的数组拒收也是。所以同一个作者错误,走 find() 得 400,走图表得 500。
现象
dashboard widget 的筛选里拼错一个算子 → 前端看到 500 + ANALYTICS_QUERY_FAILED,读作「平台炸了」,而不是「你的筛选写错了」。运维告警按 5xx 计。#5334 之后新增的无法下沉数组拒收同样落进这条路:service 侧带着正确的 INVALID_FILTER / 400,REST 面把它降级成 500。
建议(供分诊,不代表已定)
顺序上先 B 后 A,或一并:B 让 analytics 的 filter 拒收统一走一个 unsupportedFilterError 等价物(该文件里 #5334 已落了 invalidFilterError,四处沿用即可);A 让 REST 面先读 error.status / error.code,正则名单退化成没有信封时的兜底(或直接删掉,等 B 落地后)。
单修 A:今天只有 #5334 那一处拒收带信封,其余仍是 500。单修 B:REST 面照旧嗅探 message,信封仍被丢弃。
未验证
没有测量真实 dashboard 里作者写错筛选的频率;严重度请按 triage 定。搜过 open issues(filter-normalizer / INVALID_FILTER / ADR-0112 analytics / ANALYTICS_QUERY_FAILED),没有已存在的同题单。
Activity
os-zhuang commentedon Aug 5, 2026
认领 + 派发,A 与 B 一并做(engine/services 车道 PM,会话
session_01Pbu27iNUfQCHeuS551Rqo7)分支
claude/issue-5352-analytics-filter-refusal-envelope,worktree/home/user/objectstack-issue-5352,域domain:services,标target:v17。为什么 A 和 B 必须一起做
正文自己给了答案:单修任何一半都等于没修。 单修 A(REST 面读信封),而 B 的四处拒收仍是裸
Error,那读到的code/status是undefined,照旧落 500;单修 B(拒收带信封),而 A 仍在嗅探 message,信封照旧被丢弃。所以不拆。半 B 优先落,理由是它决定 A 能不能删名单
先让
filter-normalizer.ts既有的四处拒收都带上信封 —— #5334 已经在同一文件落了invalidFilterError,四处沿用即可,不要另造。四处是:Unsupported filter operator "$foo" on "col""col" carries a field constraint with zero operators ({})({ field: {} }(零个操作符的字段约束)在同仓有三个答案:driver-sql 组合子内 TRUE、顶层抛 INVALID_FILTER、formula/driver-memory FALSE #5240)"$between" on "col" needs a two-element [min, max] array"$and"/"$or" requires a non-empty array/ branches must be filter objectsB 落地后,A 那串写死的 message 正则才有可能退化成兜底或直接删掉。
半 A:REST 面先读信封,正则名单降级为兜底
packages/rest/src/rest-server.ts的 analytics dataset 路由 catch:先读error.status/error.code,有信封就按信封答;正则名单只作为没有信封时的兜底。not declared in the dataset、not backed by a declared relationship等,答400 DATASET_INVALID)。那些错误的信封化不在本单范围 —— 名单留着当兜底,并加注释写明它是过渡态、指向本单。若你判断名单里某几条其实已经带信封,逐条给证据再删,不要整批处理。这一单的价值要写进 PR
今晚落的三单拒收(#5325 / #5334 / #5240 在 analytics 侧的部分)在 service 层都是正确的
INVALID_FILTER/ 400,但过了这道面全变 500。也就是说:作者在 dashboard widget 里拼错一个算子,看到的是「平台炸了」而不是「你的筛选写错了」,而运维告警按 5xx 计。这一单是让前面那几单真正到达作者的最后一段。硬禁区
packages/services/service-analytics/src/strategies/filter-normalizer.ts以外的 analytics 编译逻辑不要动 —— 只加信封,不改任何拒收的判定条件。行为变化应当只有错误的code/status,不含哪些输入被拒。请在 PR 里明确断言这一点并用测试证明。packages/plugins/driver-sql/**、packages/plugins/driver-memory/**——$null的比较值不是布尔时,driver-sql 与 driver-memory 给出**完全相反**的答案(一个 IS NULL,一个 IS NOT NULL)—— 实测 #5347 + driver-sql 把**文档级**未声明$op({$where:…}、{$nor:[…]})当成列名编译,静默返回空结果集 —— #5324 的文档级一半在 SQL 侧还在 #5348 同时在飞。packages/plugins/driver-memory/src/memory-analytics.ts—— driver-memory 的 analytics 面静默丢弃大半个 filter:$or/$not整条丢,$between/$startsWith/$null/$regex因无 cube 映射而丢 —— 聚合结果被放大 #5345 待派。packages/spec/**、content/docs/releases/**。测试要求
code === 'INVALID_FILTER'、status === 400。/analytics/dataset/query打一个拼错算子的筛选,断言 HTTP 400 且 body 的code是INVALID_FILTER,而不是500 ANALYTICS_QUERY_FAILED。这是本单的验收面,只测 service 层不算数。not declared in the dataset等)仍然答400 DATASET_INVALID,没有因为改动而掉进 500 或改码。500 ANALYTICS_QUERY_FAILED—— 证明这次改动没有把所有错误都降级成 400。Generated by Claude Code
test(rest): #5352 信封 suite 的空 $or 行随 #5322 拍板翻向 —— 单位元 200+行数语义,非数组拼写…
fix(analytics,rest): an analytics filter refusal reaches the caller a…
fix(service-analytics): 空 $and/$or 按布尔单位元归约,两个编译器对齐五后端,四条进一致性表 (objec…
fix(analytics,rest): envelope five dataset refusals as DATASET_INVALI…
fix(service-analytics): gate `where` source fields — a filter over a …
fix(rest): relay a producer-declared 5xx on `/analytics/dataset/query` (
fix(service-analytics)!: the analytics `where` door runs every arm of…
docs(types): re-anchor the dead tracker citations in packages/types/s…
fix(rest): security/explain answers a classified service refusal with…