- lock profile task tracking scope to user - add analytics date dimension module support and tests - regenerate SpacetimeDB Rust bindings with private APIs
21 KiB
埋点系统新增周、月、季、年维度映射表计划
目标
在 Genarrative 的埋点/统计系统中新增“周、月、季、年”维度映射表,让后续统计查询可以按不同时间粒度稳定聚合,而不是只依赖运行时临时计算日期范围。
本计划只做设计与落地步骤,不直接修改业务代码。
当前上下文与初步发现
- 当前仓库根目录为
/home/dsk/workspace/Genarrative。 - 远端更新后已定位到当前真实埋点/任务系统:
- 原始埋点表:
tracking_event - 日聚合投影表:
tracking_daily_stat - 任务配置表:
profile_task_config - 任务进度表:
profile_task_progress - 领奖记录表:
profile_task_reward_claim
- 原始埋点表:
- 相关文件:
server-rs/crates/spacetime-module/src/runtime/profile.rsserver-rs/crates/module-runtime/src/domain.rsserver-rs/crates/module-runtime/src/application.rsserver-rs/crates/api-server/src/runtime_profile.rsapps/admin-web/src/pages/AdminTaskConfigPage.tsxapps/admin-web/src/api/adminApiTypes.tsapps/admin-web/src/config/trackingEventDefinitions.tsdocs/technical/PROFILE_TASK_AND_TRACKING_SYSTEM_2026-05-03.mddocs/tracking/TRACKING_QUERY_PLAYBOOK_2026-05-03.md
- 当前
tracking_event是明细表,tracking_daily_stat是统一日汇总表,不按范围拆表;它通过event_key + scope_kind + scope_id + day_key区分不同聚合桶。 - 当前任务系统是“个人任务系统”,首版任务配置均面向用户维度;后台暴露“埋点范围”选择会导致运营误配。
- 已决定采用任务配置方案 B:
- 后台任务配置不再让运营手动选择埋点范围。
- 后端个人任务配置统一限制为
RuntimeTrackingScopeKind::User。 - 若 API 收到非
user的scopeKind,应拒绝或兼容忽略但最终落库必须为User;推荐直接拒绝并返回清晰错误。
- 已发现一个需要纳入本计划修复的语义问题:
profile_task_tracking_scope_id里RuntimeTrackingScopeKind::Work当前返回user_id,这不符合 work 维度语义。即使个人任务暂不支持 work,也应避免错误映射继续存在。 - 当前日期桶使用北京时间自然日:
day_key = floor((occurred_at_micros + 8h) / 1d);周/月/季/年映射表应优先沿用这一业务日口径,除非产品另行确认。 - 如果新增正式后端表,需要同步:
- 表定义
- reducer/procedure
- migration.rs
- 生成绑定
- spacetime-client facade
- shared-contracts / API DTO,如有接口暴露
关键假设
在未定位现有埋点模块前,先按以下假设规划:
- 当前已有某种事件明细表或统计事实表,例如:
telemetry_eventanalytics_eventmetric_eventnarrative_telemetry- 或类似命名
- 新增的“映射表”用于把具体日期或事件时间映射到时间维度 bucket。
- 映射维度包括:
- 周:week
- 月:month
- 季:quarter
- 年:year
- 已明确选择“一张通用日期维度映射表”方案:
analytics_date_dimension。 - 统计口径需要明确:
- 周从周一还是周日开始
- 是否使用 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
可选策略:
- 写入事件时同步写
event_date_key。 - 查询统计时从 timestamp 临时计算 date_key。
- 后台迁移为历史事件补
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
重点确认:
tracking_event是否仍包含event_key/scope_kind/scope_id/day_key/user_id/occurred_at。tracking_daily_stat是否仍按event_key + scope_kind + scope_id + day_key生成stat_id。profile_task_config是否仍包含scope_kind和sort_order。- 后台
AdminTaskConfigPage是否仍暴露“埋点范围”下拉。 profile_task_tracking_scope_id中Work => user_id的错误映射是否仍存在。
Step 1.5:先收紧个人任务配置的埋点范围,采用方案 B
在做周/月/季/年维度映射前,先修正个人任务配置边界,避免后续在错误配置模型上继续扩展。
目标行为:
个人任务配置只支持用户维度埋点。
后台页面不再展示“埋点范围”。
后端不允许 profile_task_config 被写入 site/work/module 维度。
建议实现:
- 前端隐藏
AdminTaskConfigPage的“埋点范围”选择。- 文件:
apps/admin-web/src/pages/AdminTaskConfigPage.tsx - 移除或隐藏:
scopeKinds下拉 UI。 - 保存请求仍可兼容传
scopeKind: 'user',避免一次性改动 API contract。
- 文件:
- 后端 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 层返回清晰错误。
- 文件:
- 若暂不改 API DTO,则保持字段存在但限定值只能是
user。- 文件:
apps/admin-web/src/api/adminApiTypes.ts AdminUpsertProfileTaskConfigRequest.scopeKind可保留,前端固定传user。
- 文件:
- 更新后台埋点定义注册表的语义:
- 文件:
apps/admin-web/src/config/trackingEventDefinitions.ts - 当前每个 event definition 包含
scopeKind,如果个人任务统一user,可以保留为只读内部默认值;但不要让运营在页面改。
- 文件:
- 更新技术文档:
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,但保留这个分支会误导后续扩展。
推荐修复策略:
- 对个人任务进度计算来说,
Work不应进入该函数。 - 将
profile_task_tracking_scope_id改为返回Result<String, String>或Option<String>。 - 对不支持的范围返回错误,而不是伪造 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:确定时间口径
必须先确认:
- 周维度是否使用 ISO week。
- 周开始日是周一还是周日。
- 月/季/年是否按自然日历。
- 统计时区是 UTC、服务器时区,还是用户本地时区。
- 跨年周如何命名,例如
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:补测试
测试范围:
- 领域日期映射测试
- SpacetimeDB reducer 幂等测试
- API 查询维度测试
- 历史事件迁移测试,如涉及
- 跨边界日期测试
- 个人任务配置 scope 限制测试
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。
风险 2:Work 维度缺少 work_id,上游却静默用 user_id 代替
当前 profile_task_tracking_scope_id 中 Work => user_id 是错误语义。若后续扩展作品任务,会把作品维度统计错误映射到用户维度。
缓解:
移除 Work => user_id 映射;非 User 的个人任务配置应被拒绝。未来做作品任务时新增明确 work_id 来源和任务类型。
风险 3:时区口径影响统计结果
周/月/季/年映射对时区敏感。当前日桶使用北京时间自然日:floor((occurred_at_micros + 8h) / 1d)。新增映射表应明确沿用北京时间业务日,还是切换为 UTC/用户本地时区。
风险 4:ISO week 跨年
ISO week-year 与自然年不同。若前端展示按自然年理解,可能产生认知差异。
风险 5:修改已有事件表可能触发 SpacetimeDB 迁移限制
如果已有事件表需要新增字段:
- 字段必须加末尾。
- 必须提供 default。
- 历史数据要分批迁移。
风险 5:表设计过早绑定单一业务
建议用通用 date dimension,而不是为某个单一埋点写死周/月/季/年表,避免后续复用困难。
待确认问题
- 周维度使用 ISO week 还是自然周?周一开始还是周日开始?
- 周/月/季/年映射是否沿用当前北京时间业务日口径?
- 这个映射表服务的是所有埋点,还是只服务个人任务/运营后台统计?
- 是否需要 API 暴露这些映射关系,还是只用于后端聚合?
- 是否需要回填历史事件?历史数据规模多大?
- 未来是否会存在非个人任务,例如整站任务、模块任务、作品任务?如果会,应另行设计任务类型和
scope_id来源,不应复用当前个人任务配置页直接开放 scope。
建议结论
优先采用“一张通用日期维度映射表”的设计:
analytics_date_dimension
通过字段同时提供:
day / week / month / quarter / year
后续统计按 granularity 选择 bucket key 聚合。这样比直接新增四张独立映射表更稳定、更容易复用,也更容易处理跨年周、季度边界和历史回填。