Skip to content

[17.0-rc2验收] analytics: 带 measure-scoped filter / derived 度量的 dataset 查询,响应 fields 丢失维度描述符 → 表头回退成原始维度名(如 "owner" 而非 "Owner") #5537

Closed

Description

@yinlianghui

现象

当一个 dataset 查询的 measures 里包含带自身 filter 的度量(measure-scoped filter 子查询合并路径)或 derived 度量 时,POST /api/v1/analytics/dataset/query 返回的 fields 数组会完全省略维度(dimension)的描述符。维度值仍在 rows 里,但没有对应的 {name,type,label,format} 条目。于是任何消费者(console 表格渲染器)拿不到维度列的 label,只能回退到原始行键。

HotCRM 「Sales Performance」仪表盘上肉眼可见:同一个 owner 维度(dataset 声明 label:'Owner'),

  • Open Pipeline by Owner(度量无 filter)表头渲染为 Owner ✅
  • Win / Loss by Rep(度量 won_count/lost_count/… 各带 filter + win_rate 是 ratio)表头渲染为小写 owner ❌

而同一张 Win/Loss 表若把维度换成字符串维度 lead_source,表头显示 Lead Source —— 这是巧合:console 的回退是「按原始键 humanize」,lead_source humanize 恰好得到 Lead Source(与真 label 相同),owner humanize 得到不变的 owner(≠ label Owner),于是 owner 把这个 bug 暴露了出来。实际上两种维度的 label/format/type 元数据都丢了,只是字符串维度被 humanize 掩盖。

复现(curl,可直接执行)

BASE=http://localhost:4099
TOKEN=$(curl -s -X POST $BASE/api/v1/auth/sign-in/email -H 'Content-Type: application/json' \
  -d '{"email":"admin@objectos.ai","password":"admin123"}' | python3 -c 'import sys,json;print(json.load(sys.stdin)["token"])')

# A) 无 filter 度量 —— fields 含维度 owner ✅
curl -s -X POST $BASE/api/v1/analytics/dataset/query -H "Authorization: Bearer $TOKEN" -H 'Content-Type: application/json' \
  -d '{"datasetName":"opportunity_metrics","selection":{"dimensions":["owner"],"measures":["opp_count"]}}' \
  | python3 -c 'import sys,json;print([f["name"] for f in json.load(sys.stdin)["fields"]])'
# => ['owner', 'opp_count']

# B) 带 filter 的度量 —— fields 丢了维度 owner ❌
curl -s -X POST $BASE/api/v1/analytics/dataset/query -H "Authorization: Bearer $TOKEN" -H 'Content-Type: application/json' \
  -d '{"datasetName":"opportunity_metrics","selection":{"dimensions":["owner"],"measures":["won_count"]}}' \
  | python3 -c 'import sys,json;print([f["name"] for f in json.load(sys.stdin)["fields"]])'
# => ['won_count']            ← 维度 owner 不在 fields 里,但 rows 里有 owner

# C) 字符串维度同样丢失(被 humanize 掩盖) ❌
curl -s -X POST $BASE/api/v1/analytics/dataset/query -H "Authorization: Bearer $TOKEN" -H 'Content-Type: application/json' \
  -d '{"datasetName":"opportunity_metrics","selection":{"dimensions":["lead_source"],"measures":["won_count","decided_count","win_rate"]}}' \
  | python3 -c 'import sys,json;d=json.load(sys.stdin);print("fields",[f["name"] for f in d["fields"]],"rowkeys",list(d["rows"][0].keys()))'
# => fields ['won_count','decided_count','win_rate']  rowkeys ['lead_source','won_count','decided_count','win_rate']

复现两次稳定。浏览器侧(chromium + storageState 登录)抓 sales_dashboard 五张 table 的 <thead>,两次运行都得到:Open Pipeline by Owner → Owner;Quota Attainment by Rep → Owner(forecast_metrics);Win / Loss by Rep → owner;Win / Loss by Lead Source → Lead Source;pivot → Stage。

期望 vs 实际

  • 期望:无论度量是否带 filter / 是否 derived,响应 fields 都应包含被选维度的描述符(name/type/label/format),表头恒显示 Owner。
  • 实际:一旦查询走了「measure-scoped filter 子查询合并」或含 derived 度量的路径,fields 只剩度量列,维度描述符缺失;维度列的 label/format/type 全部丢失,渲染器回退到原始键。

落点分析

packages/services/service-analytics/src/analytics-service.ts queryDataset 尾部:

  • L873–907 把度量列的 label/format/currency/percentScale 富化到 result.fields(遍历已存在的 field 条目)。
  • L909–923 有一段专为维度补 label 的代码,注释即写着 “the measure-only enrichment above left dimension headers bare (the renderer then fell back to the raw dimension name)” —— 但它 for (const f of result.fields) 只富化已经存在于 result.fields 的维度条目,不会为缺失的维度新建 field 条目。

而在带 filter/derived 度量的路径上,度量来自 dataset-executor.ts 的补充分组子查询,经 mergeByDimensions 合并回主网格(见该文件 L28、L140–194 的 mergeByDimensions/partitionMeasures)。合并出的 result.fields 里根本没有维度列(rows 里有,fields 里没有),于是 L909–923 的富化对维度是空操作,原始键泄漏到前端。

建议修复方向(任一):在 executor 的合并路径上把被选维度的 field 描述符补进 result.fields;或把 L909–923 改成「维度缺失时新建 field 条目」而非仅富化已存在条目。console 端的 humanize 回退是次要症状,根因在服务端 fields 装配。

影响面

  • 前端:凡使用带 filter/derived 度量的 dataset table(win-rate 家族、任何 measure-level filter 的看板)维度列表头显示原始键;数值维度会额外丢失 format(如日期维度的 dateGranularity 格式、货币/百分比 scale)。
  • 数据正确、行数正确,仅列元数据丢失 → 归为 p2(体验/一致性,非数据错误)。

HotCRM 关联(仅记录,不在 hotcrm 重复立单):sales_dashboard 「Win / Loss by Rep」表头小写 owner。HotCRM 元数据 opportunity.dataset.ts 声明 { name:'owner', label:'Owner', field:'owner_id', type:'lookup' } 正确,问题在平台。

环境

hotcrm@0899b4f + @objectstack 17.0.0-rc.2;独立 dev server(file DB, --seed-admin, admin@objectos.ai)。

Activity

  1. added
    pm:queueReady for the PM dispatch loop
    domain:servicesRetired 2026-10-22, alias of domain:server (services surface); in-flight cards keep it until landed
    on Aug 5, 2026
  2. os-zhuang commented on Aug 5, 2026

    @os-zhuang
    Contributor

    分诊:入队 pm:queue,域 domain:services。

    • 落点锚定:packages/services/service-analytics/src/analytics-service.ts queryDataset 尾部的维度富化段(只富化 result.fields 中已存在的条目,不为缺失维度新建)与 dataset-executor.ts 的 mergeByDimensions 合并路径(合并产物 fields 不含维度列)。正文已给两条修复方向与可执行复现,属落点明确的具体缺陷(p2)。
    • 过时前提检查:origin/main 上该包近期仅 fix(service-analytics): 分析查询里的 null 比较数是 null 谓词,不是 = '' (#5332) #5525(null comparand)等无关改动,前提成立。
    • 查重:三仓 open issue/PR 无影子单、无在飞修复。

    本评论来自分诊座位 Routine(#5474 试点),不构成认领。


    Generated by Claude Code

  3. self-assigned this
    on Aug 5, 2026
  4. os-zhuang commented on Aug 5, 2026

    @os-zhuang
    Contributor

    认领(services 车道 PM 派发,session_01BWS4heBoAitLmzCLhcYdbK)


    Generated by Claude Code

  5. os-zhuang commented on Aug 6, 2026

    @os-zhuang
    Contributor

    PM 复核:ACCEPT — PR #5691(session_01BWS4heBoAitLmzCLhcYdbK 车道)

    处置:CI 全绿后转 ready 并开 auto-merge 进队。#5537 为 rc2 验收链(#5520→#5537)收尾单。


    Generated by Claude Code

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Labels

bugSomething isn't workingdomain:servicesRetired 2026-10-22, alias of domain:server (services surface); in-flight cards keep it until landedpm:queueReady for the PM dispatch loop

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions