1use serde::{Deserialize, Serialize};
9
10pub const ANALYTICS_JSONL_SCHEMA_VERSION: u32 = 1;
12
13#[derive(Debug, Clone, Copy, Default, Serialize, Deserialize, PartialEq, Eq)]
15#[serde(rename_all = "snake_case")]
16pub enum WorkspaceLabelMode {
17 #[default]
19 FullPath,
20 Hashed,
22 BasenameOnly,
24}
25
26impl WorkspaceLabelMode {
27 pub fn parse(value: &str) -> anyhow::Result<Self> {
28 match value {
29 "full_path" => Ok(Self::FullPath),
30 "hashed" => Ok(Self::Hashed),
31 "basename_only" => Ok(Self::BasenameOnly),
32 other => anyhow::bail!(
33 "unknown workspace label mode {other:?}; expected full_path, hashed, or \
34 basename_only"
35 ),
36 }
37 }
38
39 pub fn label(&self, workspace: &str) -> (String, String) {
41 match self {
42 WorkspaceLabelMode::FullPath => (workspace.to_string(), workspace.to_string()),
43 WorkspaceLabelMode::Hashed => {
44 let hash = fnv1a(workspace.as_bytes());
45 (hash.clone(), hash)
46 }
47 WorkspaceLabelMode::BasenameOnly => {
48 let basename = workspace
49 .trim_end_matches('/')
50 .rsplit('/')
51 .next()
52 .unwrap_or(workspace)
53 .to_string();
54 (fnv1a(workspace.as_bytes()), basename)
55 }
56 }
57 }
58}
59
60pub(crate) fn fnv1a(data: &[u8]) -> String {
61 let mut hash: u64 = 0xcbf2_9ce4_8422_2325;
62 for byte in data {
63 hash ^= u64::from(*byte);
64 hash = hash.wrapping_mul(0x0000_0100_0000_01b3);
65 }
66 format!("{hash:016x}")
67}
68
69#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
70#[serde(rename_all = "camelCase")]
71pub struct SessionRecord {
72 pub thread_id: String,
73 pub workspace_key: Option<String>,
74 pub workspace_label: Option<String>,
75 pub provider: Option<String>,
76 pub model: Option<String>,
77 pub created_at_ms: i64,
79 pub updated_at_ms: i64,
80}
81
82#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
83#[serde(rename_all = "camelCase")]
84pub struct TurnRecord {
85 pub thread_id: String,
86 pub turn_id: String,
87 pub provider: Option<String>,
88 pub model: Option<String>,
89 pub runtime_profile: Option<String>,
90 pub started_at_ms: Option<i64>,
91 pub completed_at_ms: Option<i64>,
92 pub status: String,
94 pub error_kind: Option<String>,
95}
96
97#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
98#[serde(rename_all = "camelCase")]
99pub struct TokenUsageRecord {
100 pub thread_id: String,
101 pub turn_id: String,
102 pub provider: Option<String>,
103 pub model: Option<String>,
104 pub recorded_at_ms: i64,
105 pub prompt_tokens: u32,
106 pub completion_tokens: u32,
107 pub total_tokens: u32,
108 pub cached_prompt_tokens: u32,
109}
110
111#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
112#[serde(rename_all = "camelCase")]
113pub struct ToolCallRecord {
114 pub thread_id: String,
115 pub turn_id: String,
116 pub tool_id: String,
117 pub tool_name: Option<String>,
118 pub started_at_ms: Option<i64>,
119 pub completed_at_ms: Option<i64>,
120 pub duration_ms: Option<i64>,
121 pub status: String,
123 pub is_error: bool,
124}
125
126#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq)]
129#[serde(rename_all = "camelCase")]
130pub struct StatsFilter {
131 pub since_ms: Option<i64>,
132 pub until_ms: Option<i64>,
133 pub workspace_key: Option<String>,
134 pub provider: Option<String>,
135 pub model: Option<String>,
136 pub thread_id: Option<String>,
137 pub tool_name: Option<String>,
138 pub min_calls: Option<u64>,
139 pub limit: Option<u64>,
142}
143
144#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
145#[serde(rename_all = "camelCase")]
146pub struct ToolSummary {
147 pub tool_name: String,
148 pub call_count: u64,
149 pub error_count: u64,
150 pub error_rate: f64,
151 pub total_duration_ms: i64,
152 pub avg_duration_ms: Option<f64>,
153 pub p50_duration_ms: Option<i64>,
154 pub p95_duration_ms: Option<i64>,
155 pub p99_duration_ms: Option<i64>,
156}
157
158#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
159#[serde(rename_all = "camelCase")]
160pub struct TokenSummaryRow {
161 pub group: String,
164 pub prompt_tokens: u64,
165 pub completion_tokens: u64,
166 pub total_tokens: u64,
167 pub cached_prompt_tokens: u64,
168 pub turn_count: u64,
169}
170
171#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
172#[serde(rename_all = "snake_case")]
173pub enum TokenGroup {
174 Day,
175 Session,
176 Provider,
177 Model,
178 Workspace,
179}
180
181impl TokenGroup {
182 pub fn parse(value: &str) -> anyhow::Result<Self> {
183 match value {
184 "day" => Ok(Self::Day),
185 "session" => Ok(Self::Session),
186 "provider" => Ok(Self::Provider),
187 "model" => Ok(Self::Model),
188 "workspace" => Ok(Self::Workspace),
189 other => anyhow::bail!(
190 "unknown token grouping {other:?}; expected day, session, provider, model, or \
191 workspace"
192 ),
193 }
194 }
195}
196
197#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
198#[serde(rename_all = "camelCase")]
199pub struct SessionSummary {
200 pub thread_id: String,
201 pub workspace_label: Option<String>,
202 pub provider: Option<String>,
203 pub model: Option<String>,
204 pub turn_count: u64,
205 pub tool_call_count: u64,
206 pub tool_error_count: u64,
207 pub total_tokens: u64,
208 pub total_tool_duration_ms: i64,
209 pub first_activity_ms: Option<i64>,
210 pub last_activity_ms: Option<i64>,
211}
212
213#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
214#[serde(rename_all = "camelCase")]
215pub struct UsageSummary {
216 pub turn_count: u64,
217 pub completed_turn_count: u64,
218 pub failed_turn_count: u64,
219 pub tool_call_count: u64,
220 pub tool_error_count: u64,
221 pub prompt_tokens: u64,
222 pub completion_tokens: u64,
223 pub total_tokens: u64,
224 pub cached_prompt_tokens: u64,
225 pub session_count: u64,
226 pub most_called_tool: Option<String>,
227}
228
229#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
230#[serde(rename_all = "camelCase")]
231pub struct DailyRollupRow {
232 pub day: String,
233 pub workspace_key: Option<String>,
234 pub provider: Option<String>,
235 pub model: Option<String>,
236 pub tool_name: Option<String>,
237 pub call_count: u64,
238 pub error_count: u64,
239 pub total_duration_ms: i64,
240 pub p50_duration_ms: Option<i64>,
241 pub p95_duration_ms: Option<i64>,
242 pub p99_duration_ms: Option<i64>,
243 pub prompt_tokens: u64,
244 pub completion_tokens: u64,
245 pub total_tokens: u64,
246 pub cached_prompt_tokens: u64,
247}