feat: add analytics date dimension bindings
Some checks failed
CI / verify (push) Has been cancelled
Some checks failed
CI / verify (push) Has been cancelled
- lock profile task tracking scope to user - add analytics date dimension module support and tests - regenerate SpacetimeDB Rust bindings with private APIs
This commit is contained in:
@@ -0,0 +1,148 @@
|
||||
use module_runtime::{
|
||||
AnalyticsDateDimensionSnapshot, RuntimeProfileFieldError,
|
||||
build_analytics_date_dimension_from_date_key, parse_analytics_calendar_date_key,
|
||||
validate_analytics_date_dimension_date_key,
|
||||
};
|
||||
|
||||
fn dimension(calendar_date: &str) -> AnalyticsDateDimensionSnapshot {
|
||||
let date_key = parse_analytics_calendar_date_key(calendar_date).expect("日期应可解析");
|
||||
build_analytics_date_dimension_from_date_key(date_key)
|
||||
}
|
||||
|
||||
fn date_key(calendar_date: &str) -> i64 {
|
||||
parse_analytics_calendar_date_key(calendar_date).expect("日期应可解析")
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn analytics_date_dimension_handles_leap_day() {
|
||||
// 中文注释:2024 是闰年,2 月应包含 29 日且属于 ISO 第 9 周。
|
||||
let snapshot = dimension("2024-02-29");
|
||||
|
||||
assert_eq!(snapshot.calendar_date, "2024-02-29");
|
||||
assert_eq!(snapshot.weekday, 4);
|
||||
assert_eq!(snapshot.iso_week_key, 202409);
|
||||
assert_eq!(snapshot.week_start_date_key, date_key("2024-02-26"));
|
||||
assert_eq!(snapshot.week_end_date_key, date_key("2024-03-03"));
|
||||
assert_eq!(snapshot.month_key, 202402);
|
||||
assert_eq!(snapshot.month_start_date_key, date_key("2024-02-01"));
|
||||
assert_eq!(snapshot.month_end_date_key, date_key("2024-02-29"));
|
||||
assert_eq!(snapshot.quarter_key, 20241);
|
||||
assert_eq!(snapshot.year_key, 2024);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn analytics_date_dimension_uses_iso_week_year_across_year_boundary() {
|
||||
// 中文注释:2025-12-29 是周一,但 ISO week-year 已经进入 2026-W01。
|
||||
let snapshot = dimension("2025-12-29");
|
||||
|
||||
assert_eq!(snapshot.calendar_date, "2025-12-29");
|
||||
assert_eq!(snapshot.weekday, 1);
|
||||
assert_eq!(snapshot.iso_week_key, 202601);
|
||||
assert_eq!(snapshot.week_start_date_key, date_key("2025-12-29"));
|
||||
assert_eq!(snapshot.week_end_date_key, date_key("2026-01-04"));
|
||||
assert_eq!(snapshot.month_key, 202512);
|
||||
assert_eq!(snapshot.quarter_key, 20254);
|
||||
assert_eq!(snapshot.year_key, 2025);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn analytics_date_dimension_handles_new_year_inside_iso_week() {
|
||||
// 中文注释:2026-01-01 仍落在 2026-W01,周范围跨自然年。
|
||||
let snapshot = dimension("2026-01-01");
|
||||
|
||||
assert_eq!(snapshot.calendar_date, "2026-01-01");
|
||||
assert_eq!(snapshot.weekday, 4);
|
||||
assert_eq!(snapshot.iso_week_key, 202601);
|
||||
assert_eq!(snapshot.week_start_date_key, date_key("2025-12-29"));
|
||||
assert_eq!(snapshot.week_end_date_key, date_key("2026-01-04"));
|
||||
assert_eq!(snapshot.month_key, 202601);
|
||||
assert_eq!(snapshot.month_start_date_key, date_key("2026-01-01"));
|
||||
assert_eq!(snapshot.month_end_date_key, date_key("2026-01-31"));
|
||||
assert_eq!(snapshot.quarter_key, 20261);
|
||||
assert_eq!(snapshot.year_key, 2026);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn analytics_date_dimension_handles_q1_end() {
|
||||
// 中文注释:Q1 结束日应映射到 2026Q1,季度边界为 1 月 1 日到 3 月 31 日。
|
||||
let snapshot = dimension("2026-03-31");
|
||||
|
||||
assert_eq!(snapshot.calendar_date, "2026-03-31");
|
||||
assert_eq!(snapshot.weekday, 2);
|
||||
assert_eq!(snapshot.iso_week_key, 202614);
|
||||
assert_eq!(snapshot.month_key, 202603);
|
||||
assert_eq!(snapshot.month_start_date_key, date_key("2026-03-01"));
|
||||
assert_eq!(snapshot.month_end_date_key, date_key("2026-03-31"));
|
||||
assert_eq!(snapshot.quarter_key, 20261);
|
||||
assert_eq!(snapshot.quarter_start_date_key, date_key("2026-01-01"));
|
||||
assert_eq!(snapshot.quarter_end_date_key, date_key("2026-03-31"));
|
||||
assert_eq!(snapshot.year_key, 2026);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn analytics_date_dimension_handles_q2_start() {
|
||||
// 中文注释:Q2 开始日应映射到 2026Q2,季度边界为 4 月 1 日到 6 月 30 日。
|
||||
let snapshot = dimension("2026-04-01");
|
||||
|
||||
assert_eq!(snapshot.calendar_date, "2026-04-01");
|
||||
assert_eq!(snapshot.weekday, 3);
|
||||
assert_eq!(snapshot.iso_week_key, 202614);
|
||||
assert_eq!(snapshot.month_key, 202604);
|
||||
assert_eq!(snapshot.month_start_date_key, date_key("2026-04-01"));
|
||||
assert_eq!(snapshot.month_end_date_key, date_key("2026-04-30"));
|
||||
assert_eq!(snapshot.quarter_key, 20262);
|
||||
assert_eq!(snapshot.quarter_start_date_key, date_key("2026-04-01"));
|
||||
assert_eq!(snapshot.quarter_end_date_key, date_key("2026-06-30"));
|
||||
assert_eq!(snapshot.year_key, 2026);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn analytics_date_dimension_handles_year_end() {
|
||||
// 中文注释:2026 年末属于 2026-W53,且所有自然年边界应保持在 2026 年内。
|
||||
let snapshot = dimension("2026-12-31");
|
||||
|
||||
assert_eq!(snapshot.calendar_date, "2026-12-31");
|
||||
assert_eq!(snapshot.weekday, 4);
|
||||
assert_eq!(snapshot.iso_week_key, 202653);
|
||||
assert_eq!(snapshot.week_start_date_key, date_key("2026-12-28"));
|
||||
assert_eq!(snapshot.week_end_date_key, date_key("2027-01-03"));
|
||||
assert_eq!(snapshot.month_key, 202612);
|
||||
assert_eq!(snapshot.month_start_date_key, date_key("2026-12-01"));
|
||||
assert_eq!(snapshot.month_end_date_key, date_key("2026-12-31"));
|
||||
assert_eq!(snapshot.quarter_key, 20264);
|
||||
assert_eq!(snapshot.quarter_start_date_key, date_key("2026-10-01"));
|
||||
assert_eq!(snapshot.quarter_end_date_key, date_key("2026-12-31"));
|
||||
assert_eq!(snapshot.year_key, 2026);
|
||||
assert_eq!(snapshot.year_start_date_key, date_key("2026-01-01"));
|
||||
assert_eq!(snapshot.year_end_date_key, date_key("2026-12-31"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn analytics_date_key_parser_rejects_invalid_calendar_dates() {
|
||||
// 中文注释:非法日期和非 YYYY-MM-DD 字符串都必须解析失败,避免写入脏维表。
|
||||
assert_eq!(
|
||||
parse_analytics_calendar_date_key("2026-02-30"),
|
||||
Err(RuntimeProfileFieldError::InvalidAnalyticsCalendarDate)
|
||||
);
|
||||
assert_eq!(
|
||||
parse_analytics_calendar_date_key("bad"),
|
||||
Err(RuntimeProfileFieldError::InvalidAnalyticsCalendarDate)
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn analytics_date_dimension_rejects_out_of_supported_range() {
|
||||
// 中文注释:维表只允许受控业务日期范围,避免裸 date_key 极值进入 reducer 日历算法。
|
||||
assert_eq!(
|
||||
parse_analytics_calendar_date_key("1999-12-31"),
|
||||
Err(RuntimeProfileFieldError::InvalidAnalyticsCalendarDate)
|
||||
);
|
||||
assert_eq!(
|
||||
parse_analytics_calendar_date_key("2101-01-01"),
|
||||
Err(RuntimeProfileFieldError::InvalidAnalyticsCalendarDate)
|
||||
);
|
||||
assert_eq!(
|
||||
validate_analytics_date_dimension_date_key(i64::MAX),
|
||||
Err(RuntimeProfileFieldError::InvalidAnalyticsCalendarDate)
|
||||
);
|
||||
}
|
||||
83
server-rs/crates/module-runtime/tests/profile_task_scope.rs
Normal file
83
server-rs/crates/module-runtime/tests/profile_task_scope.rs
Normal file
@@ -0,0 +1,83 @@
|
||||
use module_runtime::{
|
||||
RuntimeProfileFieldError, RuntimeProfileTaskCycle, RuntimeTrackingScopeKind,
|
||||
build_runtime_profile_task_config_admin_upsert_input, build_runtime_tracking_daily_stat_id,
|
||||
};
|
||||
|
||||
fn build_task_scope_input(
|
||||
scope_kind: RuntimeTrackingScopeKind,
|
||||
) -> Result<module_runtime::RuntimeProfileTaskConfigAdminUpsertInput, RuntimeProfileFieldError> {
|
||||
build_runtime_profile_task_config_admin_upsert_input(
|
||||
"admin-1".to_string(),
|
||||
"daily-login".to_string(),
|
||||
"每日登录".to_string(),
|
||||
"".to_string(),
|
||||
"daily_login".to_string(),
|
||||
RuntimeProfileTaskCycle::Daily,
|
||||
scope_kind,
|
||||
1,
|
||||
10,
|
||||
true,
|
||||
10,
|
||||
1_000,
|
||||
)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn admin_upsert_build_input_accepts_user_scope() {
|
||||
let input = build_task_scope_input(RuntimeTrackingScopeKind::User)
|
||||
.expect("个人任务 user scope 应允许保存");
|
||||
|
||||
assert_eq!(input.scope_kind, RuntimeTrackingScopeKind::User);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn admin_upsert_build_input_rejects_non_user_scope_with_clear_message() {
|
||||
for scope_kind in [
|
||||
RuntimeTrackingScopeKind::Site,
|
||||
RuntimeTrackingScopeKind::Module,
|
||||
RuntimeTrackingScopeKind::Work,
|
||||
] {
|
||||
let error = build_task_scope_input(scope_kind).expect_err("非 user scope 应拒绝保存");
|
||||
|
||||
assert_eq!(
|
||||
error,
|
||||
RuntimeProfileFieldError::UnsupportedProfileTaskScopeKind
|
||||
);
|
||||
assert!(
|
||||
error.to_string().contains("仅支持 user"),
|
||||
"错误信息应明确说明个人任务仅支持 user,实际为:{}",
|
||||
error
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn tracking_daily_stat_id_keeps_work_scope_separate_from_user_scope() {
|
||||
let user_id = "user-1";
|
||||
let work_id = "work-1";
|
||||
let day_key = 20_000;
|
||||
|
||||
let user_stat_id = build_runtime_tracking_daily_stat_id(
|
||||
"daily_login",
|
||||
RuntimeTrackingScopeKind::User,
|
||||
user_id,
|
||||
day_key,
|
||||
);
|
||||
let work_stat_id = build_runtime_tracking_daily_stat_id(
|
||||
"daily_login",
|
||||
RuntimeTrackingScopeKind::Work,
|
||||
work_id,
|
||||
day_key,
|
||||
);
|
||||
let invalid_work_with_user_id_stat_id = build_runtime_tracking_daily_stat_id(
|
||||
"daily_login",
|
||||
RuntimeTrackingScopeKind::Work,
|
||||
user_id,
|
||||
day_key,
|
||||
);
|
||||
|
||||
// 中文注释:Work 维度必须保留独立 scope_kind,不允许被静默当作 user_id 查询用户桶。
|
||||
assert!(user_stat_id.contains(":user:user-1:"));
|
||||
assert!(work_stat_id.contains(":work:work-1:"));
|
||||
assert_ne!(user_stat_id, invalid_work_with_user_id_stat_id);
|
||||
}
|
||||
Reference in New Issue
Block a user