use crate::ab_testing::types::{ABTestConfig, VariantAnalytics};
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ABTestReport {
pub test_config: ABTestConfig,
pub variant_analytics: HashMap<String, VariantAnalytics>,
pub total_requests: u64,
pub start_time: Option<chrono::DateTime<chrono::Utc>>,
pub end_time: Option<chrono::DateTime<chrono::Utc>>,
pub is_active: bool,
}
impl ABTestReport {
pub fn new(
test_config: ABTestConfig,
variant_analytics: HashMap<String, VariantAnalytics>,
) -> Self {
let total_requests: u64 = variant_analytics.values().map(|a| a.request_count).sum();
let is_active = test_config.enabled
&& test_config.start_time.is_none_or(|t| t <= chrono::Utc::now())
&& test_config.end_time.is_none_or(|t| t >= chrono::Utc::now());
Self {
test_config,
variant_analytics,
total_requests,
start_time: None,
end_time: None,
is_active,
}
}
pub fn best_variant(&self) -> Option<&VariantAnalytics> {
self.variant_analytics.values().max_by(|a, b| {
a.success_rate()
.partial_cmp(&b.success_rate())
.unwrap_or(std::cmp::Ordering::Equal)
})
}
pub fn worst_variant(&self) -> Option<&VariantAnalytics> {
self.variant_analytics.values().min_by(|a, b| {
a.success_rate()
.partial_cmp(&b.success_rate())
.unwrap_or(std::cmp::Ordering::Equal)
})
}
pub fn statistical_significance(&self) -> f64 {
if self.variant_analytics.len() < 2 {
return 0.0;
}
let variants: Vec<&VariantAnalytics> = self.variant_analytics.values().collect();
if variants.len() < 2 {
return 0.0;
}
let best = variants
.iter()
.max_by(|a, b| {
a.success_rate()
.partial_cmp(&b.success_rate())
.unwrap_or(std::cmp::Ordering::Equal)
})
.unwrap();
let worst = variants
.iter()
.min_by(|a, b| {
a.success_rate()
.partial_cmp(&b.success_rate())
.unwrap_or(std::cmp::Ordering::Equal)
})
.unwrap();
let n1 = best.request_count as f64;
let n2 = worst.request_count as f64;
if n1 < 5.0 || n2 < 5.0 {
return 0.0;
}
let p1 = best.success_rate();
let p2 = worst.success_rate();
let pooled = (best.success_count as f64 + worst.success_count as f64) / (n1 + n2);
if pooled <= 0.0 || pooled >= 1.0 {
return 0.0;
}
let se = (pooled * (1.0 - pooled) * (1.0 / n1 + 1.0 / n2)).sqrt();
if se < f64::EPSILON {
return 0.0;
}
let z = (p1 - p2).abs() / se;
let confidence = z_to_confidence(z) * 100.0;
confidence.min(100.0)
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct VariantComparison {
pub variant_a_id: String,
pub variant_b_id: String,
pub success_rate_diff: f64,
pub response_time_diff_ms: f64,
pub error_rate_diff: f64,
pub request_count_diff: i64,
}
impl VariantComparison {
pub fn new(variant_a: &VariantAnalytics, variant_b: &VariantAnalytics) -> Self {
Self {
variant_a_id: variant_a.variant_id.clone(),
variant_b_id: variant_b.variant_id.clone(),
success_rate_diff: variant_a.success_rate() - variant_b.success_rate(),
response_time_diff_ms: variant_a.avg_response_time_ms - variant_b.avg_response_time_ms,
error_rate_diff: variant_a.error_rate() - variant_b.error_rate(),
request_count_diff: variant_a.request_count as i64 - variant_b.request_count as i64,
}
}
}
fn z_to_confidence(z: f64) -> f64 {
let z_abs = z.abs();
let p = 0.2316419;
let b1 = 0.319381530;
let b2 = -0.356563782;
let b3 = 1.781477937;
let b4 = -1.821255978;
let b5 = 1.330274429;
let t = 1.0 / (1.0 + p * z_abs);
let t2 = t * t;
let t3 = t2 * t;
let t4 = t3 * t;
let t5 = t4 * t;
let pdf = (-0.5 * z_abs * z_abs).exp() / (2.0 * std::f64::consts::PI).sqrt();
let cdf = 1.0 - pdf * (b1 * t + b2 * t2 + b3 * t3 + b4 * t4 + b5 * t5);
1.0 - 2.0 * (1.0 - cdf)
}