memvid_cli/analytics/
queue.rs1use serde::{Deserialize, Serialize};
7use std::fs::{self, OpenOptions};
8use std::io::{BufRead, BufReader, Write};
9use std::path::PathBuf;
10use std::sync::Mutex;
11
12#[derive(Debug, Clone, Serialize, Deserialize)]
14pub struct AnalyticsEvent {
15 pub anon_id: String,
16 pub file_hash: String,
17 pub client: String,
18 pub command: String,
19 pub success: bool,
20 pub timestamp: String,
21 #[serde(default)]
22 pub file_created: bool,
23 #[serde(default)]
24 pub file_opened: bool,
25 #[serde(default = "default_tier")]
27 pub user_tier: String,
28}
29
30fn default_tier() -> String {
31 "free".to_string()
32}
33
34static QUEUE_LOCK: Mutex<()> = Mutex::new(());
36
37fn get_analytics_dir() -> Option<PathBuf> {
39 dirs::data_local_dir().map(|d| d.join("memvid").join("analytics"))
40}
41
42fn get_queue_path() -> Option<PathBuf> {
44 get_analytics_dir().map(|d| d.join("queue.jsonl"))
45}
46
47fn ensure_dir() -> Option<PathBuf> {
49 let dir = get_analytics_dir()?;
50 fs::create_dir_all(&dir).ok()?;
51 Some(dir)
52}
53
54pub fn track_event(event: AnalyticsEvent) {
57 if let Err(_e) = track_event_inner(event) {
58 #[cfg(debug_assertions)]
60 eprintln!("[analytics] Failed to queue event: {}", _e);
61 }
62}
63
64fn track_event_inner(event: AnalyticsEvent) -> std::io::Result<()> {
65 let _lock = QUEUE_LOCK.lock().map_err(|_| {
66 std::io::Error::new(std::io::ErrorKind::Other, "Failed to acquire queue lock")
67 })?;
68
69 let queue_path = get_queue_path()
70 .ok_or_else(|| std::io::Error::new(std::io::ErrorKind::NotFound, "Cannot find data dir"))?;
71
72 ensure_dir();
73
74 let mut file = OpenOptions::new()
75 .create(true)
76 .append(true)
77 .open(&queue_path)?;
78
79 let json = serde_json::to_string(&event)
80 .map_err(|e| std::io::Error::new(std::io::ErrorKind::InvalidData, e.to_string()))?;
81
82 writeln!(file, "{}", json)?;
83 Ok(())
84}
85
86pub fn read_pending_events() -> Vec<AnalyticsEvent> {
88 let _lock = match QUEUE_LOCK.lock() {
89 Ok(lock) => lock,
90 Err(_) => return vec![],
91 };
92
93 let queue_path = match get_queue_path() {
94 Some(p) => p,
95 None => return vec![],
96 };
97
98 if !queue_path.exists() {
99 return vec![];
100 }
101
102 let file = match fs::File::open(&queue_path) {
103 Ok(f) => f,
104 Err(_) => return vec![],
105 };
106
107 let reader = BufReader::new(file);
108 let mut events = Vec::new();
109
110 for line in reader.lines() {
111 if let Ok(line) = line {
112 if let Ok(event) = serde_json::from_str::<AnalyticsEvent>(&line) {
113 events.push(event);
114 }
115 }
116 }
117
118 events
119}
120
121pub fn clear_queue() {
123 let _lock = match QUEUE_LOCK.lock() {
124 Ok(lock) => lock,
125 Err(_) => return,
126 };
127
128 if let Some(queue_path) = get_queue_path() {
129 let _ = fs::remove_file(&queue_path);
130 }
131}
132
133pub fn pending_count() -> usize {
135 read_pending_events().len()
136}
137
138#[allow(dead_code)]
140pub fn queue_size_bytes() -> u64 {
141 get_queue_path()
142 .and_then(|p| fs::metadata(p).ok())
143 .map(|m| m.len())
144 .unwrap_or(0)
145}