feat: add analytics metric granularity query
Some checks failed
CI / verify (push) Has been cancelled
Some checks failed
CI / verify (push) Has been cancelled
This commit is contained in:
@@ -109,10 +109,10 @@ use crate::{
|
||||
admin_list_profile_invite_codes, admin_list_profile_redeem_codes,
|
||||
admin_list_profile_task_configs, admin_upsert_profile_invite_code,
|
||||
admin_upsert_profile_redeem_code, admin_upsert_profile_task_config,
|
||||
claim_profile_task_reward, create_profile_recharge_order, get_profile_dashboard,
|
||||
get_profile_play_stats, get_profile_recharge_center, get_profile_referral_invite_center,
|
||||
get_profile_task_center, get_profile_wallet_ledger, redeem_profile_referral_invite_code,
|
||||
redeem_profile_reward_code,
|
||||
claim_profile_task_reward, create_profile_recharge_order, get_profile_analytics_metric,
|
||||
get_profile_dashboard, get_profile_play_stats, get_profile_recharge_center,
|
||||
get_profile_referral_invite_center, get_profile_task_center, get_profile_wallet_ledger,
|
||||
redeem_profile_referral_invite_code, redeem_profile_reward_code,
|
||||
},
|
||||
runtime_save::{
|
||||
delete_runtime_snapshot, get_runtime_snapshot, list_profile_save_archives,
|
||||
@@ -1074,6 +1074,13 @@ pub fn build_router(state: AppState) -> Router {
|
||||
require_bearer_auth,
|
||||
)),
|
||||
)
|
||||
.route(
|
||||
"/api/profile/analytics/metric",
|
||||
get(get_profile_analytics_metric).route_layer(middleware::from_fn_with_state(
|
||||
state.clone(),
|
||||
require_bearer_auth,
|
||||
)),
|
||||
)
|
||||
.route(
|
||||
"/api/profile/tasks",
|
||||
get(get_profile_task_center).route_layer(middleware::from_fn_with_state(
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
use axum::{
|
||||
Json,
|
||||
extract::{Extension, Path, State},
|
||||
extract::{Extension, Path, Query, State},
|
||||
http::StatusCode,
|
||||
response::Response,
|
||||
};
|
||||
use module_runtime::{
|
||||
PROFILE_RECHARGE_PAYMENT_CHANNEL_MOCK, RuntimeProfileInviteCodeRecord,
|
||||
AnalyticsGranularity, PROFILE_RECHARGE_PAYMENT_CHANNEL_MOCK, RuntimeProfileInviteCodeRecord,
|
||||
RuntimeProfileMembershipBenefitRecord, RuntimeProfileRechargeCenterRecord,
|
||||
RuntimeProfileRechargeOrderRecord, RuntimeProfileRechargeProductRecord,
|
||||
RuntimeProfileRedeemCodeMode, RuntimeProfileRedeemCodeRecord,
|
||||
@@ -15,10 +15,12 @@ use module_runtime::{
|
||||
RuntimeReferralInviteCenterRecord, RuntimeTrackingScopeKind,
|
||||
};
|
||||
use serde_json::{Value, json};
|
||||
use serde::Deserialize;
|
||||
use shared_contracts::runtime::{
|
||||
AdminDisableProfileRedeemCodeRequest, AdminDisableProfileTaskConfigRequest,
|
||||
AdminUpsertProfileInviteCodeRequest, AdminUpsertProfileRedeemCodeRequest,
|
||||
AdminUpsertProfileTaskConfigRequest, ClaimProfileTaskRewardResponse,
|
||||
AdminUpsertProfileTaskConfigRequest, AnalyticsBucketMetricResponse,
|
||||
AnalyticsMetricQueryResponse, ClaimProfileTaskRewardResponse,
|
||||
CreateProfileRechargeOrderRequest, CreateProfileRechargeOrderResponse,
|
||||
PROFILE_TASK_CYCLE_DAILY, PROFILE_TASK_STATUS_CLAIMABLE, PROFILE_TASK_STATUS_CLAIMED,
|
||||
PROFILE_TASK_STATUS_DISABLED, PROFILE_TASK_STATUS_INCOMPLETE,
|
||||
@@ -31,6 +33,8 @@ use shared_contracts::runtime::{
|
||||
PROFILE_WALLET_LEDGER_SOURCE_TYPE_POINTS_RECHARGE,
|
||||
PROFILE_WALLET_LEDGER_SOURCE_TYPE_PUZZLE_AUTHOR_INCENTIVE_CLAIM,
|
||||
PROFILE_WALLET_LEDGER_SOURCE_TYPE_REDEEM_CODE_REWARD,
|
||||
ANALYTICS_GRANULARITY_DAY, ANALYTICS_GRANULARITY_MONTH, ANALYTICS_GRANULARITY_QUARTER,
|
||||
ANALYTICS_GRANULARITY_WEEK, ANALYTICS_GRANULARITY_YEAR,
|
||||
PROFILE_WALLET_LEDGER_SOURCE_TYPE_SNAPSHOT_SYNC, ProfileDashboardSummaryResponse,
|
||||
ProfileInviteCodeAdminListResponse, ProfileInviteCodeAdminResponse,
|
||||
ProfileMembershipBenefitResponse, ProfileMembershipResponse, ProfilePlayStatsResponse,
|
||||
@@ -278,6 +282,51 @@ pub async fn redeem_profile_reward_code(
|
||||
))
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct AnalyticsMetricQueryParams {
|
||||
pub event_key: String,
|
||||
pub scope_kind: String,
|
||||
pub scope_id: String,
|
||||
pub granularity: String,
|
||||
}
|
||||
|
||||
pub async fn get_profile_analytics_metric(
|
||||
State(state): State<AppState>,
|
||||
Extension(request_context): Extension<RequestContext>,
|
||||
Extension(_authenticated): Extension<AuthenticatedAccessToken>,
|
||||
Query(query): Query<AnalyticsMetricQueryParams>,
|
||||
) -> Result<Json<Value>, Response> {
|
||||
let scope_kind = parse_tracking_scope_kind(&query.scope_kind).map_err(|error| {
|
||||
runtime_profile_error_response(
|
||||
&request_context,
|
||||
AppError::from_status(StatusCode::BAD_REQUEST).with_message(error),
|
||||
)
|
||||
})?;
|
||||
let granularity = parse_analytics_granularity(&query.granularity).map_err(|error| {
|
||||
runtime_profile_error_response(
|
||||
&request_context,
|
||||
AppError::from_status(StatusCode::BAD_REQUEST).with_message(error),
|
||||
)
|
||||
})?;
|
||||
|
||||
let record = state
|
||||
.spacetime_client()
|
||||
.query_analytics_metric(query.event_key, scope_kind, query.scope_id, granularity)
|
||||
.await
|
||||
.map_err(|error| {
|
||||
runtime_profile_error_response(
|
||||
&request_context,
|
||||
map_runtime_profile_client_error(error),
|
||||
)
|
||||
})?;
|
||||
|
||||
Ok(json_success_body(
|
||||
Some(&request_context),
|
||||
build_analytics_metric_query_response(record),
|
||||
))
|
||||
}
|
||||
|
||||
pub async fn get_profile_task_center(
|
||||
State(state): State<AppState>,
|
||||
Extension(request_context): Extension<RequestContext>,
|
||||
@@ -811,6 +860,23 @@ fn build_profile_task_center_response(
|
||||
}
|
||||
}
|
||||
|
||||
fn build_analytics_metric_query_response(
|
||||
record: module_runtime::AnalyticsMetricQueryResponse,
|
||||
) -> AnalyticsMetricQueryResponse {
|
||||
AnalyticsMetricQueryResponse {
|
||||
buckets: record
|
||||
.buckets
|
||||
.into_iter()
|
||||
.map(|bucket| AnalyticsBucketMetricResponse {
|
||||
bucket_key: bucket.bucket_key,
|
||||
bucket_start_date_key: bucket.bucket_start_date_key,
|
||||
bucket_end_date_key: bucket.bucket_end_date_key,
|
||||
value: bucket.value,
|
||||
})
|
||||
.collect(),
|
||||
}
|
||||
}
|
||||
|
||||
fn build_profile_task_item_response(
|
||||
record: RuntimeProfileTaskItemRecord,
|
||||
) -> ProfileTaskItemResponse {
|
||||
@@ -935,6 +1001,17 @@ fn parse_tracking_scope_kind(raw: &str) -> Result<RuntimeTrackingScopeKind, Stri
|
||||
}
|
||||
}
|
||||
|
||||
fn parse_analytics_granularity(raw: &str) -> Result<AnalyticsGranularity, String> {
|
||||
match raw.trim().to_ascii_lowercase().as_str() {
|
||||
ANALYTICS_GRANULARITY_DAY => Ok(AnalyticsGranularity::Day),
|
||||
ANALYTICS_GRANULARITY_WEEK => Ok(AnalyticsGranularity::Week),
|
||||
ANALYTICS_GRANULARITY_MONTH => Ok(AnalyticsGranularity::Month),
|
||||
ANALYTICS_GRANULARITY_QUARTER => Ok(AnalyticsGranularity::Quarter),
|
||||
ANALYTICS_GRANULARITY_YEAR => Ok(AnalyticsGranularity::Year),
|
||||
_ => Err("统计粒度无效".to_string()),
|
||||
}
|
||||
}
|
||||
|
||||
fn format_profile_task_cycle(cycle: RuntimeProfileTaskCycle) -> &'static str {
|
||||
match cycle {
|
||||
RuntimeProfileTaskCycle::Daily => PROFILE_TASK_CYCLE_DAILY,
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
|
||||
use serde_json::Value;
|
||||
use shared_kernel::{offset_datetime_to_unix_micros, parse_rfc3339};
|
||||
use std::collections::BTreeMap;
|
||||
|
||||
use crate::domain::*;
|
||||
use crate::errors::RuntimeProfileFieldError;
|
||||
@@ -502,6 +503,81 @@ pub fn build_runtime_tracking_daily_stat_id(
|
||||
)
|
||||
}
|
||||
|
||||
pub fn aggregate_runtime_tracking_daily_stats(
|
||||
stats: Vec<RuntimeAnalyticsDailyStatSnapshot>,
|
||||
event_key: &str,
|
||||
scope_kind: RuntimeTrackingScopeKind,
|
||||
scope_id: &str,
|
||||
granularity: AnalyticsGranularity,
|
||||
) -> Vec<AnalyticsBucketMetric> {
|
||||
let mut buckets: BTreeMap<(String, i64, i64), u64> = BTreeMap::new();
|
||||
let event_key = event_key.trim();
|
||||
let scope_id = scope_id.trim();
|
||||
|
||||
for stat in stats {
|
||||
if stat.event_key.trim() != event_key
|
||||
|| stat.scope_kind != scope_kind
|
||||
|| stat.scope_id.trim() != scope_id
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
let dimension = build_analytics_date_dimension_from_date_key(stat.day_key);
|
||||
let (bucket_key, bucket_start_date_key, bucket_end_date_key) =
|
||||
analytics_bucket_for_dimension(&dimension, granularity);
|
||||
*buckets
|
||||
.entry((bucket_key, bucket_start_date_key, bucket_end_date_key))
|
||||
.or_insert(0) += u64::from(stat.count);
|
||||
}
|
||||
|
||||
buckets
|
||||
.into_iter()
|
||||
.map(
|
||||
|((bucket_key, bucket_start_date_key, bucket_end_date_key), value)| {
|
||||
AnalyticsBucketMetric {
|
||||
bucket_key,
|
||||
bucket_start_date_key,
|
||||
bucket_end_date_key,
|
||||
value,
|
||||
}
|
||||
},
|
||||
)
|
||||
.collect()
|
||||
}
|
||||
|
||||
fn analytics_bucket_for_dimension(
|
||||
dimension: &AnalyticsDateDimensionSnapshot,
|
||||
granularity: AnalyticsGranularity,
|
||||
) -> (String, i64, i64) {
|
||||
match granularity {
|
||||
AnalyticsGranularity::Day => (
|
||||
dimension.calendar_date.clone(),
|
||||
dimension.date_key,
|
||||
dimension.date_key,
|
||||
),
|
||||
AnalyticsGranularity::Week => (
|
||||
dimension.iso_week_key.to_string(),
|
||||
dimension.week_start_date_key,
|
||||
dimension.week_end_date_key,
|
||||
),
|
||||
AnalyticsGranularity::Month => (
|
||||
dimension.month_key.to_string(),
|
||||
dimension.month_start_date_key,
|
||||
dimension.month_end_date_key,
|
||||
),
|
||||
AnalyticsGranularity::Quarter => (
|
||||
dimension.quarter_key.to_string(),
|
||||
dimension.quarter_start_date_key,
|
||||
dimension.quarter_end_date_key,
|
||||
),
|
||||
AnalyticsGranularity::Year => (
|
||||
dimension.year_key.to_string(),
|
||||
dimension.year_start_date_key,
|
||||
dimension.year_end_date_key,
|
||||
),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn build_runtime_profile_task_config_record(
|
||||
snapshot: RuntimeProfileTaskConfigSnapshot,
|
||||
) -> RuntimeProfileTaskConfigRecord {
|
||||
|
||||
@@ -116,6 +116,24 @@ pub fn build_runtime_profile_task_center_get_input(
|
||||
Ok(RuntimeProfileTaskCenterGetInput { user_id })
|
||||
}
|
||||
|
||||
pub fn build_analytics_metric_query_input(
|
||||
event_key: String,
|
||||
scope_kind: RuntimeTrackingScopeKind,
|
||||
scope_id: String,
|
||||
granularity: AnalyticsGranularity,
|
||||
) -> Result<AnalyticsMetricQueryInput, RuntimeProfileFieldError> {
|
||||
let event_key = normalize_required_string(event_key)
|
||||
.ok_or(RuntimeProfileFieldError::MissingTaskEventKey)?;
|
||||
let scope_id = normalize_required_string(scope_id)
|
||||
.ok_or(RuntimeProfileFieldError::MissingTrackingScopeId)?;
|
||||
Ok(AnalyticsMetricQueryInput {
|
||||
event_key,
|
||||
scope_kind,
|
||||
scope_id,
|
||||
granularity,
|
||||
})
|
||||
}
|
||||
|
||||
pub fn build_runtime_profile_task_claim_input(
|
||||
user_id: String,
|
||||
task_id: String,
|
||||
|
||||
@@ -58,6 +58,78 @@ pub struct AnalyticsDateDimensionSnapshot {
|
||||
pub year_end_date_key: i64,
|
||||
}
|
||||
|
||||
#[cfg_attr(feature = "spacetime-types", derive(SpacetimeType))]
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
|
||||
pub enum AnalyticsGranularity {
|
||||
Day,
|
||||
Week,
|
||||
Month,
|
||||
Quarter,
|
||||
Year,
|
||||
}
|
||||
|
||||
impl AnalyticsGranularity {
|
||||
pub fn as_str(&self) -> &'static str {
|
||||
match self {
|
||||
Self::Day => "day",
|
||||
Self::Week => "week",
|
||||
Self::Month => "month",
|
||||
Self::Quarter => "quarter",
|
||||
Self::Year => "year",
|
||||
}
|
||||
}
|
||||
|
||||
pub fn from_client_str(value: &str) -> Option<Self> {
|
||||
match value.trim().to_ascii_lowercase().as_str() {
|
||||
"day" => Some(Self::Day),
|
||||
"week" => Some(Self::Week),
|
||||
"month" => Some(Self::Month),
|
||||
"quarter" => Some(Self::Quarter),
|
||||
"year" => Some(Self::Year),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||
pub struct RuntimeAnalyticsDailyStatSnapshot {
|
||||
pub event_key: String,
|
||||
pub scope_kind: RuntimeTrackingScopeKind,
|
||||
pub scope_id: String,
|
||||
pub day_key: i64,
|
||||
pub count: u32,
|
||||
}
|
||||
|
||||
#[cfg_attr(feature = "spacetime-types", derive(SpacetimeType))]
|
||||
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
|
||||
pub struct AnalyticsBucketMetric {
|
||||
pub bucket_key: String,
|
||||
pub bucket_start_date_key: i64,
|
||||
pub bucket_end_date_key: i64,
|
||||
pub value: u64,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||
pub struct AnalyticsMetricQueryRequest {
|
||||
pub event_key: String,
|
||||
pub scope_kind: RuntimeTrackingScopeKind,
|
||||
pub scope_id: String,
|
||||
pub granularity: AnalyticsGranularity,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||
pub struct AnalyticsMetricQueryResponse {
|
||||
pub buckets: Vec<AnalyticsBucketMetric>,
|
||||
}
|
||||
|
||||
#[cfg_attr(feature = "spacetime-types", derive(SpacetimeType))]
|
||||
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
|
||||
pub struct AnalyticsMetricQueryProcedureResult {
|
||||
pub ok: bool,
|
||||
pub buckets: Vec<AnalyticsBucketMetric>,
|
||||
pub error_message: Option<String>,
|
||||
}
|
||||
|
||||
/// 运行时平台主题。
|
||||
///
|
||||
/// 当前只冻结 light/dark 两种主题,避免各层散落字符串字面量。
|
||||
@@ -552,6 +624,15 @@ pub struct RuntimeProfileTaskCenterGetInput {
|
||||
pub user_id: String,
|
||||
}
|
||||
|
||||
#[cfg_attr(feature = "spacetime-types", derive(SpacetimeType))]
|
||||
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
|
||||
pub struct AnalyticsMetricQueryInput {
|
||||
pub event_key: String,
|
||||
pub scope_kind: RuntimeTrackingScopeKind,
|
||||
pub scope_id: String,
|
||||
pub granularity: AnalyticsGranularity,
|
||||
}
|
||||
|
||||
#[cfg_attr(feature = "spacetime-types", derive(SpacetimeType))]
|
||||
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
|
||||
pub struct RuntimeProfileTaskClaimInput {
|
||||
|
||||
105
server-rs/crates/module-runtime/tests/analytics_granularity.rs
Normal file
105
server-rs/crates/module-runtime/tests/analytics_granularity.rs
Normal file
@@ -0,0 +1,105 @@
|
||||
use module_runtime::{
|
||||
AnalyticsGranularity, RuntimeAnalyticsDailyStatSnapshot, RuntimeTrackingScopeKind,
|
||||
aggregate_runtime_tracking_daily_stats,
|
||||
};
|
||||
|
||||
fn stat(event_key: &str, scope_id: &str, day_key: i64, count: u32) -> RuntimeAnalyticsDailyStatSnapshot {
|
||||
RuntimeAnalyticsDailyStatSnapshot {
|
||||
event_key: event_key.to_string(),
|
||||
scope_kind: RuntimeTrackingScopeKind::User,
|
||||
scope_id: scope_id.to_string(),
|
||||
day_key,
|
||||
count,
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn aggregates_daily_stats_by_iso_week_bucket() {
|
||||
let buckets = aggregate_runtime_tracking_daily_stats(
|
||||
vec![
|
||||
stat("daily_login", "user-1", 20_517, 1), // 2026-03-05
|
||||
stat("daily_login", "user-1", 20_518, 2), // 2026-03-06
|
||||
stat("daily_login", "user-1", 20_524, 3), // 2026-03-12
|
||||
stat("daily_login", "user-2", 20_517, 9),
|
||||
],
|
||||
"daily_login",
|
||||
RuntimeTrackingScopeKind::User,
|
||||
"user-1",
|
||||
AnalyticsGranularity::Week,
|
||||
);
|
||||
|
||||
assert_eq!(buckets.len(), 2);
|
||||
assert_eq!(buckets[0].bucket_key, "202610");
|
||||
assert_eq!(buckets[0].bucket_start_date_key, 20_514); // 2026-03-02
|
||||
assert_eq!(buckets[0].bucket_end_date_key, 20_520); // 2026-03-08
|
||||
assert_eq!(buckets[0].value, 3);
|
||||
assert_eq!(buckets[1].bucket_key, "202611");
|
||||
assert_eq!(buckets[1].bucket_start_date_key, 20_521); // 2026-03-09
|
||||
assert_eq!(buckets[1].bucket_end_date_key, 20_527); // 2026-03-15
|
||||
assert_eq!(buckets[1].value, 3);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn aggregates_daily_stats_by_month_quarter_and_year_bucket() {
|
||||
let stats = vec![
|
||||
stat("play", "user-1", 20_545, 2), // 2026-04-02
|
||||
stat("play", "user-1", 20_573, 3), // 2026-04-30
|
||||
stat("play", "user-1", 20_574, 5), // 2026-05-01
|
||||
stat("play", "user-1", 20_818, 7), // 2026-12-31
|
||||
];
|
||||
|
||||
let month = aggregate_runtime_tracking_daily_stats(
|
||||
stats.clone(),
|
||||
"play",
|
||||
RuntimeTrackingScopeKind::User,
|
||||
"user-1",
|
||||
AnalyticsGranularity::Month,
|
||||
);
|
||||
assert_eq!(month.iter().map(|bucket| (&bucket.bucket_key, bucket.value)).collect::<Vec<_>>(), vec![(&"202604".to_string(), 5), (&"202605".to_string(), 5), (&"202612".to_string(), 7)]);
|
||||
assert_eq!(month[0].bucket_start_date_key, 20_544);
|
||||
assert_eq!(month[0].bucket_end_date_key, 20_573);
|
||||
|
||||
let quarter = aggregate_runtime_tracking_daily_stats(
|
||||
stats.clone(),
|
||||
"play",
|
||||
RuntimeTrackingScopeKind::User,
|
||||
"user-1",
|
||||
AnalyticsGranularity::Quarter,
|
||||
);
|
||||
assert_eq!(quarter.iter().map(|bucket| (&bucket.bucket_key, bucket.value)).collect::<Vec<_>>(), vec![(&"20262".to_string(), 10), (&"20264".to_string(), 7)]);
|
||||
|
||||
let year = aggregate_runtime_tracking_daily_stats(
|
||||
stats,
|
||||
"play",
|
||||
RuntimeTrackingScopeKind::User,
|
||||
"user-1",
|
||||
AnalyticsGranularity::Year,
|
||||
);
|
||||
assert_eq!(year.len(), 1);
|
||||
assert_eq!(year[0].bucket_key, "2026");
|
||||
assert_eq!(year[0].value, 17);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn day_granularity_keeps_each_day_bucket_and_filters_scope() {
|
||||
let buckets = aggregate_runtime_tracking_daily_stats(
|
||||
vec![
|
||||
stat("daily_login", "user-1", 20_517, 1),
|
||||
stat("daily_login", "user-1", 20_518, 2),
|
||||
stat("daily_login", "user-2", 20_517, 9),
|
||||
stat("other", "user-1", 20_517, 9),
|
||||
],
|
||||
"daily_login",
|
||||
RuntimeTrackingScopeKind::User,
|
||||
"user-1",
|
||||
AnalyticsGranularity::Day,
|
||||
);
|
||||
|
||||
assert_eq!(buckets.len(), 2);
|
||||
assert_eq!(buckets[0].bucket_key, "2026-03-05");
|
||||
assert_eq!(buckets[0].bucket_start_date_key, 20_517);
|
||||
assert_eq!(buckets[0].bucket_end_date_key, 20_517);
|
||||
assert_eq!(buckets[0].value, 1);
|
||||
assert_eq!(buckets[1].bucket_key, "2026-03-06");
|
||||
assert_eq!(buckets[1].value, 2);
|
||||
}
|
||||
@@ -25,6 +25,11 @@ pub const TRACKING_SCOPE_KIND_SITE: &str = "site";
|
||||
pub const TRACKING_SCOPE_KIND_WORK: &str = "work";
|
||||
pub const TRACKING_SCOPE_KIND_MODULE: &str = "module";
|
||||
pub const TRACKING_SCOPE_KIND_USER: &str = "user";
|
||||
pub const ANALYTICS_GRANULARITY_DAY: &str = "day";
|
||||
pub const ANALYTICS_GRANULARITY_WEEK: &str = "week";
|
||||
pub const ANALYTICS_GRANULARITY_MONTH: &str = "month";
|
||||
pub const ANALYTICS_GRANULARITY_QUARTER: &str = "quarter";
|
||||
pub const ANALYTICS_GRANULARITY_YEAR: &str = "year";
|
||||
pub const BROWSE_HISTORY_THEME_MODE_MARTIAL: &str = "martial";
|
||||
pub const BROWSE_HISTORY_THEME_MODE_ARCANE: &str = "arcane";
|
||||
pub const BROWSE_HISTORY_THEME_MODE_MACHINA: &str = "machina";
|
||||
@@ -367,6 +372,30 @@ pub struct ProfileTaskConfigAdminListResponse {
|
||||
pub entries: Vec<ProfileTaskConfigAdminResponse>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct AnalyticsMetricQueryRequest {
|
||||
pub event_key: String,
|
||||
pub scope_kind: String,
|
||||
pub scope_id: String,
|
||||
pub granularity: String,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct AnalyticsBucketMetricResponse {
|
||||
pub bucket_key: String,
|
||||
pub bucket_start_date_key: i64,
|
||||
pub bucket_end_date_key: i64,
|
||||
pub value: u64,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct AnalyticsMetricQueryResponse {
|
||||
pub buckets: Vec<AnalyticsBucketMetricResponse>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct AdminUpsertProfileTaskConfigRequest {
|
||||
|
||||
@@ -136,6 +136,7 @@ use module_puzzle::{
|
||||
use module_runtime::{
|
||||
RuntimeBrowseHistoryRecord, RuntimePlatformTheme as DomainRuntimePlatformTheme,
|
||||
RuntimeProfileDashboardRecord, RuntimeProfileInviteCodeRecord, RuntimeProfilePlayStatsRecord,
|
||||
AnalyticsMetricQueryResponse as DomainAnalyticsMetricQueryResponse,
|
||||
RuntimeProfileRechargeCenterRecord, RuntimeProfileRechargeOrderRecord,
|
||||
RuntimeProfileRedeemCodeMode as DomainRuntimeProfileRedeemCodeMode,
|
||||
RuntimeProfileRedeemCodeRecord, RuntimeProfileRewardCodeRedeemRecord,
|
||||
@@ -167,6 +168,7 @@ use module_runtime::{
|
||||
build_runtime_profile_wallet_adjustment_input,
|
||||
build_runtime_profile_wallet_ledger_entry_record,
|
||||
build_runtime_profile_wallet_ledger_list_input, build_runtime_referral_invite_center_get_input,
|
||||
build_analytics_metric_query_input,
|
||||
build_runtime_referral_invite_center_record, build_runtime_referral_redeem_input,
|
||||
build_runtime_referral_redeem_record, build_runtime_setting_get_input,
|
||||
build_runtime_setting_record, build_runtime_setting_upsert_input,
|
||||
|
||||
@@ -181,6 +181,17 @@ impl From<module_runtime::RuntimeProfileTaskCenterGetInput> for RuntimeProfileTa
|
||||
}
|
||||
}
|
||||
|
||||
impl From<module_runtime::AnalyticsMetricQueryInput> for AnalyticsMetricQueryInput {
|
||||
fn from(input: module_runtime::AnalyticsMetricQueryInput) -> Self {
|
||||
Self {
|
||||
event_key: input.event_key,
|
||||
scope_kind: map_runtime_tracking_scope_kind(input.scope_kind),
|
||||
scope_id: input.scope_id,
|
||||
granularity: map_analytics_granularity(input.granularity),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<module_runtime::RuntimeProfileTaskClaimInput> for RuntimeProfileTaskClaimInput {
|
||||
fn from(input: module_runtime::RuntimeProfileTaskClaimInput) -> Self {
|
||||
Self {
|
||||
@@ -899,6 +910,22 @@ pub(crate) fn map_runtime_profile_task_center_procedure_result(
|
||||
))
|
||||
}
|
||||
|
||||
pub(crate) fn map_analytics_metric_query_procedure_result(
|
||||
result: AnalyticsMetricQueryProcedureResult,
|
||||
) -> Result<DomainAnalyticsMetricQueryResponse, SpacetimeClientError> {
|
||||
if !result.ok {
|
||||
return Err(SpacetimeClientError::procedure_failed(result.error_message));
|
||||
}
|
||||
|
||||
Ok(DomainAnalyticsMetricQueryResponse {
|
||||
buckets: result
|
||||
.buckets
|
||||
.into_iter()
|
||||
.map(map_analytics_bucket_metric)
|
||||
.collect(),
|
||||
})
|
||||
}
|
||||
|
||||
pub(crate) fn map_runtime_profile_task_claim_procedure_result(
|
||||
result: RuntimeProfileTaskClaimProcedureResult,
|
||||
) -> Result<RuntimeProfileTaskClaimRecord, SpacetimeClientError> {
|
||||
@@ -1751,6 +1778,17 @@ pub(crate) fn map_runtime_profile_dashboard_snapshot(
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn map_analytics_bucket_metric(
|
||||
bucket: AnalyticsBucketMetric,
|
||||
) -> module_runtime::AnalyticsBucketMetric {
|
||||
module_runtime::AnalyticsBucketMetric {
|
||||
bucket_key: bucket.bucket_key,
|
||||
bucket_start_date_key: bucket.bucket_start_date_key,
|
||||
bucket_end_date_key: bucket.bucket_end_date_key,
|
||||
value: bucket.value,
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn map_runtime_profile_wallet_ledger_entry_snapshot(
|
||||
snapshot: RuntimeProfileWalletLedgerEntrySnapshot,
|
||||
) -> module_runtime::RuntimeProfileWalletLedgerEntrySnapshot {
|
||||
@@ -4012,6 +4050,18 @@ pub(crate) fn map_runtime_profile_wallet_ledger_source_type_back(
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn map_analytics_granularity(
|
||||
granularity: module_runtime::AnalyticsGranularity,
|
||||
) -> AnalyticsGranularity {
|
||||
match granularity {
|
||||
module_runtime::AnalyticsGranularity::Day => AnalyticsGranularity::Day,
|
||||
module_runtime::AnalyticsGranularity::Week => AnalyticsGranularity::Week,
|
||||
module_runtime::AnalyticsGranularity::Month => AnalyticsGranularity::Month,
|
||||
module_runtime::AnalyticsGranularity::Quarter => AnalyticsGranularity::Quarter,
|
||||
module_runtime::AnalyticsGranularity::Year => AnalyticsGranularity::Year,
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn map_runtime_tracking_scope_kind(
|
||||
value: DomainRuntimeTrackingScopeKind,
|
||||
) -> crate::module_bindings::RuntimeTrackingScopeKind {
|
||||
|
||||
@@ -0,0 +1,18 @@
|
||||
// THIS FILE IS AUTOMATICALLY GENERATED BY SPACETIMEDB. EDITS TO THIS FILE
|
||||
// WILL NOT BE SAVED. MODIFY TABLES IN YOUR MODULE SOURCE CODE INSTEAD.
|
||||
|
||||
#![allow(unused, clippy::all)]
|
||||
use spacetimedb_sdk::__codegen::{self as __sdk, __lib, __sats, __ws};
|
||||
|
||||
#[derive(__lib::ser::Serialize, __lib::de::Deserialize, Clone, PartialEq, Debug)]
|
||||
#[sats(crate = __lib)]
|
||||
pub struct AnalyticsBucketMetric {
|
||||
pub bucket_key: String,
|
||||
pub bucket_start_date_key: i64,
|
||||
pub bucket_end_date_key: i64,
|
||||
pub value: u64,
|
||||
}
|
||||
|
||||
impl __sdk::InModule for AnalyticsBucketMetric {
|
||||
type Module = super::RemoteModule;
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
// THIS FILE IS AUTOMATICALLY GENERATED BY SPACETIMEDB. EDITS TO THIS FILE
|
||||
// WILL NOT BE SAVED. MODIFY TABLES IN YOUR MODULE SOURCE CODE INSTEAD.
|
||||
|
||||
#![allow(unused, clippy::all)]
|
||||
use spacetimedb_sdk::__codegen::{self as __sdk, __lib, __sats, __ws};
|
||||
|
||||
#[derive(__lib::ser::Serialize, __lib::de::Deserialize, Clone, PartialEq, Debug)]
|
||||
#[sats(crate = __lib)]
|
||||
#[derive(Copy, Eq, Hash)]
|
||||
pub enum AnalyticsGranularity {
|
||||
Day,
|
||||
|
||||
Week,
|
||||
|
||||
Month,
|
||||
|
||||
Quarter,
|
||||
|
||||
Year,
|
||||
}
|
||||
|
||||
impl __sdk::InModule for AnalyticsGranularity {
|
||||
type Module = super::RemoteModule;
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
// THIS FILE IS AUTOMATICALLY GENERATED BY SPACETIMEDB. EDITS TO THIS FILE
|
||||
// WILL NOT BE SAVED. MODIFY TABLES IN YOUR MODULE SOURCE CODE INSTEAD.
|
||||
|
||||
#![allow(unused, clippy::all)]
|
||||
use spacetimedb_sdk::__codegen::{self as __sdk, __lib, __sats, __ws};
|
||||
|
||||
use super::analytics_granularity_type::AnalyticsGranularity;
|
||||
use super::runtime_tracking_scope_kind_type::RuntimeTrackingScopeKind;
|
||||
|
||||
#[derive(__lib::ser::Serialize, __lib::de::Deserialize, Clone, PartialEq, Debug)]
|
||||
#[sats(crate = __lib)]
|
||||
pub struct AnalyticsMetricQueryInput {
|
||||
pub event_key: String,
|
||||
pub scope_kind: RuntimeTrackingScopeKind,
|
||||
pub scope_id: String,
|
||||
pub granularity: AnalyticsGranularity,
|
||||
}
|
||||
|
||||
impl __sdk::InModule for AnalyticsMetricQueryInput {
|
||||
type Module = super::RemoteModule;
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
// THIS FILE IS AUTOMATICALLY GENERATED BY SPACETIMEDB. EDITS TO THIS FILE
|
||||
// WILL NOT BE SAVED. MODIFY TABLES IN YOUR MODULE SOURCE CODE INSTEAD.
|
||||
|
||||
#![allow(unused, clippy::all)]
|
||||
use spacetimedb_sdk::__codegen::{self as __sdk, __lib, __sats, __ws};
|
||||
|
||||
use super::analytics_bucket_metric_type::AnalyticsBucketMetric;
|
||||
|
||||
#[derive(__lib::ser::Serialize, __lib::de::Deserialize, Clone, PartialEq, Debug)]
|
||||
#[sats(crate = __lib)]
|
||||
pub struct AnalyticsMetricQueryProcedureResult {
|
||||
pub ok: bool,
|
||||
pub buckets: Vec<AnalyticsBucketMetric>,
|
||||
pub error_message: Option<String>,
|
||||
}
|
||||
|
||||
impl __sdk::InModule for AnalyticsMetricQueryProcedureResult {
|
||||
type Module = super::RemoteModule;
|
||||
}
|
||||
@@ -690,6 +690,12 @@ pub mod user_account_type;
|
||||
pub mod user_browse_history_table;
|
||||
pub mod user_browse_history_type;
|
||||
|
||||
pub mod analytics_bucket_metric_type;
|
||||
pub mod analytics_granularity_type;
|
||||
pub mod analytics_metric_query_input_type;
|
||||
pub mod analytics_metric_query_procedure_result_type;
|
||||
pub mod query_analytics_metric_procedure;
|
||||
|
||||
pub use accept_quest_reducer::accept_quest;
|
||||
pub use acknowledge_quest_completion_reducer::acknowledge_quest_completion;
|
||||
pub use admin_disable_profile_redeem_code_procedure::admin_disable_profile_redeem_code;
|
||||
@@ -736,6 +742,10 @@ pub use analytics_date_dimension_ensure_input_type::AnalyticsDateDimensionEnsure
|
||||
pub use analytics_date_dimension_seed_input_type::AnalyticsDateDimensionSeedInput;
|
||||
pub use analytics_date_dimension_table::*;
|
||||
pub use analytics_date_dimension_type::AnalyticsDateDimension;
|
||||
pub use analytics_bucket_metric_type::AnalyticsBucketMetric;
|
||||
pub use analytics_granularity_type::AnalyticsGranularity;
|
||||
pub use analytics_metric_query_input_type::AnalyticsMetricQueryInput;
|
||||
pub use analytics_metric_query_procedure_result_type::AnalyticsMetricQueryProcedureResult;
|
||||
pub use append_ai_text_chunk_and_return_procedure::append_ai_text_chunk_and_return;
|
||||
pub use apply_chapter_progression_ledger_entry_and_return_procedure::apply_chapter_progression_ledger_entry_and_return;
|
||||
pub use apply_chapter_progression_ledger_entry_reducer::apply_chapter_progression_ledger_entry;
|
||||
@@ -978,6 +988,7 @@ pub use get_profile_play_stats_procedure::get_profile_play_stats;
|
||||
pub use get_profile_recharge_center_procedure::get_profile_recharge_center;
|
||||
pub use get_profile_referral_invite_center_procedure::get_profile_referral_invite_center;
|
||||
pub use get_profile_task_center_procedure::get_profile_task_center;
|
||||
pub use query_analytics_metric_procedure::query_analytics_metric;
|
||||
pub use get_puzzle_agent_session_procedure::get_puzzle_agent_session;
|
||||
pub use get_puzzle_gallery_detail_procedure::get_puzzle_gallery_detail;
|
||||
pub use get_puzzle_run_procedure::get_puzzle_run;
|
||||
|
||||
@@ -0,0 +1,59 @@
|
||||
// THIS FILE IS AUTOMATICALLY GENERATED BY SPACETIMEDB. EDITS TO THIS FILE
|
||||
// WILL NOT BE SAVED. MODIFY TABLES IN YOUR MODULE SOURCE CODE INSTEAD.
|
||||
|
||||
#![allow(unused, clippy::all)]
|
||||
use spacetimedb_sdk::__codegen::{self as __sdk, __lib, __sats, __ws};
|
||||
|
||||
use super::analytics_metric_query_input_type::AnalyticsMetricQueryInput;
|
||||
use super::analytics_metric_query_procedure_result_type::AnalyticsMetricQueryProcedureResult;
|
||||
|
||||
#[derive(__lib::ser::Serialize, __lib::de::Deserialize, Clone, PartialEq, Debug)]
|
||||
#[sats(crate = __lib)]
|
||||
struct QueryAnalyticsMetricArgs {
|
||||
pub input: AnalyticsMetricQueryInput,
|
||||
}
|
||||
|
||||
impl __sdk::InModule for QueryAnalyticsMetricArgs {
|
||||
type Module = super::RemoteModule;
|
||||
}
|
||||
|
||||
#[allow(non_camel_case_types)]
|
||||
/// Extension trait for access to the procedure `query_analytics_metric`.
|
||||
///
|
||||
/// Implemented for [`super::RemoteProcedures`].
|
||||
pub trait query_analytics_metric {
|
||||
fn query_analytics_metric(&self, input: AnalyticsMetricQueryInput) {
|
||||
self.query_analytics_metric_then(input, |_, _| {});
|
||||
}
|
||||
|
||||
fn query_analytics_metric_then(
|
||||
&self,
|
||||
input: AnalyticsMetricQueryInput,
|
||||
|
||||
__callback: impl FnOnce(
|
||||
&super::ProcedureEventContext,
|
||||
Result<AnalyticsMetricQueryProcedureResult, __sdk::InternalError>,
|
||||
) + Send
|
||||
+ 'static,
|
||||
);
|
||||
}
|
||||
|
||||
impl query_analytics_metric for super::RemoteProcedures {
|
||||
fn query_analytics_metric_then(
|
||||
&self,
|
||||
input: AnalyticsMetricQueryInput,
|
||||
|
||||
__callback: impl FnOnce(
|
||||
&super::ProcedureEventContext,
|
||||
Result<AnalyticsMetricQueryProcedureResult, __sdk::InternalError>,
|
||||
) + Send
|
||||
+ 'static,
|
||||
) {
|
||||
self.imp
|
||||
.invoke_procedure_with_callback::<_, AnalyticsMetricQueryProcedureResult>(
|
||||
"query_analytics_metric",
|
||||
QueryAnalyticsMetricArgs { input },
|
||||
__callback,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -348,6 +348,31 @@ impl SpacetimeClient {
|
||||
.await
|
||||
}
|
||||
|
||||
pub async fn query_analytics_metric(
|
||||
&self,
|
||||
event_key: String,
|
||||
scope_kind: DomainRuntimeTrackingScopeKind,
|
||||
scope_id: String,
|
||||
granularity: module_runtime::AnalyticsGranularity,
|
||||
) -> Result<DomainAnalyticsMetricQueryResponse, SpacetimeClientError> {
|
||||
let procedure_input =
|
||||
build_analytics_metric_query_input(event_key, scope_kind, scope_id, granularity)
|
||||
.map_err(SpacetimeClientError::validation_failed)?
|
||||
.into();
|
||||
|
||||
self.call_after_connect(move |connection, sender| {
|
||||
connection
|
||||
.procedures()
|
||||
.query_analytics_metric_then(procedure_input, move |_, result| {
|
||||
let mapped = result
|
||||
.map_err(SpacetimeClientError::from_sdk_error)
|
||||
.and_then(map_analytics_metric_query_procedure_result);
|
||||
send_once(&sender, mapped);
|
||||
});
|
||||
})
|
||||
.await
|
||||
}
|
||||
|
||||
pub async fn admin_list_profile_task_configs(
|
||||
&self,
|
||||
admin_user_id: String,
|
||||
|
||||
@@ -471,6 +471,27 @@ pub fn list_profile_wallet_ledger(
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// analytics metric 查询直接聚合 tracking_daily_stat,避免 API 层订阅全量表后自行汇总。
|
||||
#[spacetimedb::procedure]
|
||||
pub fn query_analytics_metric(
|
||||
ctx: &mut ProcedureContext,
|
||||
input: AnalyticsMetricQueryInput,
|
||||
) -> AnalyticsMetricQueryProcedureResult {
|
||||
match ctx.try_with_tx(|tx| query_analytics_metric_buckets(tx, input.clone())) {
|
||||
Ok(buckets) => AnalyticsMetricQueryProcedureResult {
|
||||
ok: true,
|
||||
buckets,
|
||||
error_message: None,
|
||||
},
|
||||
Err(message) => AnalyticsMetricQueryProcedureResult {
|
||||
ok: false,
|
||||
buckets: Vec::new(),
|
||||
error_message: Some(message),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// 任务中心读取会顺手记录当日登录埋点,确保“每日登录”只依赖后端事实。
|
||||
#[spacetimedb::procedure]
|
||||
pub fn get_profile_task_center(
|
||||
@@ -2726,6 +2747,45 @@ fn build_profile_task_center_snapshot(
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
fn query_analytics_metric_buckets(
|
||||
ctx: &ReducerContext,
|
||||
input: AnalyticsMetricQueryInput,
|
||||
) -> Result<Vec<AnalyticsBucketMetric>, String> {
|
||||
let validated_input = build_analytics_metric_query_input(
|
||||
input.event_key,
|
||||
input.scope_kind,
|
||||
input.scope_id,
|
||||
input.granularity,
|
||||
)
|
||||
.map_err(|error| error.to_string())?;
|
||||
let stats = ctx
|
||||
.db
|
||||
.tracking_daily_stat()
|
||||
.iter()
|
||||
.filter(|row| {
|
||||
row.event_key.trim() == validated_input.event_key
|
||||
&& row.scope_kind == validated_input.scope_kind
|
||||
&& row.scope_id.trim() == validated_input.scope_id
|
||||
})
|
||||
.map(|row| RuntimeAnalyticsDailyStatSnapshot {
|
||||
event_key: row.event_key,
|
||||
scope_kind: row.scope_kind,
|
||||
scope_id: row.scope_id,
|
||||
day_key: row.day_key,
|
||||
count: row.count,
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
Ok(aggregate_runtime_tracking_daily_stats(
|
||||
stats,
|
||||
&validated_input.event_key,
|
||||
validated_input.scope_kind,
|
||||
&validated_input.scope_id,
|
||||
validated_input.granularity,
|
||||
))
|
||||
}
|
||||
|
||||
fn refresh_profile_task_progress(
|
||||
ctx: &ReducerContext,
|
||||
user_id: &str,
|
||||
|
||||
Reference in New Issue
Block a user