Skip to content

Setup System Overview: every KPI tile reads 0 — the dataset path lowers an all-time range to WHERE created_at = $1 #4475

Description

@baozhoutao

Found while verifying the 17.0.0-rc.1 checklist on #3909. Verified in the browser on main @ 1ee48bc60, showcase under os serve --dev --ui, published @objectstack/console@17.0.0-rc.1 at /_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:

tile shown actual
用户总数 / Users 0 4
活跃会话 / Sessions 0 16
登录事件 / Login events 0 non-zero
权限变更, 配置变更 0 —

The data is there — the same page, same session, querying the platform's own API returns the truth:

GET  /api/v1/data/sys_user                                   → total 4
POST /api/v1/analytics/query {cube:'sys_user',measures:['count']} → [{"count":4}]

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 whose created_at is 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):

... FROM "showcase_invoice" WHERE (issued_on >= $1 AND issued_on < $2) GROUP BY date_trunc('month', issued_on)

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 names for the shapes I tried, so the request the Console builds is the reliable reproduction. Open Setup → System Overview with "All time" selected and read the sql field of any tile's response.

Part of the #3909 rc.0/rc.1 verification.

Activity

  1. baozhoutao commented on Aug 1, 2026

    @baozhoutao
    ContributorAuthor

    Tracked under the v17 verification tracker #3909.

  2. baozhoutao commented on Aug 1, 2026

    @baozhoutao
    ContributorAuthor

    Rolled up in #4482 (all 17 defects from the v17 verification, grouped by severity with a suggested RC-exit triage).

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

    @os-zhuang
    Contributor

    交接说明(进行中)

    分支: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 把一个裸预设名字符串当成比较值发了出来。

    链路:

    1. 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 天"的拼法。
    2. objectui packages/core/src/utils/dashboard-filters.ts 的 resolveDashboardFilterDefs:内置的 dateRange 声明会把预设名字符串归一化成 { preset }(defaultValue: preset && preset !== 'custom' ? { preset } : undefined),但 globalFilters 分支是原样透传 f.defaultValue。这个不对称就是 bug。
    3. 于是 buildFilterCondition 里 typeof v === 'object' 为 false,走到 "A bare string date means equality on that day" 那一支,原样返回 'last_7_days' → WHERE created_at = 'last_7_days' → 恒 0。
    4. 同一个不对称也解释了为什么选择器显示"全部时间":DateRangeFilter 的 selectValue = value?.preset ?? (value?.from || value?.to ? CUSTOM : ALL),裸字符串上这三个属性都是 undefined,所以 UI 落到 ALL_VALUE 显示"全部时间",而底下发出去的却是那个字符串。"显示全部时间"和"每块 KPI 读 0"是同一个根因的两个症状,不是两件事。

    当前卡在哪

    objectui worktree(/home/user/objectui-analytics)已建好、依赖已装完,但修复代码还没写、没提交。风险提示:这部分工作目前只存在于本次会话的分析里,远端没有。

    下一步具体该做什么

    1. 在 objectui 的 packages/core/src/utils/dashboard-filters.ts 里,给 resolveDashboardFilterDefs 的 globalFilters 分支补上与内置 dateRange 同一套归一化:type 为 date/dateRange 且 defaultValue 是已知预设名(PRESET_RANGES 的 key)时,转成 { preset }。这不是"消费端加 ?? 兜底"——它是同一个归一化函数对兄弟声明补齐同一个转换,而且 spec 只允许标量,producer 侧没有别的写法可改。
    2. 回归测试:dashboard-filters 的单测里断言 defaultValue:'last_7_days' 归一化为 {preset:'last_7_days'},且 buildFilterCondition 产出 {$gte:'{7_days_ago}', $lte:'{today}'} 而不是裸字符串;再补一条 DashboardFilterBar 显示 "Last 7 days" 而非 "All time"。
    3. 闸门:objectui 侧 pnpm --filter @object-ui/core test + pnpm typecheck。
    4. framework 侧不需要改 system_overview.dashboard.ts——那份 metadata 是合规的,改它只会把 bug 藏起来。

    残留问题(建议另开 issue,不在本次范围):date 型 filter 拿到一个既不是已知预设、也不是合法 ISO 日期的字符串时,仍会静默降级成一个永不命中的等值比较,继续产生"看着健康的 0"。预设名这一类由上面的修复覆盖,但拼错的自定义值不会。


    Generated by Claude Code

  5. os-zhuang commented on Aug 1, 2026

    @os-zhuang
    Contributor

    已修复并合并: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

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

Metadata

Metadata

Assignees

Labels

No labels
No labels

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions