Files
Genarrative/apps/ai-game-creator-shell/src-tauri/src/analytics/preview.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

147 lines
4.4 KiB
Rust

//! 正式 Web 预览的短时可访问性观察;不影响预览业务生命周期。
use super::{
contract::{Context, EventData, PreviewReady, PreviewSource, Source},
store::AnalyticsWriter,
};
use std::{
path::{Path, PathBuf},
sync::{Arc, Weak},
time::{Duration, Instant},
};
pub(crate) struct Lease {
_alive: Arc<()>,
}
pub(crate) struct Observation {
alive: Weak<()>,
instance_id: String,
root: PathBuf,
entry: PathBuf,
project_id: String,
revision: u64,
context: Context,
writer: AnalyticsWriter,
source: Source,
preview_source: PreviewSource,
started: Instant,
cancellation: Option<Arc<std::sync::atomic::AtomicBool>>,
}
fn nonempty_entry(entry: &Path) -> bool {
std::fs::metadata(entry).is_ok_and(|metadata| metadata.is_file() && metadata.len() > 0)
}
pub(crate) fn prepare(
root: &Path,
capture: Option<(Context, AnalyticsWriter)>,
source: Source,
preview_source: PreviewSource,
) -> Option<(Lease, Observation)> {
let (context, writer) = capture?;
let entry = crate::project_game_root(root).join("index.html");
if !nonempty_entry(&entry) {
return None;
}
let project_id = crate::read_existing_manifest_for_project(root)
.ok()?
.project_id;
let revision = crate::read_game_creator_agent_runtime_project_revision(root)
.ok()?
.revision;
let alive = Arc::new(());
let observation = Observation {
alive: Arc::downgrade(&alive),
instance_id: uuid::Uuid::new_v4().to_string(),
root: root.into(),
entry,
project_id,
revision,
context,
writer,
source,
preview_source,
started: Instant::now(),
cancellation: None,
};
Some((Lease { _alive: alive }, observation))
}
impl Observation {
pub(crate) fn with_cancellation(
mut self,
cancellation: Option<Arc<std::sync::atomic::AtomicBool>>,
) -> Self {
self.cancellation = cancellation;
self
}
fn current(&self) -> bool {
!self
.cancellation
.as_ref()
.is_some_and(|flag| flag.load(std::sync::atomic::Ordering::Acquire))
&& self.alive.strong_count() > 0
&& self.entry == crate::project_game_root(&self.root).join("index.html")
&& nonempty_entry(&self.entry)
&& crate::read_game_creator_agent_runtime_project_revision(&self.root)
.is_ok_and(|revision| revision.revision == self.revision)
&& crate::read_existing_manifest_for_project(&self.root)
.is_ok_and(|manifest| manifest.project_id == self.project_id)
}
pub(crate) fn schedule(self, port: u16) {
tauri::async_runtime::spawn(self.observe(port));
}
pub(crate) async fn observe(self, port: u16) {
if !self.current() {
return;
}
let reachable = tokio::time::timeout(Duration::from_secs(2), async {
let client = reqwest::Client::builder()
.no_proxy()
.redirect(reqwest::redirect::Policy::none())
.build()
.ok()?;
let mut response = client
.get(format!("http://127.0.0.1:{port}/"))
.send()
.await
.ok()?;
if !response.status().is_success() {
return None;
}
while let Some(chunk) = response.chunk().await.ok()? {
if !chunk.is_empty() {
return Some(());
}
}
None
})
.await;
if !matches!(reachable, Ok(Some(()))) || !self.current() {
return;
}
let event = self.context.capture(
EventData::PreviewReady(PreviewReady {
preview_source: self.preview_source,
preview_version: self.revision.to_string(),
ready_duration_ms: u64::try_from(self.started.elapsed().as_millis()).ok(),
}),
Some(self.project_id.clone()),
self.source,
None,
);
if let Ok(event) = event {
if self.current() {
let key = format!(
"{}:{}:{}:preview_ready",
self.project_id, self.instance_id, self.revision
);
self.writer.try_record(self.context.route, event, key);
}
}
}
}