mockforge_core/ab_testing/
analytics.rs1use crate::ab_testing::types::{ABTestConfig, VariantAnalytics};
6use serde::{Deserialize, Serialize};
7use std::collections::HashMap;
8
9#[derive(Debug, Clone, Serialize, Deserialize)]
11pub struct ABTestReport {
12 pub test_config: ABTestConfig,
14 pub variant_analytics: HashMap<String, VariantAnalytics>,
16 pub total_requests: u64,
18 pub start_time: Option<chrono::DateTime<chrono::Utc>>,
20 pub end_time: Option<chrono::DateTime<chrono::Utc>>,
22 pub is_active: bool,
24}
25
26impl ABTestReport {
27 pub fn new(
29 test_config: ABTestConfig,
30 variant_analytics: HashMap<String, VariantAnalytics>,
31 ) -> Self {
32 let total_requests: u64 = variant_analytics.values().map(|a| a.request_count).sum();
33 let is_active = test_config.enabled
34 && test_config.start_time.is_none_or(|t| t <= chrono::Utc::now())
35 && test_config.end_time.is_none_or(|t| t >= chrono::Utc::now());
36
37 Self {
38 test_config,
39 variant_analytics,
40 total_requests,
41 start_time: None,
42 end_time: None,
43 is_active,
44 }
45 }
46
47 pub fn best_variant(&self) -> Option<&VariantAnalytics> {
49 self.variant_analytics.values().max_by(|a, b| {
50 a.success_rate()
51 .partial_cmp(&b.success_rate())
52 .unwrap_or(std::cmp::Ordering::Equal)
53 })
54 }
55
56 pub fn worst_variant(&self) -> Option<&VariantAnalytics> {
58 self.variant_analytics.values().min_by(|a, b| {
59 a.success_rate()
60 .partial_cmp(&b.success_rate())
61 .unwrap_or(std::cmp::Ordering::Equal)
62 })
63 }
64
65 pub fn statistical_significance(&self) -> f64 {
71 if self.variant_analytics.len() < 2 {
72 return 0.0;
73 }
74
75 let variants: Vec<&VariantAnalytics> = self.variant_analytics.values().collect();
76 if variants.len() < 2 {
77 return 0.0;
78 }
79
80 let best = variants
82 .iter()
83 .max_by(|a, b| {
84 a.success_rate()
85 .partial_cmp(&b.success_rate())
86 .unwrap_or(std::cmp::Ordering::Equal)
87 })
88 .unwrap();
89 let worst = variants
90 .iter()
91 .min_by(|a, b| {
92 a.success_rate()
93 .partial_cmp(&b.success_rate())
94 .unwrap_or(std::cmp::Ordering::Equal)
95 })
96 .unwrap();
97
98 let n1 = best.request_count as f64;
99 let n2 = worst.request_count as f64;
100
101 if n1 < 5.0 || n2 < 5.0 {
103 return 0.0;
104 }
105
106 let p1 = best.success_rate();
107 let p2 = worst.success_rate();
108
109 let pooled = (best.success_count as f64 + worst.success_count as f64) / (n1 + n2);
111
112 if pooled <= 0.0 || pooled >= 1.0 {
114 return 0.0;
115 }
116
117 let se = (pooled * (1.0 - pooled) * (1.0 / n1 + 1.0 / n2)).sqrt();
119 if se < f64::EPSILON {
120 return 0.0;
121 }
122
123 let z = (p1 - p2).abs() / se;
125
126 let confidence = z_to_confidence(z) * 100.0;
129
130 confidence.min(100.0)
131 }
132}
133
134#[derive(Debug, Clone, Serialize, Deserialize)]
136pub struct VariantComparison {
137 pub variant_a_id: String,
139 pub variant_b_id: String,
141 pub success_rate_diff: f64,
143 pub response_time_diff_ms: f64,
145 pub error_rate_diff: f64,
147 pub request_count_diff: i64,
149}
150
151impl VariantComparison {
152 pub fn new(variant_a: &VariantAnalytics, variant_b: &VariantAnalytics) -> Self {
154 Self {
155 variant_a_id: variant_a.variant_id.clone(),
156 variant_b_id: variant_b.variant_id.clone(),
157 success_rate_diff: variant_a.success_rate() - variant_b.success_rate(),
158 response_time_diff_ms: variant_a.avg_response_time_ms - variant_b.avg_response_time_ms,
159 error_rate_diff: variant_a.error_rate() - variant_b.error_rate(),
160 request_count_diff: variant_a.request_count as i64 - variant_b.request_count as i64,
161 }
162 }
163}
164
165fn z_to_confidence(z: f64) -> f64 {
170 let z_abs = z.abs();
172
173 let p = 0.2316419;
175 let b1 = 0.319381530;
176 let b2 = -0.356563782;
177 let b3 = 1.781477937;
178 let b4 = -1.821255978;
179 let b5 = 1.330274429;
180
181 let t = 1.0 / (1.0 + p * z_abs);
182 let t2 = t * t;
183 let t3 = t2 * t;
184 let t4 = t3 * t;
185 let t5 = t4 * t;
186
187 let pdf = (-0.5 * z_abs * z_abs).exp() / (2.0 * std::f64::consts::PI).sqrt();
188 let cdf = 1.0 - pdf * (b1 * t + b2 * t2 + b3 * t3 + b4 * t4 + b5 * t5);
189
190 1.0 - 2.0 * (1.0 - cdf)
192}