reasonkit/telemetry/
mod.rs1mod config;
33mod events;
34mod privacy;
35mod schema;
36mod storage;
37
38pub use config::*;
44pub use events::*;
45pub use privacy::*;
46pub use schema::*;
47pub use storage::*;
48
49use chrono::{DateTime, Utc};
53use serde::{Deserialize, Serialize};
54use std::sync::Arc;
55use tokio::sync::RwLock;
56use uuid::Uuid;
57
58pub const TELEMETRY_SCHEMA_VERSION: u32 = 1;
60
61pub const DEFAULT_TELEMETRY_DB: &str = ".rk_telemetry.db";
63
64pub type TelemetryResult<T> = Result<T, TelemetryError>;
66
67#[derive(Debug, thiserror::Error)]
69pub enum TelemetryError {
70 #[error("Database error: {0}")]
72 Database(String),
73
74 #[error("Configuration error: {0}")]
76 Config(String),
77
78 #[error("Privacy violation: {0}")]
80 PrivacyViolation(String),
81
82 #[error("Schema validation error: {0}")]
84 SchemaValidation(String),
85
86 #[error("IO error: {0}")]
88 Io(#[from] std::io::Error),
89
90 #[error("JSON error: {0}")]
92 Json(#[from] serde_json::Error),
93
94 #[error("Telemetry is disabled")]
96 Disabled,
97}
98
99pub struct TelemetryCollector {
101 config: TelemetryConfig,
103 storage: Arc<RwLock<TelemetryStorage>>,
105 privacy: PrivacyFilter,
107 session_id: Uuid,
109 enabled: bool,
111}
112
113impl TelemetryCollector {
114 pub async fn new(config: TelemetryConfig) -> TelemetryResult<Self> {
116 if !config.enabled {
117 return Ok(Self {
118 config: config.clone(),
119 storage: Arc::new(RwLock::new(TelemetryStorage::noop())),
120 privacy: PrivacyFilter::new(config.privacy.clone()),
121 session_id: Uuid::new_v4(),
122 enabled: false,
123 });
124 }
125
126 let mut storage = TelemetryStorage::new(&config.db_path).await?;
127 let privacy = PrivacyFilter::new(config.privacy.clone());
128 let session_id = Uuid::new_v4();
129
130 storage.insert_session(session_id).await?;
132
133 Ok(Self {
134 config: config.clone(),
135 storage: Arc::new(RwLock::new(storage)),
136 privacy,
137 session_id,
138 enabled: true,
139 })
140 }
141
142 pub async fn record_query(&self, event: QueryEvent) -> TelemetryResult<()> {
144 if !self.enabled {
145 return Ok(());
146 }
147
148 let sanitized = self.privacy.sanitize_query_event(event)?;
150
151 let mut storage = self.storage.write().await;
153 storage.insert_query_event(&sanitized).await
154 }
155
156 pub async fn record_feedback(&self, event: FeedbackEvent) -> TelemetryResult<()> {
158 if !self.enabled {
159 return Ok(());
160 }
161
162 let sanitized = self.privacy.sanitize_feedback_event(event)?;
163
164 let mut storage = self.storage.write().await;
165 storage.insert_feedback_event(&sanitized).await
166 }
167
168 pub async fn record_trace(&self, event: TraceEvent) -> TelemetryResult<()> {
170 if !self.enabled {
171 return Ok(());
172 }
173
174 let sanitized = self.privacy.sanitize_trace_event(event)?;
175
176 let mut storage = self.storage.write().await;
177 storage.insert_trace_event(&sanitized).await
178 }
179
180 pub async fn get_aggregated_metrics(&self) -> TelemetryResult<AggregatedMetrics> {
182 if !self.enabled {
183 return Err(TelemetryError::Disabled);
184 }
185
186 let storage = self.storage.read().await;
187 storage.get_aggregated_metrics().await
188 }
189
190 pub async fn export_for_community(&self) -> TelemetryResult<CommunityExport> {
192 if !self.enabled || !self.config.community_contribution {
193 return Err(TelemetryError::Disabled);
194 }
195
196 let storage = self.storage.read().await;
197 storage.export_anonymized().await
198 }
199
200 pub fn session_id(&self) -> Uuid {
202 self.session_id
203 }
204
205 pub fn is_enabled(&self) -> bool {
207 self.enabled
208 }
209}
210
211#[derive(Debug, Clone, Serialize, Deserialize)]
213pub struct AggregatedMetrics {
214 pub total_queries: u64,
216 pub avg_latency_ms: f64,
218 pub tool_usage: Vec<ToolUsageMetric>,
220 pub query_clusters: Vec<QueryCluster>,
222 pub feedback_summary: FeedbackSummary,
224 pub time_range: TimeRange,
226}
227
228#[derive(Debug, Clone, Serialize, Deserialize)]
230pub struct ToolUsageMetric {
231 pub tool: String,
233 pub count: u64,
235 pub success_rate: f64,
237 pub avg_execution_ms: f64,
239}
240
241#[derive(Debug, Clone, Serialize, Deserialize)]
243pub struct QueryCluster {
244 pub id: u32,
246 pub centroid: Option<Vec<f32>>,
248 pub count: u64,
250 pub representative: String,
252 pub common_tools: Vec<String>,
254}
255
256#[derive(Debug, Clone, Serialize, Deserialize)]
258pub struct FeedbackSummary {
259 pub total_feedback: u64,
261 pub positive_ratio: f64,
263 pub improvement_areas: Vec<String>,
265}
266
267#[derive(Debug, Clone, Serialize, Deserialize)]
269pub struct TimeRange {
270 pub start: DateTime<Utc>,
272 pub end: DateTime<Utc>,
274}
275
276#[derive(Debug, Clone, Serialize, Deserialize)]
278pub struct CommunityExport {
279 pub schema_version: u32,
281 pub exported_at: DateTime<Utc>,
283 pub aggregates: AggregatedMetrics,
285 pub dp_epsilon: f64,
287 pub contributor_hash: String,
289}
290
291#[cfg(test)]
292mod tests {
293 use super::*;
294
295 #[tokio::test]
296 async fn test_telemetry_disabled() {
297 let config = TelemetryConfig {
298 enabled: false,
299 ..Default::default()
300 };
301
302 let collector = TelemetryCollector::new(config).await.unwrap();
303 assert!(!collector.is_enabled());
304 }
305
306 #[tokio::test]
307 async fn test_session_id_generation() {
308 let config = TelemetryConfig::default();
309 let collector = TelemetryCollector::new(config).await.unwrap();
310
311 let _ = collector.session_id();
313 }
314}