use serde::{Deserialize, Serialize};
use std::fs;
use std::path::PathBuf;
use std::sync::atomic::{AtomicBool, Ordering};
use std::sync::{Arc, OnceLock};
use std::time::{Duration, Instant, SystemTime, UNIX_EPOCH};
use uuid::Uuid;
use crate::error::Result;
static TELEMETRY: OnceLock<Arc<TelemetryClient>> = OnceLock::new();
static TELEMETRY_DISABLED: AtomicBool = AtomicBool::new(false);
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct TelemetryConfig {
pub enabled: bool,
#[serde(default = "generate_device_id")]
pub device_id: String,
#[serde(default = "default_endpoint")]
pub endpoint: String,
#[serde(default = "default_batch_size")]
pub batch_size: usize,
#[serde(default = "default_flush_interval")]
pub flush_interval_secs: u64,
}
fn generate_device_id() -> String {
Uuid::new_v4().to_string()
}
fn default_endpoint() -> String {
"https://telemetry.nervosys.ai/v1/events".to_string()
}
fn default_batch_size() -> usize {
25
}
fn default_flush_interval() -> u64 {
300 }
impl Default for TelemetryConfig {
fn default() -> Self {
Self {
enabled: false, device_id: generate_device_id(),
endpoint: default_endpoint(),
batch_size: default_batch_size(),
flush_interval_secs: default_flush_interval(),
}
}
}
#[cfg(feature = "otel")]
#[derive(Debug, Clone)]
pub struct OtlpSettings {
pub endpoint: String,
pub protocol: OtlpProtocol,
pub service_name: String,
}
#[cfg(feature = "otel")]
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum OtlpProtocol {
HttpBinary,
HttpJson,
}
#[cfg(not(feature = "otel"))]
pub struct OtlpSettings;
impl OtlpSettings {
#[must_use]
pub fn endpoint_from_env() -> Option<String> {
for key in [
"OTEL_EXPORTER_OTLP_LOGS_ENDPOINT",
"OTEL_EXPORTER_OTLP_ENDPOINT",
] {
if let Ok(value) = std::env::var(key) {
let trimmed = value.trim();
if !trimmed.is_empty() {
return Some(trimmed.to_string());
}
}
}
None
}
}
#[cfg(feature = "otel")]
impl OtlpSettings {
#[must_use]
pub fn from_env() -> Option<Self> {
let endpoint = Self::endpoint_from_env()?;
let protocol = match std::env::var("OTEL_EXPORTER_OTLP_LOGS_PROTOCOL")
.or_else(|_| std::env::var("OTEL_EXPORTER_OTLP_PROTOCOL"))
.unwrap_or_default()
.trim()
.to_ascii_lowercase()
.as_str()
{
"http/json" => OtlpProtocol::HttpJson,
_ => OtlpProtocol::HttpBinary,
};
let service_name = std::env::var("OTEL_SERVICE_NAME")
.ok()
.map(|s| s.trim().to_string())
.filter(|s| !s.is_empty())
.unwrap_or_else(|| env!("CARGO_PKG_NAME").to_string());
Some(Self {
endpoint,
protocol,
service_name,
})
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(tag = "type", rename_all = "snake_case")]
pub enum TelemetryEvent {
AppStart {
version: String,
os: String,
arch: String,
features: Vec<String>,
},
CommandRun {
command: String,
#[serde(skip_serializing_if = "Option::is_none")]
subcommand: Option<String>,
duration_ms: u64,
success: bool,
},
ModelOperation {
operation: String,
format: String,
size_bucket: String, duration_ms: u64,
success: bool,
},
Conversion {
source_format: String,
target_format: String,
duration_ms: u64,
success: bool,
},
ApiCall {
endpoint: String,
method: String,
status_code: u16,
duration_ms: u64,
},
Error {
error_type: String,
#[serde(skip_serializing_if = "Option::is_none")]
context: Option<String>,
},
FeatureUsed {
feature: String,
#[serde(skip_serializing_if = "Option::is_none")]
detail: Option<String>,
},
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub(crate) struct TelemetryEnvelope {
pub(crate) device_id: String,
pub(crate) session_id: String,
pub(crate) timestamp: u64,
pub(crate) event: TelemetryEvent,
}
pub struct TelemetryClient {
config: TelemetryConfig,
session_id: String,
events: parking_lot::Mutex<Vec<TelemetryEnvelope>>,
enabled: AtomicBool,
}
impl TelemetryClient {
pub fn new(config: TelemetryConfig) -> Self {
let enabled = config.enabled && !Self::is_disabled_by_env();
Self {
enabled: AtomicBool::new(enabled),
session_id: Uuid::new_v4().to_string(),
events: parking_lot::Mutex::new(Vec::new()),
config,
}
}
fn is_disabled_by_env() -> bool {
std::env::var("AIM_TELEMETRY_ENABLED")
.map(|v| v.to_lowercase() == "false" || v == "0")
.unwrap_or(false)
|| std::env::var("AIM_TELEMETRY_DISABLED")
.map(|v| v == "1" || v.to_lowercase() == "true")
.unwrap_or(false)
|| std::env::var("DO_NOT_TRACK")
.map(|v| v == "1" || v.to_lowercase() == "true")
.unwrap_or(false)
}
pub fn disable(&self) {
self.enabled.store(false, Ordering::SeqCst);
}
pub fn enable(&self) {
self.enabled.store(true, Ordering::SeqCst);
}
pub fn is_enabled(&self) -> bool {
self.enabled.load(Ordering::SeqCst)
}
pub fn track(&self, event: TelemetryEvent) {
if !self.is_enabled() {
return;
}
let envelope = TelemetryEnvelope {
device_id: self.config.device_id.clone(),
session_id: self.session_id.clone(),
timestamp: SystemTime::now()
.duration_since(UNIX_EPOCH)
.unwrap_or_default()
.as_secs(),
event,
};
let mut events = self.events.lock();
events.push(envelope);
if events.len() >= self.config.batch_size {
let batch = std::mem::take(&mut *events);
drop(events);
self.send_batch(batch);
}
}
pub fn flush(&self) {
if !self.is_enabled() {
return;
}
let batch = std::mem::take(&mut *self.events.lock());
if !batch.is_empty() {
self.send_batch(batch);
}
}
fn send_batch(&self, events: Vec<TelemetryEnvelope>) {
let endpoint = self.config.endpoint.clone();
std::thread::spawn(move || {
let mut last_error = None;
for attempt in 0..3 {
if attempt > 0 {
std::thread::sleep(Duration::from_millis(100 * (1 << attempt)));
}
let result = match Self::try_send_otlp(&events) {
Some(otlp_result) => otlp_result,
None => Self::send_http_batch(&endpoint, &events),
};
match result {
Ok(()) => {
let _ = Self::flush_local_queue(&endpoint);
return;
}
Err(e) => {
last_error = Some(e);
}
}
}
if let Some(_err) = last_error {
if let Ok(body) = serde_json::to_string(&events) {
let _ = Self::write_to_local_queue(&body);
}
}
});
}
#[cfg(feature = "otel")]
fn try_send_otlp(events: &[TelemetryEnvelope]) -> Option<std::result::Result<(), String>> {
let settings = OtlpSettings::from_env()?;
Some(crate::telemetry_otlp::export(events, &settings))
}
#[cfg(not(feature = "otel"))]
#[allow(clippy::unnecessary_wraps)]
fn try_send_otlp(_events: &[TelemetryEnvelope]) -> Option<std::result::Result<(), String>> {
if OtlpSettings::endpoint_from_env().is_some() {
eprintln!(
"warning: OTEL_EXPORTER_OTLP_ENDPOINT is set, but this binary was \
built without the `otel` feature. Telemetry will not be exported \
over OTLP."
);
}
None
}
fn send_http_batch(
endpoint: &str,
events: &[TelemetryEnvelope],
) -> std::result::Result<(), String> {
use reqwest::blocking::Client;
let client = Client::builder()
.timeout(Duration::from_secs(10))
.build()
.map_err(|e| format!("Failed to create HTTP client: {}", e))?;
let response = client
.post(endpoint)
.header("Content-Type", "application/json")
.header("User-Agent", format!("aim/{}", env!("CARGO_PKG_VERSION")))
.json(events)
.send()
.map_err(|e| format!("HTTP request failed: {}", e))?;
if response.status().is_success() {
Ok(())
} else {
Err(format!("Server returned error: {}", response.status()))
}
}
fn flush_local_queue(endpoint: &str) -> std::result::Result<(), String> {
let queue_file = Self::get_queue_file_path();
if !queue_file.exists() {
return Ok(());
}
let contents =
fs::read_to_string(&queue_file).map_err(|e| format!("Failed to read queue: {}", e))?;
let mut all_sent = true;
let mut remaining_lines = Vec::new();
for line in contents.lines() {
if line.trim().is_empty() {
continue;
}
if let Ok(events) = serde_json::from_str::<Vec<TelemetryEnvelope>>(line) {
if Self::send_http_batch(endpoint, &events).is_err() {
all_sent = false;
remaining_lines.push(line.to_string());
}
}
}
if all_sent {
let _ = fs::remove_file(&queue_file);
} else if !remaining_lines.is_empty() {
use std::io::Write;
if let Ok(mut file) = fs::File::create(&queue_file) {
for line in remaining_lines {
let _ = writeln!(file, "{}", line);
}
}
}
Ok(())
}
fn get_queue_file_path() -> PathBuf {
directories::BaseDirs::new()
.map(|d| {
d.cache_dir()
.join("ai")
.join("telemetry")
.join("events.jsonl")
})
.unwrap_or_else(|| {
PathBuf::from(".")
.join(".cache")
.join("ai")
.join("telemetry")
.join("events.jsonl")
})
}
fn write_to_local_queue(body: &str) -> std::io::Result<()> {
use std::io::Write;
let queue_file = Self::get_queue_file_path();
if let Some(queue_dir) = queue_file.parent() {
fs::create_dir_all(queue_dir)?;
let _ = crate::permissions::restrict_dir(queue_dir);
}
let mut options = fs::OpenOptions::new();
options.create(true).append(true);
crate::permissions::set_create_mode(&mut options);
let mut file = options.open(queue_file)?;
writeln!(file, "{}", body)?;
Ok(())
}
pub fn device_id(&self) -> &str {
&self.config.device_id
}
}
impl Drop for TelemetryClient {
fn drop(&mut self) {
self.flush();
}
}
pub fn init(config: TelemetryConfig) {
let _ = TELEMETRY.set(Arc::new(TelemetryClient::new(config)));
}
pub fn init_default(config_dir: Option<&PathBuf>) -> Result<()> {
let config = load_or_create_config(config_dir)?;
init(config);
Ok(())
}
fn load_or_create_config(config_dir: Option<&PathBuf>) -> Result<TelemetryConfig> {
let config_path = config_dir
.cloned()
.or_else(|| directories::BaseDirs::new().map(|d| d.config_dir().join("ai").join("models")))
.map(|d| d.join("telemetry.yaml"));
if let Some(path) = &config_path {
if path.exists() {
let contents = fs::read_to_string(path)?;
let config: TelemetryConfig = serde_yaml_ng::from_str(&contents)?;
return Ok(config);
}
}
let config = TelemetryConfig::default();
if let Some(path) = config_path {
if let Some(parent) = path.parent() {
fs::create_dir_all(parent)?;
}
let contents = serde_yaml_ng::to_string(&config)?;
fs::write(&path, contents)?;
}
Ok(config)
}
pub fn disable() {
TELEMETRY_DISABLED.store(true, Ordering::SeqCst);
if let Some(client) = TELEMETRY.get() {
client.disable();
}
}
pub fn is_enabled() -> bool {
!TELEMETRY_DISABLED.load(Ordering::SeqCst)
&& TELEMETRY.get().map(|c| c.is_enabled()).unwrap_or(false)
}
pub fn track(event: TelemetryEvent) {
if let Some(client) = TELEMETRY.get() {
client.track(event);
}
}
pub fn flush() {
if let Some(client) = TELEMETRY.get() {
client.flush();
}
}
pub fn track_app_start() {
let features = collect_enabled_features();
track(TelemetryEvent::AppStart {
version: env!("CARGO_PKG_VERSION").to_string(),
os: std::env::consts::OS.to_string(),
arch: std::env::consts::ARCH.to_string(),
features,
});
}
pub fn track_command(command: &str, subcommand: Option<&str>, duration: Duration, success: bool) {
track(TelemetryEvent::CommandRun {
command: command.to_string(),
subcommand: subcommand.map(|s| s.to_string()),
duration_ms: duration.as_millis() as u64,
success,
});
}
pub fn track_model_op(
operation: &str,
format: &str,
size_bytes: u64,
duration: Duration,
success: bool,
) {
let size_bucket = match size_bytes {
0..=10_000_000 => "small", 10_000_001..=100_000_000 => "medium", 100_000_001..=1_000_000_000 => "large", _ => "xlarge", };
track(TelemetryEvent::ModelOperation {
operation: operation.to_string(),
format: format.to_string(),
size_bucket: size_bucket.to_string(),
duration_ms: duration.as_millis() as u64,
success,
});
}
pub fn track_conversion(
source_format: &str,
target_format: &str,
duration: Duration,
success: bool,
) {
track(TelemetryEvent::Conversion {
source_format: source_format.to_string(),
target_format: target_format.to_string(),
duration_ms: duration.as_millis() as u64,
success,
});
}
pub fn track_api_call(endpoint: &str, method: &str, status_code: u16, duration: Duration) {
track(TelemetryEvent::ApiCall {
endpoint: endpoint.to_string(),
method: method.to_string(),
status_code,
duration_ms: duration.as_millis() as u64,
});
}
pub fn track_error(error_type: &str, context: Option<&str>) {
track(TelemetryEvent::Error {
error_type: error_type.to_string(),
context: context.map(|s| s.to_string()),
});
}
pub fn track_feature(feature: &str, detail: Option<&str>) {
track(TelemetryEvent::FeatureUsed {
feature: feature.to_string(),
detail: detail.map(|s| s.to_string()),
});
}
#[allow(unused_mut, clippy::vec_init_then_push)]
fn collect_enabled_features() -> Vec<String> {
let mut features: Vec<String> = vec![];
#[cfg(feature = "api")]
features.push("api".to_string());
#[cfg(feature = "python")]
features.push("python".to_string());
#[cfg(feature = "cloud")]
features.push("cloud".to_string());
features
}
pub struct TrackingTimer {
start: Instant,
command: String,
subcommand: Option<String>,
}
impl TrackingTimer {
pub fn new(command: &str, subcommand: Option<&str>) -> Self {
Self {
start: Instant::now(),
command: command.to_string(),
subcommand: subcommand.map(|s| s.to_string()),
}
}
pub fn finish(self, success: bool) {
track_command(
&self.command,
self.subcommand.as_deref(),
self.start.elapsed(),
success,
);
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_telemetry_config_default() {
let config = TelemetryConfig::default();
assert!(!config.enabled);
assert!(!config.device_id.is_empty());
assert!(config.endpoint.contains("telemetry"));
assert_eq!(config.batch_size, 25);
assert_eq!(config.flush_interval_secs, 300);
}
#[test]
fn test_telemetry_client_disable() {
let config = TelemetryConfig {
enabled: true,
..TelemetryConfig::default()
};
let client = TelemetryClient::new(config);
assert!(client.is_enabled());
client.disable();
assert!(!client.is_enabled());
}
#[test]
fn test_telemetry_client_enable() {
let config = TelemetryConfig {
enabled: false,
..TelemetryConfig::default()
};
let client = TelemetryClient::new(config);
assert!(!client.is_enabled());
client.enable();
assert!(client.is_enabled());
}
#[test]
fn test_telemetry_client_device_id() {
let config = TelemetryConfig::default();
let expected_id = config.device_id.clone();
let client = TelemetryClient::new(config);
assert_eq!(client.device_id(), expected_id);
}
#[test]
fn test_telemetry_client_track_when_disabled() {
let config = TelemetryConfig {
enabled: false,
..TelemetryConfig::default()
};
let client = TelemetryClient::new(config);
client.track(TelemetryEvent::CommandRun {
command: "test".to_string(),
subcommand: None,
duration_ms: 0,
success: true,
});
let events = client.events.lock();
assert!(events.is_empty());
}
#[test]
fn test_telemetry_client_track_when_enabled() {
let config = TelemetryConfig {
enabled: true,
batch_size: 100, ..TelemetryConfig::default()
};
let client = TelemetryClient::new(config);
client.track(TelemetryEvent::CommandRun {
command: "store".to_string(),
subcommand: Some("model".to_string()),
duration_ms: 150,
success: true,
});
let events = client.events.lock();
assert_eq!(events.len(), 1);
assert!(!events[0].device_id.is_empty());
assert!(!events[0].session_id.is_empty());
assert!(events[0].timestamp > 0);
}
#[test]
fn test_telemetry_client_flush_when_disabled() {
let config = TelemetryConfig {
enabled: false,
..TelemetryConfig::default()
};
let client = TelemetryClient::new(config);
client.flush();
}
#[test]
fn test_size_bucket() {
let check_bucket = |size: u64| -> &'static str {
match size {
0..=10_000_000 => "small",
10_000_001..=100_000_000 => "medium",
100_000_001..=1_000_000_000 => "large",
_ => "xlarge",
}
};
assert_eq!(check_bucket(1_000), "small");
assert_eq!(check_bucket(50_000_000), "medium");
assert_eq!(check_bucket(500_000_000), "large");
assert_eq!(check_bucket(2_000_000_000), "xlarge");
}
#[test]
fn test_event_serialization() {
let event = TelemetryEvent::CommandRun {
command: "store".to_string(),
subcommand: None,
duration_ms: 150,
success: true,
};
let json = serde_json::to_string(&event).unwrap();
assert!(json.contains("command_run"));
assert!(json.contains("store"));
}
#[test]
fn test_event_app_start_serialization() {
let event = TelemetryEvent::AppStart {
version: "1.3.0".to_string(),
os: "windows".to_string(),
arch: "x86_64".to_string(),
features: vec!["api".to_string(), "cloud".to_string()],
};
let json = serde_json::to_string(&event).unwrap();
assert!(json.contains("app_start"));
assert!(json.contains("1.3.0"));
}
#[test]
fn test_event_model_operation_serialization() {
let event = TelemetryEvent::ModelOperation {
operation: "store".to_string(),
format: "safetensors".to_string(),
size_bucket: "medium".to_string(),
duration_ms: 3000,
success: true,
};
let json = serde_json::to_string(&event).unwrap();
assert!(json.contains("model_operation"));
}
#[test]
fn test_event_conversion_serialization() {
let event = TelemetryEvent::Conversion {
source_format: "pytorch".to_string(),
target_format: "onnx".to_string(),
duration_ms: 5000,
success: false,
};
let json = serde_json::to_string(&event).unwrap();
assert!(json.contains("conversion"));
assert!(json.contains("false"));
}
#[test]
fn test_event_api_call_serialization() {
let event = TelemetryEvent::ApiCall {
endpoint: "/api/v1/models".to_string(),
method: "GET".to_string(),
status_code: 200,
duration_ms: 50,
};
let json = serde_json::to_string(&event).unwrap();
assert!(json.contains("api_call"));
assert!(json.contains("200"));
}
#[test]
fn test_event_error_serialization() {
let event = TelemetryEvent::Error {
error_type: "CryptoError".to_string(),
context: Some("decryption failed".to_string()),
};
let json = serde_json::to_string(&event).unwrap();
assert!(json.contains("error"));
assert!(json.contains("CryptoError"));
let event2 = TelemetryEvent::Error {
error_type: "IoError".to_string(),
context: None,
};
let json2 = serde_json::to_string(&event2).unwrap();
assert!(!json2.contains("context"));
}
#[test]
fn test_event_feature_used_serialization() {
let event = TelemetryEvent::FeatureUsed {
feature: "cloud_push".to_string(),
detail: Some("s3".to_string()),
};
let json = serde_json::to_string(&event).unwrap();
assert!(json.contains("feature_used"));
let event2 = TelemetryEvent::FeatureUsed {
feature: "rag".to_string(),
detail: None,
};
let json2 = serde_json::to_string(&event2).unwrap();
assert!(!json2.contains("detail"));
}
#[test]
fn test_telemetry_config_serialization_roundtrip() {
let config = TelemetryConfig::default();
let yaml = serde_yaml_ng::to_string(&config).unwrap();
let deserialized: TelemetryConfig = serde_yaml_ng::from_str(&yaml).unwrap();
assert_eq!(deserialized.batch_size, config.batch_size);
assert_eq!(deserialized.flush_interval_secs, config.flush_interval_secs);
}
#[test]
fn test_get_queue_file_path() {
let path = TelemetryClient::get_queue_file_path();
assert!(path.to_string_lossy().contains("telemetry"));
assert!(path.to_string_lossy().contains("events.jsonl"));
}
#[test]
fn test_tracking_timer() {
let timer = TrackingTimer::new("test-cmd", Some("sub"));
assert_eq!(timer.command, "test-cmd");
assert_eq!(timer.subcommand.as_deref(), Some("sub"));
}
#[test]
fn test_tracking_timer_no_subcommand() {
let timer = TrackingTimer::new("list", None);
assert!(timer.subcommand.is_none());
}
#[test]
fn test_collect_enabled_features() {
let features = collect_enabled_features();
assert!(features.len() <= 10);
}
#[test]
fn test_global_is_enabled_without_init() {
assert!(!is_enabled());
}
#[test]
fn test_is_disabled_by_env_default() {
let _ = TelemetryClient::is_disabled_by_env();
}
#[test]
fn test_otlp_endpoint_alone_does_not_enable_telemetry() {
let config = TelemetryConfig::default();
assert!(!config.enabled, "precondition: default is opt-in");
let client = TelemetryClient::new(config);
assert!(
!client.is_enabled(),
"an OTLP endpoint configures where events go, not whether they are \
collected"
);
}
#[test]
fn test_explicit_disable_still_wins_over_otlp_config() {
let config = TelemetryConfig {
enabled: true,
..Default::default()
};
let client = TelemetryClient::new(config);
client.disable();
assert!(
!client.is_enabled(),
"an explicit disable must not be overridden by exporter settings"
);
}
#[test]
fn test_telemetry_client_track_multiple_events() {
let config = TelemetryConfig {
enabled: true,
batch_size: 100,
..TelemetryConfig::default()
};
let client = TelemetryClient::new(config);
client.track(TelemetryEvent::AppStart {
version: "1.3.0".to_string(),
os: "windows".to_string(),
arch: "x86_64".to_string(),
features: vec!["api".to_string()],
});
client.track(TelemetryEvent::CommandRun {
command: "store".to_string(),
subcommand: None,
duration_ms: 100,
success: true,
});
client.track(TelemetryEvent::Error {
error_type: "IoError".to_string(),
context: Some("test context".to_string()),
});
let events = client.events.lock();
assert_eq!(events.len(), 3);
}
#[test]
fn test_telemetry_client_flush_clears_events() {
let config = TelemetryConfig {
enabled: true,
batch_size: 100,
..TelemetryConfig::default()
};
let client = TelemetryClient::new(config);
client.track(TelemetryEvent::FeatureUsed {
feature: "rag".to_string(),
detail: None,
});
assert_eq!(client.events.lock().len(), 1);
client.flush();
std::thread::sleep(std::time::Duration::from_millis(50));
assert_eq!(client.events.lock().len(), 0);
}
#[test]
fn test_telemetry_config_custom_values() {
let config = TelemetryConfig {
enabled: true,
device_id: "custom-id".to_string(),
endpoint: "https://custom.endpoint/v1".to_string(),
batch_size: 10,
flush_interval_secs: 60,
};
assert!(config.enabled);
assert_eq!(config.device_id, "custom-id");
assert_eq!(config.batch_size, 10);
assert_eq!(config.flush_interval_secs, 60);
}
#[test]
fn test_telemetry_envelope_fields() {
let config = TelemetryConfig {
enabled: true,
batch_size: 100,
..TelemetryConfig::default()
};
let client = TelemetryClient::new(config);
client.track(TelemetryEvent::Conversion {
source_format: "pytorch".to_string(),
target_format: "onnx".to_string(),
duration_ms: 2000,
success: true,
});
let events = client.events.lock();
let env = &events[0];
assert!(!env.device_id.is_empty());
assert!(!env.session_id.is_empty());
assert!(env.timestamp > 0);
let json = serde_json::to_string(&env.event).unwrap();
assert!(json.contains("conversion"));
assert!(json.contains("pytorch"));
}
#[test]
fn test_tracking_timer_finish_does_not_panic() {
let timer = TrackingTimer::new("bench-cmd", Some("sub-a"));
std::thread::sleep(std::time::Duration::from_millis(5));
timer.finish(true);
}
#[test]
fn test_tracking_timer_finish_failure() {
let timer = TrackingTimer::new("failing-cmd", None);
timer.finish(false);
}
#[test]
fn test_global_track_noop_without_init() {
track(TelemetryEvent::FeatureUsed {
feature: "test".to_string(),
detail: None,
});
}
#[test]
fn test_global_flush_noop_without_init() {
flush();
}
#[test]
fn test_global_disable_noop_without_init() {
disable();
}
#[test]
fn test_convenience_track_functions_noop() {
track_app_start();
track_command("test", Some("sub"), Duration::from_millis(10), true);
track_command("test", None, Duration::from_millis(5), false);
track_model_op(
"store",
"safetensors",
1_000,
Duration::from_millis(50),
true,
);
track_model_op("get", "gguf", 500_000_000, Duration::from_millis(100), true);
track_model_op(
"store",
"onnx",
50_000_000,
Duration::from_millis(75),
false,
);
track_model_op(
"store",
"pytorch",
2_000_000_000,
Duration::from_millis(200),
true,
);
track_conversion("pytorch", "onnx", Duration::from_secs(5), true);
track_api_call("/models", "GET", 200, Duration::from_millis(30));
track_error("TestError", Some("test context"));
track_error("TestError", None);
track_feature("rag", Some("search"));
track_feature("cloud", None);
}
#[test]
fn test_size_bucket_boundaries() {
let check_bucket = |size: u64| -> &'static str {
match size {
0..=10_000_000 => "small",
10_000_001..=100_000_000 => "medium",
100_000_001..=1_000_000_000 => "large",
_ => "xlarge",
}
};
assert_eq!(check_bucket(0), "small");
assert_eq!(check_bucket(10_000_000), "small"); assert_eq!(check_bucket(10_000_001), "medium"); assert_eq!(check_bucket(100_000_000), "medium"); assert_eq!(check_bucket(100_000_001), "large"); assert_eq!(check_bucket(1_000_000_000), "large"); assert_eq!(check_bucket(1_000_000_001), "xlarge"); }
#[test]
fn test_telemetry_envelope_serialization_roundtrip() {
let envelope = TelemetryEnvelope {
device_id: "dev-1".to_string(),
session_id: "sess-1".to_string(),
timestamp: 1700000000,
event: TelemetryEvent::ApiCall {
endpoint: "/health".to_string(),
method: "GET".to_string(),
status_code: 200,
duration_ms: 5,
},
};
let json = serde_json::to_string(&envelope).unwrap();
let d: TelemetryEnvelope = serde_json::from_str(&json).unwrap();
assert_eq!(d.device_id, "dev-1");
assert_eq!(d.session_id, "sess-1");
assert_eq!(d.timestamp, 1700000000);
}
#[test]
fn test_write_to_local_queue_and_read() {
let dir = tempfile::tempdir().unwrap();
let queue_path = dir.path().join("events.jsonl");
use std::io::Write;
let body = r#"[{"device_id":"d","session_id":"s","timestamp":1,"event":{"type":"feature_used","feature":"test"}}]"#;
let mut f = std::fs::File::create(&queue_path).unwrap();
writeln!(f, "{}", body).unwrap();
drop(f);
let contents = std::fs::read_to_string(&queue_path).unwrap();
assert!(contents.contains("feature_used"));
for line in contents.lines() {
if !line.trim().is_empty() {
let batch: Vec<TelemetryEnvelope> = serde_json::from_str(line).unwrap();
assert_eq!(batch.len(), 1);
}
}
}
#[test]
fn test_load_or_create_config_new() {
let dir = tempfile::tempdir().unwrap();
let config_dir = dir.path().to_path_buf();
let config = load_or_create_config(Some(&config_dir)).unwrap();
assert!(!config.enabled); assert!(!config.device_id.is_empty());
let config_path = config_dir.join("telemetry.yaml");
assert!(config_path.exists());
}
#[test]
fn test_load_or_create_config_existing() {
let dir = tempfile::tempdir().unwrap();
let config_dir = dir.path().to_path_buf();
let config_path = config_dir.join("telemetry.yaml");
let custom = TelemetryConfig {
enabled: true,
device_id: "existing-device".to_string(),
batch_size: 50,
..TelemetryConfig::default()
};
let yaml = serde_yaml_ng::to_string(&custom).unwrap();
std::fs::write(&config_path, &yaml).unwrap();
let loaded = load_or_create_config(Some(&config_dir)).unwrap();
assert!(loaded.enabled);
assert_eq!(loaded.device_id, "existing-device");
assert_eq!(loaded.batch_size, 50);
}
#[test]
fn test_init_and_track() {
let config = TelemetryConfig {
enabled: false,
..TelemetryConfig::default()
};
init(config);
}
#[test]
fn test_default_helper_functions() {
assert!(!generate_device_id().is_empty());
assert!(default_endpoint().contains("telemetry"));
assert_eq!(default_batch_size(), 25);
assert_eq!(default_flush_interval(), 300);
}
}