feat: add work-level play tracking
Some checks failed
CI / verify (push) Has been cancelled

This commit is contained in:
2026-05-09 19:56:59 +08:00
parent 32a1530ab1
commit 3ad1075227
24 changed files with 1452 additions and 105 deletions

View File

@@ -0,0 +1,111 @@
use module_runtime::RuntimeTrackingScopeKind;
use serde_json::{Value, json};
use crate::{
auth::AuthenticatedAccessToken,
request_context::RequestContext,
state::AppState,
tracking::{TrackingEventDraft, record_tracking_event_after_success},
};
pub(crate) const WORK_PLAY_START_EVENT_KEY: &str = "work_play_start";
pub(crate) struct WorkPlayTrackingDraft {
pub play_type: &'static str,
pub work_id: String,
pub user_id: String,
pub owner_user_id: Option<String>,
pub profile_id: Option<String>,
pub run_id: Option<String>,
pub source_route: &'static str,
pub extra: Value,
}
impl WorkPlayTrackingDraft {
pub(crate) fn new(
play_type: &'static str,
work_id: impl Into<String>,
authenticated: &AuthenticatedAccessToken,
source_route: &'static str,
) -> Self {
let user_id = authenticated.claims().user_id().to_string();
Self {
play_type,
work_id: work_id.into(),
user_id,
owner_user_id: None,
profile_id: None,
run_id: None,
source_route,
extra: json!({}),
}
}
pub(crate) fn owner_user_id(mut self, owner_user_id: impl Into<String>) -> Self {
self.owner_user_id = Some(owner_user_id.into());
self
}
pub(crate) fn profile_id(mut self, profile_id: impl Into<String>) -> Self {
self.profile_id = Some(profile_id.into());
self
}
pub(crate) fn run_id(mut self, run_id: impl Into<String>) -> Self {
self.run_id = Some(run_id.into());
self
}
pub(crate) fn extra(mut self, extra: Value) -> Self {
self.extra = extra;
self
}
}
/// 作品级正式游玩埋点scope 固定为 workscope_id 固定为稳定作品 ID。
/// 中文注释:该埋点用于“某作品被多少用户玩过”等分析,写入失败不阻断 runtime 主流程。
pub(crate) async fn record_work_play_start_after_success(
state: &AppState,
request_context: &RequestContext,
draft: WorkPlayTrackingDraft,
) {
let mut metadata = json!({
"operation": WORK_PLAY_START_EVENT_KEY,
"playType": draft.play_type,
"workId": draft.work_id,
"sourceRoute": draft.source_route,
});
metadata["userId"] = json!(draft.user_id);
if let Some(owner_user_id) = draft.owner_user_id.as_deref() {
metadata["ownerUserId"] = json!(owner_user_id);
}
if let Some(profile_id) = draft.profile_id.as_deref() {
metadata["profileId"] = json!(profile_id);
}
if let Some(run_id) = draft.run_id.as_deref() {
metadata["runId"] = json!(run_id);
}
if !draft.extra.is_null() {
metadata["extra"] = draft.extra;
}
let mut tracking = TrackingEventDraft::new(WORK_PLAY_START_EVENT_KEY, draft.play_type);
tracking.scope_kind = RuntimeTrackingScopeKind::Work;
tracking.scope_id = draft.work_id;
tracking.user_id = Some(draft.user_id);
tracking.owner_user_id = draft.owner_user_id;
tracking.profile_id = draft.profile_id;
tracking.metadata = metadata;
record_tracking_event_after_success(state, request_context, tracking).await;
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn work_play_event_key_is_stable() {
assert_eq!(WORK_PLAY_START_EVENT_KEY, "work_play_start");
}
}