konnektoren_core/analytics/metrics/
success_rate.rs1use super::super::Trend;
2use super::metric::Metric;
3use crate::challenges::ChallengeHistory;
4use crate::challenges::Performance;
5use chrono::{DateTime, Duration, Utc};
6
7#[derive(Clone, PartialEq, Default)]
8pub struct SuccessRateMetric {
9 history: ChallengeHistory,
10 reference_time: DateTime<Utc>,
11}
12
13impl SuccessRateMetric {
14 pub fn new(history: ChallengeHistory) -> Self {
15 let reference_time = history
17 .challenges
18 .iter()
19 .filter_map(|c| c.end_time)
20 .max()
21 .unwrap_or_else(Utc::now);
22
23 Self {
24 history,
25 reference_time,
26 }
27 }
28
29 fn calculate(&self) -> f64 {
30 let total_challenges = self.history.len();
31 if total_challenges == 0 {
32 return 0.0;
33 }
34
35 let successful_challenges = self
36 .history
37 .challenges
38 .iter()
39 .filter(|challenge| {
40 challenge.performance(&challenge.challenge_result) >= 80 })
42 .count();
43
44 (successful_challenges as f64 / total_challenges as f64) * 100.0
45 }
46
47 pub fn get_trend(&self, time_window: Duration) -> Trend {
48 let window_start = self.reference_time - time_window;
49
50 let mut challenges: Vec<_> = self.history.challenges.iter().collect();
52 challenges.sort_by_key(|c| c.start_time);
53
54 let (older_challenges, recent_challenges): (Vec<_>, Vec<_>) = challenges
55 .into_iter()
56 .partition(|c| c.start_time.is_none_or(|t| t < window_start));
57
58 if recent_challenges.is_empty() {
60 return Trend::Stable;
61 }
62
63 let recent_success_count = recent_challenges
65 .iter()
66 .filter(|c| c.performance(&c.challenge_result) >= 80)
67 .count();
68 let recent_success_rate = recent_success_count as f64 / recent_challenges.len() as f64;
69
70 let older_success_rate = if older_challenges.is_empty() {
72 recent_success_rate } else {
74 let older_success_count = older_challenges
75 .iter()
76 .filter(|c| c.performance(&c.challenge_result) >= 80)
77 .count();
78 older_success_count as f64 / older_challenges.len() as f64
79 };
80
81 let trend_value = recent_success_rate - older_success_rate;
83 Trend::from_value(trend_value)
84 }
85}
86
87impl Metric for SuccessRateMetric {
88 fn name(&self) -> &str {
89 "Success Rate"
90 }
91
92 fn value(&self) -> f64 {
93 self.calculate()
94 }
95
96 fn description(&self) -> &str {
97 "The percentage of successful challenges out of all completed challenges."
98 }
99}
100
101#[cfg(test)]
102mod tests {
103 use super::*;
104 use crate::challenges::challenge_type::tests::{
105 BASE_TIMESTAMP, create_successful_challenge, create_unsuccessful_challenge,
106 };
107 use chrono::{Duration, TimeZone, Utc};
108
109 #[test]
110 fn test_success_rate_calculation() {
111 let mut history = ChallengeHistory::new();
112
113 history.add_challenge(create_successful_challenge());
115 history.add_challenge(create_unsuccessful_challenge());
116
117 let metric = SuccessRateMetric::new(history);
118 assert_eq!(
119 metric.value(),
120 50.0,
121 "Should have 50% success rate with one successful and one failed challenge"
122 );
123 }
124
125 #[test]
126 fn test_all_successful() {
127 let mut history = ChallengeHistory::new();
128 history.add_challenge(create_successful_challenge());
129 history.add_challenge(create_successful_challenge());
130
131 let metric = SuccessRateMetric::new(history);
132 assert_eq!(
133 metric.value(),
134 100.0,
135 "Should have 100% success rate with all successful challenges"
136 );
137 }
138
139 #[test]
140 fn test_all_unsuccessful() {
141 let mut history = ChallengeHistory::new();
142 history.add_challenge(create_unsuccessful_challenge());
143 history.add_challenge(create_unsuccessful_challenge());
144
145 let metric = SuccessRateMetric::new(history);
146 assert_eq!(
147 metric.value(),
148 0.0,
149 "Should have 0% success rate with all unsuccessful challenges"
150 );
151 }
152
153 #[test]
154 fn test_trend_calculation() {
155 let mut history = ChallengeHistory::new();
156
157 let mut old_challenge = create_unsuccessful_challenge();
159 old_challenge.start_time = Some(
160 Utc.timestamp_opt(BASE_TIMESTAMP - 3 * 24 * 3600, 0)
161 .unwrap(),
162 );
163 old_challenge.end_time = Some(
164 Utc.timestamp_opt(BASE_TIMESTAMP - 3 * 24 * 3600 + 3600, 0)
165 .unwrap(),
166 );
167 history.add_challenge(old_challenge);
168
169 let mut recent_challenge1 = create_successful_challenge();
171 recent_challenge1.start_time =
172 Some(Utc.timestamp_opt(BASE_TIMESTAMP - 12 * 3600, 0).unwrap());
173 recent_challenge1.end_time =
174 Some(Utc.timestamp_opt(BASE_TIMESTAMP - 11 * 3600, 0).unwrap());
175 history.add_challenge(recent_challenge1);
176
177 let mut recent_challenge2 = create_successful_challenge();
178 recent_challenge2.start_time =
179 Some(Utc.timestamp_opt(BASE_TIMESTAMP - 6 * 3600, 0).unwrap());
180 recent_challenge2.end_time = Some(Utc.timestamp_opt(BASE_TIMESTAMP - 5 * 3600, 0).unwrap());
181 history.add_challenge(recent_challenge2);
182
183 let metric = SuccessRateMetric::new(history);
184 let trend = metric.get_trend(Duration::days(2));
185 assert_eq!(
186 trend,
187 Trend::Improving,
188 "Trend should be improving with recent successful challenges"
189 );
190 }
191
192 #[test]
193 fn test_declining_trend() {
194 let mut history = ChallengeHistory::new();
195
196 let mut old_challenge = create_successful_challenge();
198 old_challenge.start_time = Some(
199 Utc.timestamp_opt(BASE_TIMESTAMP - 3 * 24 * 3600, 0)
200 .unwrap(),
201 );
202 old_challenge.end_time = Some(
203 Utc.timestamp_opt(BASE_TIMESTAMP - 3 * 24 * 3600 + 3600, 0)
204 .unwrap(),
205 );
206 history.add_challenge(old_challenge);
207
208 let mut recent_challenge1 = create_unsuccessful_challenge();
210 recent_challenge1.start_time =
211 Some(Utc.timestamp_opt(BASE_TIMESTAMP - 12 * 3600, 0).unwrap());
212 recent_challenge1.end_time =
213 Some(Utc.timestamp_opt(BASE_TIMESTAMP - 11 * 3600, 0).unwrap());
214 history.add_challenge(recent_challenge1);
215
216 let mut recent_challenge2 = create_unsuccessful_challenge();
217 recent_challenge2.start_time =
218 Some(Utc.timestamp_opt(BASE_TIMESTAMP - 6 * 3600, 0).unwrap());
219 recent_challenge2.end_time = Some(Utc.timestamp_opt(BASE_TIMESTAMP - 5 * 3600, 0).unwrap());
220 history.add_challenge(recent_challenge2);
221
222 let metric = SuccessRateMetric::new(history);
223 let trend = metric.get_trend(Duration::days(2));
224 assert_eq!(
225 trend,
226 Trend::Declining,
227 "Trend should be declining with recent unsuccessful challenges"
228 );
229 }
230
231 #[test]
232 fn test_stable_trend() {
233 let mut history = ChallengeHistory::new();
234 let window = Duration::days(2);
235
236 let mut old_successful = create_successful_challenge();
238 let old_time = BASE_TIMESTAMP - 3 * 24 * 3600;
239 old_successful.start_time = Some(Utc.timestamp_opt(old_time, 0).unwrap());
240 old_successful.end_time = Some(Utc.timestamp_opt(old_time + 3600, 0).unwrap());
241 history.add_challenge(old_successful);
242
243 let mut old_unsuccessful = create_unsuccessful_challenge();
244 old_unsuccessful.start_time = Some(Utc.timestamp_opt(old_time + 4 * 3600, 0).unwrap());
245 old_unsuccessful.end_time = Some(Utc.timestamp_opt(old_time + 5 * 3600, 0).unwrap());
246 history.add_challenge(old_unsuccessful);
247
248 let mut recent_successful = create_successful_challenge();
250 let recent_time = BASE_TIMESTAMP - 12 * 3600;
251 recent_successful.start_time = Some(Utc.timestamp_opt(recent_time, 0).unwrap());
252 recent_successful.end_time = Some(Utc.timestamp_opt(recent_time + 3600, 0).unwrap());
253 history.add_challenge(recent_successful);
254
255 let mut recent_unsuccessful = create_unsuccessful_challenge();
256 recent_unsuccessful.start_time =
257 Some(Utc.timestamp_opt(recent_time + 4 * 3600, 0).unwrap());
258 recent_unsuccessful.end_time = Some(Utc.timestamp_opt(recent_time + 5 * 3600, 0).unwrap());
259 history.add_challenge(recent_unsuccessful);
260
261 let metric = SuccessRateMetric::new(history);
262
263 let trend = metric.get_trend(window);
264 assert_eq!(
265 trend,
266 Trend::Stable,
267 "Trend should be stable with same success ratio"
268 );
269 }
270
271 #[test]
272 fn test_empty_history() {
273 let history = ChallengeHistory::new();
274 let metric = SuccessRateMetric::new(history);
275 assert_eq!(
276 metric.value(),
277 0.0,
278 "Empty history should have 0% success rate"
279 );
280 assert_eq!(
281 metric.get_trend(Duration::days(7)),
282 Trend::Stable,
283 "Empty history should have stable trend"
284 );
285 }
286}