Repository navigation
Setup System Overview: every KPI tile reads 0 — the dataset path lowers an all-time range to WHERE created_at = $1 #4475
Description
Activity
Tracked under the v17 verification tracker #3909.
Rolled up in #4482 (all 17 defects from the v17 verification, grouped by severity with a suggested RC-exit triage).
交接说明(进行中)
分支:
claude/v17-verification-defects-gnf9e6-analytics(framework 与 objectui 两个仓库同名分支;framework 侧已推送,objectui 侧修复尚未提交)已完成:根因定位,并且不在 framework 的 dataset 路径上
我用 curl 把 Console 实际发出的那个请求复现出来了(issue 里说没能从 curl 复现,关键是
dataset要内联传、selection.measures必须非空):POST /api/v1/analytics/dataset/query {"dataset":{"name":"sys_user_metrics","object":"sys_user","dimensions":[], "measures":[{"name":"user_count","label":"Users","aggregate":"count"}]}, "selection":{"measures":["user_count"], "runtimeFilter":{"created_at":"last_7_days"}}} → {"rows":[{"user_count":0}], "sql":"SELECT COUNT(*) AS \"user_count\" FROM \"sys_user\" WHERE created_at = $1"}和 issue 里贴的 SQL 一字不差。 而同一条路径拿到真正的区间时,lowering 是正确的:
runtimeFilter:{"created_at":{"$gte":"{7_days_ago}","$lte":"{today}"}} → "… WHERE (created_at >= $1 AND created_at < $2)" rows: [{"user_count":4}] 无 filter → rows: [{"user_count":4}] /data/sys_user → total 4所以 dataset 路径没有把 all-time 区间降级成等值比较——它收到的本来就是一个标量。真正的问题是 Console 把一个裸预设名字符串当成比较值发了出来。
链路:
- framework
packages/platform-objects/src/apps/dashboards/system_overview.dashboard.ts声明
globalFilters: [{ field:'created_at', type:'date', defaultValue:'last_7_days' }]。
这是合规写法——spec 的GlobalFilterSchema.defaultValue是z.union([string, number, boolean]),根本不允许对象形式,所以'last_7_days'是唯一能表达"默认最近 7 天"的拼法。 - objectui
packages/core/src/utils/dashboard-filters.ts的resolveDashboardFilterDefs:内置的dateRange声明会把预设名字符串归一化成{ preset }(defaultValue: preset && preset !== 'custom' ? { preset } : undefined),但globalFilters分支是原样透传f.defaultValue。这个不对称就是 bug。 - 于是
buildFilterCondition里typeof v === 'object'为 false,走到 "A bare string date means equality on that day" 那一支,原样返回'last_7_days'→WHERE created_at = 'last_7_days'→ 恒 0。 - 同一个不对称也解释了为什么选择器显示"全部时间":
DateRangeFilter的selectValue = value?.preset ?? (value?.from || value?.to ? CUSTOM : ALL),裸字符串上这三个属性都是 undefined,所以 UI 落到ALL_VALUE显示"全部时间",而底下发出去的却是那个字符串。"显示全部时间"和"每块 KPI 读 0"是同一个根因的两个症状,不是两件事。
当前卡在哪
objectui worktree(
/home/user/objectui-analytics)已建好、依赖已装完,但修复代码还没写、没提交。风险提示:这部分工作目前只存在于本次会话的分析里,远端没有。下一步具体该做什么
- 在 objectui 的
packages/core/src/utils/dashboard-filters.ts里,给resolveDashboardFilterDefs的globalFilters分支补上与内置dateRange同一套归一化:type为date/dateRange且defaultValue是已知预设名(PRESET_RANGES的 key)时,转成{ preset }。这不是"消费端加??兜底"——它是同一个归一化函数对兄弟声明补齐同一个转换,而且 spec 只允许标量,producer 侧没有别的写法可改。 - 回归测试:
dashboard-filters的单测里断言defaultValue:'last_7_days'归一化为{preset:'last_7_days'},且buildFilterCondition产出{$gte:'{7_days_ago}', $lte:'{today}'}而不是裸字符串;再补一条 DashboardFilterBar 显示 "Last 7 days" 而非 "All time"。 - 闸门:objectui 侧
pnpm --filter @object-ui/core test+pnpm typecheck。 - framework 侧不需要改
system_overview.dashboard.ts——那份 metadata 是合规的,改它只会把 bug 藏起来。
残留问题(建议另开 issue,不在本次范围):
date型 filter 拿到一个既不是已知预设、也不是合法 ISO 日期的字符串时,仍会静默降级成一个永不命中的等值比较,继续产生"看着健康的 0"。预设名这一类由上面的修复覆盖,但拼错的自定义值不会。
Generated by Claude Code
- framework
- added a commit that references this issue
on Aug 1, 2026 已修复并合并:objectstack-ai/objectui#3150(跨仓库的
Fixes不会自动关闭,故手动收口)。本 issue 的归因不成立,实际根因在 UI 侧。 原文推测 dataset 路径把 all-time 区间降级成了等值比较。实测证明 dataset 路径拿到真正的区间时 lowering 完全正确——它收到的本来就是一个标量:
runtimeFilter: {"created_at": "last_7_days"} → WHERE created_at = $1 → 0 runtimeFilter: {"created_at": {"$gte":…,"$lte":…}} → WHERE (created_at >= $1 AND created_at < $2) → 4上面第一条与本 issue 贴出的 SQL 一字不差。真凶是 Console 把一个裸的预设名字符串当成比较值发了出去:
resolveDashboardFilterDefs会把内置dateRange声明的预设名提升为{ preset },但globalFilters分支原样透传defaultValue;而GlobalFilterSchema.defaultValue是string | number | boolean,裸预设名是作者唯一能写的拼法——所以 framework 侧的system_overview.dashboard.ts是合规的,不该改,改它只会把缺陷藏起来。同一处缺失还解释了"周期选择器显示全部时间":
DateRangeFilter从value.preset/.from/.to推导选中项,裸字符串上三者皆为undefined,于是落到 ALL 哨兵。两个症状是同一个根因,这也是为什么界面看起来像"刻意没过滤、只是恰好没数据",而不像出错。验证:真实 dev server,Users 由 0 → 4(与
/data一致),Sessions 同步恢复;新增回归测试覆盖预设名提升、提升后产出区间而非等值、真实 ISO 日期仍为当天等值、非 date 型 filter 不受影响。相邻问题另立:objectstack-ai/objectui#3151 —— date filter 拿到既非已知预设、也非合法 ISO 日期的字符串时,仍会静默降级成永不命中的等值比较,继续产生"看着健康的 0"。
Generated by Claude Code
Found while verifying the 17.0.0-rc.1 checklist on #3909. Verified in the browser on
main@1ee48bc60, showcase underos serve --dev --ui, published@objectstack/console@17.0.0-rc.1at/_console/, signed in as the dev admin.What you see
Setup → 系统概览 / System Overview with the period selector on 全部时间 (All time) renders every KPI tile as 0:
The data is there — the same page, same session, querying the platform's own API returns the truth:
Root cause is visible in the widget's own response
The tiles go through
POST /api/v1/analytics/dataset/query, and that response carries the SQL it ran:{"rows":[{"user_count":0}], "fields":[{"name":"user_count","type":"number","label":"Users"}], "sql":"SELECT COUNT(*) AS \"user_count\" FROM \"sys_user\" WHERE created_at = $1"}WHERE created_at = $1— an equality test on a timestamp column. An "all time" selection is being lowered to a single-value comparison instead of an unbounded range (or no predicate at all), so it matches only rows whosecreated_atis exactly the bound and the count is always 0.Note the contrast with the cube path, which lowers a range correctly (verified separately for #3650):
So the defect is specific to how the dataset path compiles its date filter, not to date handling generally.
Why it matters for the RC
This is the first screen an operator sees in Setup, and every number on it is wrong in the safest-looking direction — zeros read as "nothing is happening yet" rather than as an error. Nothing in the UI signals a failure: the requests are
200 OK, the widgets render normally.I could not exercise the dataset route directly from curl to narrow it further — it answers
body.selection.measures must be a non-empty array of measure namesfor the shapes I tried, so the request the Console builds is the reliable reproduction. Open Setup → System Overview with "All time" selected and read thesqlfield of any tile's response.Part of the #3909 rc.0/rc.1 verification.