use serde::{Deserialize, Serialize};
use std::path::PathBuf;
use super::privacy::AnonymizationLevel;
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct TelemetryConfig {
pub enabled: bool,
pub level: TelemetryLevel,
pub storage_path: PathBuf,
pub anonymization: AnonymizationLevel,
pub remote_endpoint: Option<String>,
pub batch_size: usize,
pub flush_interval_secs: u64,
}
impl Default for TelemetryConfig {
fn default() -> Self {
Self {
enabled: false, level: TelemetryLevel::Standard,
storage_path: Self::default_storage_path(),
anonymization: AnonymizationLevel::Medium,
remote_endpoint: None,
batch_size: 100,
flush_interval_secs: 300, }
}
}
impl TelemetryConfig {
fn default_storage_path() -> PathBuf {
dirs::data_local_dir()
.unwrap_or_else(|| PathBuf::from("."))
.join("voirs")
.join("telemetry")
}
pub fn enabled() -> Self {
Self {
enabled: true,
..Default::default()
}
}
pub fn disabled() -> Self {
Self {
enabled: false,
..Default::default()
}
}
pub fn with_level(mut self, level: TelemetryLevel) -> Self {
self.level = level;
self
}
pub fn with_storage_path(mut self, path: PathBuf) -> Self {
self.storage_path = path;
self
}
pub fn with_anonymization(mut self, level: AnonymizationLevel) -> Self {
self.anonymization = level;
self
}
pub fn with_remote_endpoint(mut self, endpoint: String) -> Self {
self.remote_endpoint = Some(endpoint);
self
}
pub fn validate(&self) -> Result<(), String> {
if self.batch_size == 0 {
return Err("Batch size must be greater than 0".to_string());
}
if self.flush_interval_secs == 0 {
return Err("Flush interval must be greater than 0".to_string());
}
if let Some(ref endpoint) = self.remote_endpoint {
if !endpoint.starts_with("http://") && !endpoint.starts_with("https://") {
return Err("Remote endpoint must be a valid HTTP(S) URL".to_string());
}
}
Ok(())
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub enum TelemetryLevel {
Minimal,
Standard,
Detailed,
Debug,
}
impl TelemetryLevel {
pub fn includes_commands(&self) -> bool {
matches!(
self,
TelemetryLevel::Standard | TelemetryLevel::Detailed | TelemetryLevel::Debug
)
}
pub fn includes_performance(&self) -> bool {
matches!(
self,
TelemetryLevel::Standard | TelemetryLevel::Detailed | TelemetryLevel::Debug
)
}
pub fn includes_debug(&self) -> bool {
matches!(self, TelemetryLevel::Debug)
}
pub fn includes_errors(&self) -> bool {
true }
}
impl std::fmt::Display for TelemetryLevel {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
TelemetryLevel::Minimal => write!(f, "minimal"),
TelemetryLevel::Standard => write!(f, "standard"),
TelemetryLevel::Detailed => write!(f, "detailed"),
TelemetryLevel::Debug => write!(f, "debug"),
}
}
}
impl std::str::FromStr for TelemetryLevel {
type Err = String;
fn from_str(s: &str) -> Result<Self, Self::Err> {
match s.to_lowercase().as_str() {
"minimal" | "min" => Ok(TelemetryLevel::Minimal),
"standard" | "std" => Ok(TelemetryLevel::Standard),
"detailed" | "full" => Ok(TelemetryLevel::Detailed),
"debug" | "dbg" => Ok(TelemetryLevel::Debug),
_ => Err(format!("Invalid telemetry level: {}", s)),
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_default_config() {
let config = TelemetryConfig::default();
assert!(!config.enabled);
assert_eq!(config.level, TelemetryLevel::Standard);
assert!(config.remote_endpoint.is_none());
}
#[test]
fn test_enabled_config() {
let config = TelemetryConfig::enabled();
assert!(config.enabled);
}
#[test]
fn test_disabled_config() {
let config = TelemetryConfig::disabled();
assert!(!config.enabled);
}
#[test]
fn test_builder_pattern() {
let config = TelemetryConfig::default()
.with_level(TelemetryLevel::Detailed)
.with_remote_endpoint("https://telemetry.example.com".to_string());
assert_eq!(config.level, TelemetryLevel::Detailed);
assert_eq!(
config.remote_endpoint,
Some("https://telemetry.example.com".to_string())
);
}
#[test]
fn test_config_validation() {
let config = TelemetryConfig::default();
assert!(config.validate().is_ok());
let invalid_config = TelemetryConfig {
batch_size: 0,
..Default::default()
};
assert!(invalid_config.validate().is_err());
}
#[test]
fn test_telemetry_level_includes() {
assert!(TelemetryLevel::Minimal.includes_errors());
assert!(!TelemetryLevel::Minimal.includes_commands());
assert!(!TelemetryLevel::Minimal.includes_debug());
assert!(TelemetryLevel::Standard.includes_commands());
assert!(TelemetryLevel::Standard.includes_performance());
assert!(!TelemetryLevel::Standard.includes_debug());
assert!(TelemetryLevel::Debug.includes_debug());
}
#[test]
fn test_telemetry_level_from_str() {
assert_eq!(
"minimal".parse::<TelemetryLevel>().expect("valid format"),
TelemetryLevel::Minimal
);
assert_eq!(
"standard".parse::<TelemetryLevel>().expect("valid format"),
TelemetryLevel::Standard
);
assert_eq!(
"detailed".parse::<TelemetryLevel>().expect("valid format"),
TelemetryLevel::Detailed
);
assert_eq!(
"debug".parse::<TelemetryLevel>().expect("valid format"),
TelemetryLevel::Debug
);
assert!("invalid".parse::<TelemetryLevel>().is_err());
}
#[test]
fn test_telemetry_level_display() {
assert_eq!(TelemetryLevel::Minimal.to_string(), "minimal");
assert_eq!(TelemetryLevel::Standard.to_string(), "standard");
assert_eq!(TelemetryLevel::Detailed.to_string(), "detailed");
assert_eq!(TelemetryLevel::Debug.to_string(), "debug");
}
}