coding_agent_search/analytics/
derive.rs1use super::types::{DerivedMetrics, UsageBucket};
7
8pub fn compute_derived(bucket: &UsageBucket) -> DerivedMetrics {
10 let api_coverage_pct = safe_pct(bucket.api_coverage_message_count, bucket.message_count);
11
12 let api_tokens_per_assistant_msg =
13 safe_div(bucket.api_tokens_total, bucket.assistant_message_count);
14
15 let content_tokens_per_user_msg =
16 safe_div(bucket.content_tokens_est_total, bucket.user_message_count);
17
18 let tool_calls_per_1k_api_tokens = if bucket.api_tokens_total > 0 {
19 Some(bucket.tool_call_count as f64 / (bucket.api_tokens_total as f64 / 1000.0))
20 } else {
21 None
22 };
23
24 let tool_calls_per_1k_content_tokens = if bucket.content_tokens_est_total > 0 {
25 Some(bucket.tool_call_count as f64 / (bucket.content_tokens_est_total as f64 / 1000.0))
26 } else {
27 None
28 };
29
30 let plan_message_pct = if bucket.message_count > 0 {
31 Some((bucket.plan_message_count as f64 / bucket.message_count as f64) * 100.0)
32 } else {
33 None
34 };
35
36 let plan_token_share_content = safe_div(
37 bucket.plan_content_tokens_est_total,
38 bucket.content_tokens_est_total,
39 );
40 let plan_token_share_api = safe_div(bucket.plan_api_tokens_total, bucket.api_tokens_total);
41
42 DerivedMetrics {
43 api_coverage_pct,
44 api_tokens_per_assistant_msg,
45 content_tokens_per_user_msg,
46 tool_calls_per_1k_api_tokens,
47 tool_calls_per_1k_content_tokens,
48 plan_message_pct,
49 plan_token_share_content,
50 plan_token_share_api,
51 }
52}
53
54pub fn safe_pct(numerator: i64, denominator: i64) -> f64 {
58 if denominator == 0 {
59 0.0
60 } else {
61 let pct = (numerator as f64 / denominator as f64) * 100.0;
62 (pct * 100.0).round() / 100.0
63 }
64}
65
66pub fn safe_div(numerator: i64, denominator: i64) -> Option<f64> {
68 if denominator == 0 {
69 None
70 } else {
71 Some(numerator as f64 / denominator as f64)
72 }
73}
74
75pub fn safe_div_f64(numerator: f64, denominator: i64) -> Option<f64> {
78 if denominator == 0 {
79 None
80 } else {
81 Some(numerator / denominator as f64)
82 }
83}
84
85#[cfg(test)]
90mod tests {
91 use super::*;
92
93 #[test]
94 fn safe_div_zero_denominator() {
95 assert_eq!(safe_div(100, 0), None);
96 }
97
98 #[test]
99 fn safe_div_normal() {
100 assert_eq!(safe_div(100, 50), Some(2.0));
101 }
102
103 #[test]
104 fn safe_div_f64_zero_denominator() {
105 assert_eq!(safe_div_f64(1.50, 0), None);
106 }
107
108 #[test]
109 fn safe_div_f64_normal() {
110 let result = safe_div_f64(3.0, 2);
111 assert_eq!(result, Some(1.5));
112 }
113
114 #[test]
115 fn safe_pct_zero_denominator() {
116 assert_eq!(safe_pct(50, 0), 0.0);
117 }
118
119 #[test]
120 fn safe_pct_normal() {
121 let pct = safe_pct(75, 100);
122 assert!((pct - 75.0).abs() < 0.01);
123 }
124
125 #[test]
126 fn safe_pct_rounding() {
127 let pct = safe_pct(1, 3);
129 assert!((pct - 33.33).abs() < 0.01);
130 }
131
132 #[test]
133 fn compute_derived_empty_bucket() {
134 let bucket = UsageBucket::default();
135 let d = compute_derived(&bucket);
136 assert_eq!(d.api_coverage_pct, 0.0);
137 assert_eq!(d.api_tokens_per_assistant_msg, None);
138 assert_eq!(d.content_tokens_per_user_msg, None);
139 assert_eq!(d.tool_calls_per_1k_api_tokens, None);
140 assert_eq!(d.tool_calls_per_1k_content_tokens, None);
141 assert_eq!(d.plan_message_pct, None);
142 assert_eq!(d.plan_token_share_content, None);
143 assert_eq!(d.plan_token_share_api, None);
144 }
145
146 #[test]
147 fn compute_derived_realistic_bucket() {
148 let bucket = UsageBucket {
149 message_count: 100,
150 user_message_count: 50,
151 assistant_message_count: 50,
152 tool_call_count: 10,
153 plan_message_count: 5,
154 plan_content_tokens_est_total: 2_500,
155 plan_api_tokens_total: 3_000,
156 api_coverage_message_count: 80,
157 content_tokens_est_total: 50_000,
158 api_tokens_total: 60_000,
159 estimated_cost_usd: 3.00,
160 ..Default::default()
161 };
162 let d = compute_derived(&bucket);
163 assert!((d.api_coverage_pct - 80.0).abs() < 0.01);
164 assert_eq!(d.api_tokens_per_assistant_msg, Some(1200.0));
165 assert_eq!(d.content_tokens_per_user_msg, Some(1000.0));
166 assert!(d.tool_calls_per_1k_api_tokens.is_some());
167 assert!(d.tool_calls_per_1k_content_tokens.is_some());
168 assert!((d.plan_message_pct.unwrap() - 5.0).abs() < 0.01);
169 assert_eq!(d.plan_token_share_content, Some(0.05));
170 assert_eq!(d.plan_token_share_api, Some(0.05));
171 }
172
173 #[test]
174 fn no_nan_or_infinity() {
175 let bucket = UsageBucket {
177 message_count: 0,
178 api_tokens_total: 0,
179 content_tokens_est_total: 0,
180 ..Default::default()
181 };
182 let d = compute_derived(&bucket);
183 assert!(!d.api_coverage_pct.is_nan());
184 assert!(!d.api_coverage_pct.is_infinite());
185 }
186}