Files
Genarrative/apps/ai-game-creator-shell/src-tauri/src/analytics/project.rs
T
lhk229 1e186369c9
Project CI / AI game creator shell Rust crates (push) Successful in 1m24s
Project CI / AI game creator shell Rust smoke (push) Successful in 1m56s
Project CI / AI game creator shell Rust lane 1/2 (push) Has been cancelled
Project CI / Frontend tests (push) Has been cancelled
Project CI / Backend tests (push) Has been cancelled
Project CI / Repository checks (push) Has been cancelled
Project CI / AI game creator shell web tests (push) Has been cancelled
Project CI / AI game creator shell Rust lane 2/2 (push) Has been cancelled
Project CI / Native shell tests (push) Has been cancelled
客户端埋点设置 (#446)
Reviewed-on: https://git.genarrative.world/git/GenarrativeAI/Genarrative/pulls/446
Co-authored-by: Linghong <ink29535@proton.me>
Co-committed-by: Linghong <ink29535@proton.me>
2026-09-23 00:09:12 +08:00

105 lines
3.3 KiB
Rust

//! 正式项目成果事件。只接收宿主已提交的版本,不参与业务写入。
use super::contract::{ChangeKind, Context, EventData, ProjectSaved, RevisionCreated, Source};
use super::store::AnalyticsWriter;
use std::io::Read;
use std::path::Path;
pub(crate) fn saved(
capture: Option<(Context, AnalyticsWriter)>,
project_id: &str,
source: Source,
operation_id: &str,
data: ProjectSaved,
event_time: Option<&str>,
) {
let Some((context, writer)) = capture else {
return;
};
if let Ok(mut event) = context.capture(
EventData::ProjectSave(data),
Some(project_id.into()),
source,
None,
) {
if let Some(time) = event_time {
event.event_time = time.into();
}
writer.try_record(
context.route,
event,
format!("{project_id}:{operation_id}:project_save"),
);
}
}
pub(crate) fn file_content_changed(
root: &Path,
path: &str,
content: &[u8],
max_bytes: u64,
) -> Option<bool> {
let target = crate::resolve_local_project_path(root, path).ok()?;
match std::fs::symlink_metadata(&target) {
Err(error) if error.kind() == std::io::ErrorKind::NotFound => return Some(true),
Err(_) => return None,
Ok(_) => {}
}
let (file, metadata) =
crate::open_project_snapshot_regular_file(&target, "成果内容比较").ok()?;
if metadata.len() > max_bytes {
return None;
}
let mut before = Vec::new();
file.take(max_bytes.saturating_add(1))
.read_to_end(&mut before)
.ok()?;
(before.len() as u64 <= max_bytes).then(|| before != content)
}
pub(crate) fn revision(
capture: Option<(Context, AnalyticsWriter)>,
project_id: &str,
source: Source,
data: RevisionCreated,
) {
let Some((context, writer)) = capture else {
return;
};
let key = format!("{project_id}:{}:project_revision_created", data.revision_id);
if let Ok(event) = context.capture(
EventData::ProjectRevisionCreated(data),
Some(project_id.into()),
source,
None,
) {
writer.try_record(context.route, event, key);
}
}
// 只给已知成果分类;未知文件不猜字段,控制面和构建缓存不计成果。
pub(crate) fn file_change_kind(path: &str) -> Option<ChangeKind> {
let path = path.to_ascii_lowercase();
if crate::should_skip_project_snapshot_path(&path)
|| path.split('/').any(|part| part.starts_with('.'))
{
return None;
}
if path.starts_with("design_artifacts/") {
return Some(ChangeKind::DesignDocument);
}
if path.starts_with("ui/") {
return Some(ChangeKind::Ui);
}
let extension = path.rsplit_once('.')?.1;
match extension {
"png" | "jpg" | "jpeg" | "webp" | "gif" | "svg" | "avif" | "mp3" | "wav" | "ogg"
| "flac" | "mp4" | "webm" | "glb" | "gltf" | "ttf" | "woff" | "woff2" => {
Some(ChangeKind::Asset)
}
"js" | "jsx" | "ts" | "tsx" | "mjs" | "cjs" | "html" | "css" | "scss" | "json" | "vue"
| "svelte" | "gd" | "tscn" | "tres" | "cs" | "shader" | "glsl" | "wgsl" | "vert"
| "frag" | "rs" | "py" | "lua" | "cpp" | "h" | "c" | "hpp" => Some(ChangeKind::Code),
_ => None,
}
}