Files
Genarrative/.hermes/plans/2026-05-04_022223-analytics-time-dimension-mapping.md
kdletters 5c7c039e52
Some checks failed
CI / verify (push) Has been cancelled
feat: add analytics date dimension bindings
- lock profile task tracking scope to user

- add analytics date dimension module support and tests

- regenerate SpacetimeDB Rust bindings with private APIs
2026-05-04 13:54:41 +08:00

21 KiB
Raw Blame History

埋点系统新增周、月、季、年维度映射表计划

目标

在 Genarrative 的埋点/统计系统中新增“周、月、季、年”维度映射表,让后续统计查询可以按不同时间粒度稳定聚合,而不是只依赖运行时临时计算日期范围。

本计划只做设计与落地步骤,不直接修改业务代码。

当前上下文与初步发现

  1. 当前仓库根目录为 /home/dsk/workspace/Genarrative
  2. 远端更新后已定位到当前真实埋点/任务系统:
    • 原始埋点表:tracking_event
    • 日聚合投影表:tracking_daily_stat
    • 任务配置表:profile_task_config
    • 任务进度表:profile_task_progress
    • 领奖记录表:profile_task_reward_claim
  3. 相关文件:
    • server-rs/crates/spacetime-module/src/runtime/profile.rs
    • server-rs/crates/module-runtime/src/domain.rs
    • server-rs/crates/module-runtime/src/application.rs
    • server-rs/crates/api-server/src/runtime_profile.rs
    • apps/admin-web/src/pages/AdminTaskConfigPage.tsx
    • apps/admin-web/src/api/adminApiTypes.ts
    • apps/admin-web/src/config/trackingEventDefinitions.ts
    • docs/technical/PROFILE_TASK_AND_TRACKING_SYSTEM_2026-05-03.md
    • docs/tracking/TRACKING_QUERY_PLAYBOOK_2026-05-03.md
  4. 当前 tracking_event 是明细表,tracking_daily_stat 是统一日汇总表,不按范围拆表;它通过 event_key + scope_kind + scope_id + day_key 区分不同聚合桶。
  5. 当前任务系统是“个人任务系统”,首版任务配置均面向用户维度;后台暴露“埋点范围”选择会导致运营误配。
  6. 已决定采用任务配置方案 B
    • 后台任务配置不再让运营手动选择埋点范围。
    • 后端个人任务配置统一限制为 RuntimeTrackingScopeKind::User
    • 若 API 收到非 userscopeKind,应拒绝或兼容忽略但最终落库必须为 User;推荐直接拒绝并返回清晰错误。
  7. 已发现一个需要纳入本计划修复的语义问题:profile_task_tracking_scope_idRuntimeTrackingScopeKind::Work 当前返回 user_id,这不符合 work 维度语义。即使个人任务暂不支持 work也应避免错误映射继续存在。
  8. 当前日期桶使用北京时间自然日:day_key = floor((occurred_at_micros + 8h) / 1d);周/月/季/年映射表应优先沿用这一业务日口径,除非产品另行确认。
  9. 如果新增正式后端表,需要同步:
    • 表定义
    • reducer/procedure
    • migration.rs
    • 生成绑定
    • spacetime-client facade
    • shared-contracts / API DTO如有接口暴露

关键假设

在未定位现有埋点模块前,先按以下假设规划:

  1. 当前已有某种事件明细表或统计事实表,例如:
    • telemetry_event
    • analytics_event
    • metric_event
    • narrative_telemetry
    • 或类似命名
  2. 新增的“映射表”用于把具体日期或事件时间映射到时间维度 bucket。
  3. 映射维度包括:
    • week
    • month
    • quarter
    • year
  4. 已明确选择“一张通用日期维度映射表”方案:analytics_date_dimension
  5. 统计口径需要明确:
    • 周从周一还是周日开始
    • 是否使用 ISO week
    • 季度是自然季度还是财务季度
    • 时区使用 UTC 还是业务本地时区

推荐设计方向

方案 A单一时间维度映射表推荐

新增一张日历维度表,每一行对应一个自然日,并包含它归属的周、月、季、年。

表概念:

analytics_date_dimension

建议字段:

date_key               string   例如 2026-05-04
calendar_date          string   真实日期,按 YYYY-MM-DD 存储
weekday                u8       1-7 或 0-6需要统一约定
iso_week_key           string   例如 2026-W19
week_start_date_key    string   例如 2026-05-04
week_end_date_key      string   例如 2026-05-10
month_key              string   例如 2026-05
month_start_date_key   string
month_end_date_key     string
quarter_key            string   例如 2026-Q2
year_key               string   例如 2026
created_at             timestamp/string
updated_at             timestamp/string

优点:

  • 一张表即可支持日、周、月、季、年映射。
  • 便于后续新增半月、财年、节假日、自然周等维度。
  • 查询逻辑简单:事件日期 join/date_key 映射到目标粒度。
  • 数据量很小,按 20 年也只有约 7300 行。

缺点:

  • 需要在事件时间写入或统计查询时把 timestamp 归一为 date_key。
  • 如果要支持多时区,可能需要增加 timezone 字段或多套 calendar。

方案 B四张独立映射表

分别新增:

analytics_week_dimension
analytics_month_dimension
analytics_quarter_dimension
analytics_year_dimension

优点:

  • 每个粒度表结构更纯粹。
  • 查询时可以直接针对目标粒度表。

缺点:

  • 表更多,维护复杂。
  • 日期归属关系仍然需要额外处理。
  • 容易出现周/月/季/年口径漂移。

最终选择

本计划采用方案 A单一 analytics_date_dimension 日期维表,而不是四张独立映射表。

如业务未来明确要求“周、月、季、年各自有独立映射表”,也应优先在日期维表基础上派生视图或物化派生表,而不是一开始拆成四张重复表。

后端设计建议

1. 明确埋点领域归属

先定位现有埋点模块。如果没有独立模块,建议新增或归入:

server-rs/crates/module-analytics/

或如果当前项目已有 telemetry 命名,则保持已有命名,例如:

server-rs/crates/module-telemetry/

领域层职责:

  • 时间粒度定义
  • date_key/week_key/month_key/quarter_key/year_key 生成规则
  • 时间维度校验
  • 事件聚合查询输入的纯规则

不应包含:

  • SpacetimeDB 表读写
  • Axum handler
  • HTTP response

2. SpacetimeDB 表设计

spacetime-module 中新增时间维度表。

建议表名:

analytics_date_dimension

建议主键:

date_key

建议索引:

iso_week_key
month_key
quarter_key
year_key

如果 SpacetimeDB 表定义已有统一命名规范,应按现有规范命名。

3. 初始化/补全 reducer

新增 reducer 或内部 procedure用于生成指定日期范围内的维度数据。

建议能力:

seed_analytics_date_dimensions(start_date, end_date)
ensure_analytics_date_dimension_for_date(date_key)
ensure_analytics_date_dimensions_for_range(start_date, end_date)

设计原则:

  • 可幂等执行。
  • 已存在 date_key 时不重复插入。
  • 支持一次补一段日期。
  • 避免一次补太大范围导致事务过重。
  • 生产环境建议按年份或月份分批。

4. 事件表和映射表关系

如果事件表目前只有 timestamp建议新增或计算出

event_date_key

可选策略:

  1. 写入事件时同步写 event_date_key
  2. 查询统计时从 timestamp 临时计算 date_key。
  3. 后台迁移为历史事件补 event_date_key

推荐:

  • 新事件写入时保存 event_date_key
  • 历史事件通过批量迁移 reducer 分批补齐。

5. 聚合查询设计

支持按粒度查询时API 或 facade 可以接收:

granularity = day | week | month | quarter | year
start_date
end_date
metric/event_name
filters

内部根据粒度选择 bucket key

day      -> date_key
week     -> iso_week_key 或 week_key
month    -> month_key
quarter  -> quarter_key
year     -> year_key

返回结构建议统一:

bucket_key
bucket_label
bucket_start_date
bucket_end_date
value

可能涉及的文件

由于当前尚未定位明确埋点模块,以下是预计文件范围。

必查文件/目录

./Genarrative/server-rs/crates/
./Genarrative/server-rs/crates/spacetime-module/src/
./Genarrative/server-rs/crates/spacetime-client/src/
./Genarrative/server-rs/crates/shared-contracts/src/
./Genarrative/server-rs/crates/api-server/src/
./Genarrative/packages/shared/src/contracts/
./Genarrative/src/services/

可能新增文件

如果采用 analytics 命名:

server-rs/crates/module-analytics/src/domain.rs
server-rs/crates/module-analytics/src/commands.rs
server-rs/crates/module-analytics/src/application.rs
server-rs/crates/module-analytics/src/errors.rs
server-rs/crates/module-analytics/src/events.rs
server-rs/crates/shared-contracts/src/analytics.rs
server-rs/crates/spacetime-client/src/analytics.rs
server-rs/crates/api-server/src/analytics.rs
packages/shared/src/contracts/analytics.ts

如果只是新增 SpacetimeDB 映射表且暂不暴露 API则可能只需

server-rs/crates/spacetime-module/src/**
server-rs/crates/spacetime-module/src/migration.rs
server-rs/crates/spacetime-client/src/**   # 如果查询会被 api-server 使用

详细实施步骤

Step 1复核现有埋点系统与任务配置链路

当前已定位真实链路,实施前再做一次只读复核,确认远端最新代码没有继续变化。

已知核心表:

tracking_event              # 原始埋点明细
tracking_daily_stat         # 日聚合投影
profile_task_config         # 个人任务配置
profile_task_progress       # 个人任务进度
profile_task_reward_claim   # 领奖记录

已知核心文件:

server-rs/crates/spacetime-module/src/runtime/profile.rs
server-rs/crates/module-runtime/src/domain.rs
server-rs/crates/module-runtime/src/application.rs
server-rs/crates/api-server/src/runtime_profile.rs
apps/admin-web/src/pages/AdminTaskConfigPage.tsx
apps/admin-web/src/api/adminApiTypes.ts
apps/admin-web/src/config/trackingEventDefinitions.ts
docs/technical/PROFILE_TASK_AND_TRACKING_SYSTEM_2026-05-03.md
docs/tracking/TRACKING_QUERY_PLAYBOOK_2026-05-03.md

重点确认:

  1. tracking_event 是否仍包含 event_key/scope_kind/scope_id/day_key/user_id/occurred_at
  2. tracking_daily_stat 是否仍按 event_key + scope_kind + scope_id + day_key 生成 stat_id
  3. profile_task_config 是否仍包含 scope_kindsort_order
  4. 后台 AdminTaskConfigPage 是否仍暴露“埋点范围”下拉。
  5. profile_task_tracking_scope_idWork => user_id 的错误映射是否仍存在。

Step 1.5:先收紧个人任务配置的埋点范围,采用方案 B

在做周/月/季/年维度映射前,先修正个人任务配置边界,避免后续在错误配置模型上继续扩展。

目标行为:

个人任务配置只支持用户维度埋点。
后台页面不再展示“埋点范围”。
后端不允许 profile_task_config 被写入 site/work/module 维度。

建议实现:

  1. 前端隐藏 AdminTaskConfigPage 的“埋点范围”选择。
    • 文件:apps/admin-web/src/pages/AdminTaskConfigPage.tsx
    • 移除或隐藏:scopeKinds 下拉 UI。
    • 保存请求仍可兼容传 scopeKind: 'user',避免一次性改动 API contract。
  2. 后端 upsert 校验 scopeKind 必须为 RuntimeTrackingScopeKind::User
    • 文件:server-rs/crates/api-server/src/runtime_profile.rs
    • 或更底层:server-rs/crates/module-runtime/src/domain.rs / server-rs/crates/spacetime-module/src/runtime/profile.rs 的输入构造函数。
    • 推荐在领域输入构造处兜底校验API 层返回清晰错误。
  3. 若暂不改 API DTO则保持字段存在但限定值只能是 user
    • 文件:apps/admin-web/src/api/adminApiTypes.ts
    • AdminUpsertProfileTaskConfigRequest.scopeKind 可保留,前端固定传 user
  4. 更新后台埋点定义注册表的语义:
    • 文件:apps/admin-web/src/config/trackingEventDefinitions.ts
    • 当前每个 event definition 包含 scopeKind,如果个人任务统一 user,可以保留为只读内部默认值;但不要让运营在页面改。
  5. 更新技术文档:
    • docs/technical/PROFILE_TASK_AND_TRACKING_SYSTEM_2026-05-03.md
    • 明确“个人任务首版只支持用户维度埋点,后台不开放 scope_kind 配置”。

验收:

后台任务配置页不再出现“埋点范围”选择。
保存 daily_login 后落库 scope_kind 仍为 User。
直接调用后台 upsert 接口传 site/work/module 时被拒绝,或最终不会落库为非 User推荐拒绝。

Step 1.6:修复 Work 范围错误返回 user_id 的语义问题

当前函数:

server-rs/crates/spacetime-module/src/runtime/profile.rs
profile_task_tracking_scope_id(user_id, config)

当前问题:

RuntimeTrackingScopeKind::Work => user_id.to_string()

这会把 work 维度错误映射为用户 ID。虽然个人任务将限制为 User但保留这个分支会误导后续扩展。

推荐修复策略:

  1. 对个人任务进度计算来说,Work 不应进入该函数。
  2. profile_task_tracking_scope_id 改为返回 Result<String, String>Option<String>
  3. 对不支持的范围返回错误,而不是伪造 scope_id
Site   -> "site",如果个人任务仍不允许 site则上游先拒绝
Module -> "profile",如果个人任务仍不允许 module则上游先拒绝
User   -> user_id
Work   -> error: personal task progress does not support work scope without work_id

更严格的推荐:

个人任务链路只接受 User。
Work/Site/Module 在 profile_task_progress_count 前就被拒绝。
profile_task_tracking_scope_id 只保留 User 分支,或者非 User 返回错误。

需要同步调整调用点:

profile_task_progress_count
refresh_profile_task_progress
build_profile_task_center_snapshot
claim_profile_task_reward

避免因为函数返回 Result 后调用链未处理错误。

验收:

不存在 Work => user_id 的映射。
个人任务配置非 User 时不会静默算出错误进度。
相关测试覆盖User 正常Work/Site/Module 被拒绝。

Step 2确定时间口径

必须先确认:

  1. 周维度是否使用 ISO week。
  2. 周开始日是周一还是周日。
  3. 月/季/年是否按自然日历。
  4. 统计时区是 UTC、服务器时区还是用户本地时区。
  5. 跨年周如何命名,例如 2025-W01 可能开始于 2024 年末。

推荐默认:

时区UTC除非产品明确要求中国时区
周ISO week周一开始
月:自然月
季:自然季度
年:自然年

如果业务面向国内用户,建议考虑:

时区Asia/Shanghai
周:周一开始

Step 3设计 date dimension 表

设计字段和 key 格式,写入技术文档。

建议 key 格式:

date_key: 2026-05-04
week_key: 2026-W19
month_key: 2026-05
quarter_key: 2026-Q2
year_key: 2026

注意:

  • week_key 建议使用 ISO week-year不一定等于 calendar year。
  • quarter_key 使用 calendar year。

Step 4新增领域纯函数

在领域层或 shared-kernel 中实现纯函数:

resolve_date_dimension(date, timezone) -> AnalyticsDateDimension
resolve_bucket_key(date_dimension, granularity) -> String
resolve_bucket_range(bucket_key, granularity) -> start/end date

要求:

  • 有单元测试。
  • 覆盖跨年周。
  • 覆盖闰年 2 月。
  • 覆盖季度边界。

Step 5新增 SpacetimeDB 表

spacetime-module 中新增表。

遵守约束:

  • 新增表通常安全。
  • 不修改已有表字段。
  • 如果必须给已有事件表加 event_date_key,必须加在表定义末尾并提供 default。
  • 若要补历史数据,使用 reducer 分批迁移。

Step 6新增 seed/ensure reducer

新增幂等 reducer

seed_analytics_date_dimensions(start_date, end_date)
ensure_analytics_date_dimension_for_date(date_key)

验证点:

  • 重复执行不会重复插入。
  • 日期范围非法时返回稳定错误。
  • 单次范围过大时拒绝或分页。

Step 7接入事件写入链路

如果现有事件写入链路存在,新增:

event_date_key

策略:

  • 新事件写入时同步计算并保存。
  • 写入前确保对应 date dimension 存在。
  • 历史事件通过迁移 reducer 补齐。

如果暂不改事件表,也可以在查询阶段临时映射,但性能和一致性较差。

Step 8接入聚合查询

如已有统计接口,扩展请求参数:

granularity: day | week | month | quarter | year

查询逻辑改为:

事件/事实表
→ event_date_key
→ analytics_date_dimension
→ 取对应 bucket key
→ group by bucket key

返回 bucket 时包含:

bucket_key
bucket_start_date
bucket_end_date
value

Step 9补 shared contracts 和前端 contracts

如果有 API 暴露,需要补:

server-rs/crates/shared-contracts/src/analytics.rs
packages/shared/src/contracts/analytics.ts

建议 DTO

AnalyticsGranularity = day | week | month | quarter | year
AnalyticsBucketMetric
AnalyticsMetricQueryRequest
AnalyticsMetricQueryResponse

Step 10补测试

测试范围:

  1. 领域日期映射测试
  2. SpacetimeDB reducer 幂等测试
  3. API 查询维度测试
  4. 历史事件迁移测试,如涉及
  5. 跨边界日期测试
  6. 个人任务配置 scope 限制测试
  7. Work => user_id 错误映射回归测试

重点用例:

2024-02-29 闰年
2025-12-29 ISO week 可能属于 2026-W01
2026-01-01 跨年周
2026-03-31 Q1 结束
2026-04-01 Q2 开始
2026-12-31 年末

任务配置重点用例:

admin upsert daily_login + scopeKind=user -> 成功
admin upsert daily_login + scopeKind=site -> 失败,错误信息说明个人任务仅支持 user
admin upsert daily_login + scopeKind=module -> 失败
admin upsert daily_login + scopeKind=work -> 失败
任务中心读取 daily_login -> 按 User + 当前 user_id 查询进度
代码中不存在 Work => user_id 的静默映射

测试与验证命令

具体命令需在定位模块后确认。初步建议:

npm run typecheck
npm test

后端如涉及 Rust

cargo test -p module-analytics
cargo test -p spacetime-module
cargo test -p api-server

涉及 API smoke

npm run api-server

然后验证:

GET /healthz

涉及 SpacetimeDB schema

  • 需要生成绑定。
  • 需要确认 migration.rs 对齐。
  • 需要确认 publish 不触发不安全 schema 变更。

风险与权衡

风险 1个人任务 scope_kind 被误配置导致进度异常

当前个人任务系统本质上按用户维度计算进度。如果允许运营配置 site/work/module,可能导致任务进度查错 tracking_daily_stat 聚合桶,出现任务永远不可领取或错误可领取。

缓解:

采用方案 B后台隐藏埋点范围后端限制个人任务配置只能写入 User。

风险 2Work 维度缺少 work_id上游却静默用 user_id 代替

当前 profile_task_tracking_scope_idWork => user_id 是错误语义。若后续扩展作品任务,会把作品维度统计错误映射到用户维度。

缓解:

移除 Work => user_id 映射;非 User 的个人任务配置应被拒绝。未来做作品任务时新增明确 work_id 来源和任务类型。

风险 3时区口径影响统计结果

周/月/季/年映射对时区敏感。当前日桶使用北京时间自然日:floor((occurred_at_micros + 8h) / 1d)。新增映射表应明确沿用北京时间业务日,还是切换为 UTC/用户本地时区。

风险 4ISO week 跨年

ISO week-year 与自然年不同。若前端展示按自然年理解,可能产生认知差异。

风险 5修改已有事件表可能触发 SpacetimeDB 迁移限制

如果已有事件表需要新增字段:

  • 字段必须加末尾。
  • 必须提供 default。
  • 历史数据要分批迁移。

风险 5表设计过早绑定单一业务

建议用通用 date dimension而不是为某个单一埋点写死周/月/季/年表,避免后续复用困难。

待确认问题

  1. 周维度使用 ISO week 还是自然周?周一开始还是周日开始?
  2. 周/月/季/年映射是否沿用当前北京时间业务日口径?
  3. 这个映射表服务的是所有埋点,还是只服务个人任务/运营后台统计?
  4. 是否需要 API 暴露这些映射关系,还是只用于后端聚合?
  5. 是否需要回填历史事件?历史数据规模多大?
  6. 未来是否会存在非个人任务,例如整站任务、模块任务、作品任务?如果会,应另行设计任务类型和 scope_id 来源,不应复用当前个人任务配置页直接开放 scope。

建议结论

优先采用“一张通用日期维度映射表”的设计:

analytics_date_dimension

通过字段同时提供:

day / week / month / quarter / year

后续统计按 granularity 选择 bucket key 聚合。这样比直接新增四张独立映射表更稳定、更容易复用,也更容易处理跨年周、季度边界和历史回填。