1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
//! Stats endpoint.
//!
//! - Ref: <https://wakatime.com/developers#stats>
//! - Last checked : 2026-05-15
use std::collections::HashMap;
use chrono::{DateTime, NaiveDate, Utc};
use serde::{Deserialize, Serialize};
use crate::{ErrorMessage, WakapiClient, WakapiError};
use super::summaries::{
CategoryStats, DependencyStats, EditorStats, LanguageStats, MachineStats, OsStats, ProjectStats,
};
/// Request parameters for the Stats endpoint.
///
/// Ref: <https://wakatime.com/developers#stats>
pub struct StatsParams {
url: StatsParamsUrl,
params: StatsParamsParams,
}
struct StatsParamsUrl {
/// optional - range Optional range can be a YYYY year, YYYY-MM month, or one of last_7_days, last_30_days, last_6_months, last_year, or all_time
range: Option<String>,
}
#[derive(Serialize, Default)]
struct StatsParamsParams {
/// optional - The keystroke timeout value used to calculate these stats. Defaults the the user's keystroke timeout value.
timeout: Option<usize>,
/// optional - The writes_only value used to calculate these stats. Defaults to the user's writes_only setting.
writes_only: Option<bool>,
}
impl StatsParams {
/// Optional range can be a YYYY year, YYYY-MM month, or one of last_7_days, last_30_days, last_6_months, last_year, or all_time. When range isn’t present, the user’s public profile range is used.
pub fn from_range(range: &str) -> StatsParams {
StatsParams {
url: StatsParamsUrl {
range: Some(range.to_string()),
},
params: StatsParamsParams {
timeout: None,
writes_only: None,
},
}
}
pub fn timeout(mut self, timeout: usize) -> StatsParams {
self.params.timeout = Some(timeout);
self
}
pub fn writes_only(mut self, writes_only: bool) -> StatsParams {
self.params.writes_only = Some(writes_only);
self
}
}
/// Stats endpoint.
///
/// Ref: <https://wakatime.com/developers#stats>
#[derive(Deserialize, Debug)]
pub struct Stats {
/// data
pub data: StatsData,
}
#[derive(Deserialize, Debug)]
pub struct StatsData {
/// total coding activity, excluding "Other" language, as seconds for the given range of time
pub total_seconds: f64,
/// total coding activity as seconds for the given range of time
pub total_seconds_including_other_language: f64,
/// total coding activity, excluding "Other" language, as human readable string
pub human_readable_total: String,
/// total coding activity as human readable string
pub human_readable_total_including_other_language: String,
/// average coding activity per day as seconds for the given range of time, excluding Other language
pub daily_average: f64,
/// average coding activity per day as seconds for the given range of time
pub daily_average_including_other_language: f64,
/// daily average as human readable string, excluding Other language
pub human_readable_daily_average: String,
/// daily average as human readable string
pub human_readable_daily_average_including_other_language: String,
/// number of lines added by GenAI
pub ai_additions: Option<i64>,
/// number of lines removed by GenAI
pub ai_deletions: Option<i64>,
/// number of lines added by old-school typing
pub human_additions: Option<i64>,
/// number of lines removed by old-school typing
pub human_deletions: Option<i64>,
/// number of lines added or removed per GenAI agent
pub ai_agent_line_changes: Option<HashMap<String, i64>>,
/// total number of lines added or removed by GenAI agents
pub ai_line_changes_total: Option<i64>,
/// estimated USD cost per GenAI agent
pub ai_agent_costs: Option<HashMap<String, f64>>,
/// per-agent breakdown of lines and cost
pub ai_agent_breakdown: Option<Vec<AiAgentBreakdown>>,
/// estimated USD cost for all GenAI agents
pub ai_agent_total_cost: Option<f64>,
/// number of user input tokens used by GenAI tools
pub ai_input_tokens: Option<i64>,
/// number of output tokens used by GenAI tools
pub ai_output_tokens: Option<i64>,
/// average number of characters typed to AI tools per user prompt
pub ai_prompt_length_avg: Option<i64>,
/// sum of characters typed to AI tools
pub ai_prompt_length_sum: Option<i64>,
/// number of AI prompts
pub ai_prompt_events: Option<i64>,
/// categories stats
pub categories: Vec<CategoryStats>,
/// projects stats
pub projects: Vec<ProjectStats>,
/// languages stats
pub languages: Vec<LanguageStats>,
/// editor stats
pub editors: Vec<EditorStats>,
/// operating system stats
pub operating_systems: Vec<OsStats>,
/// dependencies stats
pub dependencies: Vec<DependencyStats>,
/// machine stats
pub machines: Vec<MachineStats>,
/// best day
pub best_day: Option<StatsBestDay>,
/// time range of these stats
pub range: Option<String>,
/// time range as human readable string
pub human_readable_range: Option<String>,
/// number of days in this range with no coding time logged
pub holidays: Option<usize>,
/// number of days in this range
pub days_including_holidays: Option<usize>,
/// number of days in this range excluding days with no coding time logged
pub days_minus_holidays: Option<usize>,
/// status of these stats in the cache
pub status: Option<String>,
/// percent these stats have finished updating in the background
pub percent_calculated: Option<usize>,
/// true if these stats are being updated in the background
pub is_already_updating: bool,
/// true if this response came from cached data; field absent when false
pub is_cached: Option<bool>,
/// true if this user's coding activity is publicly visible
pub is_coding_activity_visible: bool,
/// true if this user's language stats are publicly visible
pub is_language_usage_visible: bool,
/// true if this user's editor stats are publicly visible
pub is_editor_usage_visible: bool,
/// true if this user's category stats are publicly visible
pub is_category_usage_visible: bool,
/// true if this user's operating system stats are publicly visible
pub is_os_usage_visible: bool,
/// true if these stats got stuck while processing and will be recalculated in the background
pub is_stuck: bool,
/// true if these stats include the current day; normally false except range "all_time"
pub is_including_today: bool,
/// true if these stats are up to date
pub is_up_to_date: bool,
/// true if an update is pending for a future time range (undocumented field)
pub is_up_to_date_pending_future: Option<bool>,
/// start of this time range as ISO 8601 UTC datetime
pub start: DateTime<Utc>,
/// end of this time range as ISO 8601 UTC datetime
pub end: DateTime<Utc>,
/// timezone used in Olson Country/Region format
pub timezone: String,
/// value of the user's keystroke timeout setting in minutes
pub timeout: usize,
/// status of the user's writes_only setting
pub writes_only: bool,
/// unique id of this user
pub user_id: String,
/// public username for this user
pub username: String,
/// time when these stats were created in ISO 8601 format
pub created_at: DateTime<Utc>,
/// time when these stats were last updated in ISO 8601 format
pub modified_at: DateTime<Utc>,
}
/// Per-agent breakdown of GenAI lines changed and estimated cost.
#[derive(Deserialize, Debug)]
pub struct AiAgentBreakdown {
/// GenAI agent name
pub name: String,
/// number of lines added or removed by this agent
pub lines: i64,
/// estimated USD cost for this agent
pub cost: f64,
}
#[derive(Deserialize, Debug)]
pub struct StatsBestDay {
/// day with most coding time logged as Date string in YEAR-MONTH-DAY format
pub date: NaiveDate,
/// total coding activity for this day in human readable format
pub text: String,
/// number of seconds of coding activity, including other language, for this day
pub total_seconds: f64,
}
impl Stats {
#[cfg(feature = "blocking")]
/// Fetch the stats for the current user.
pub fn fetch(client: &WakapiClient, params: StatsParams) -> Result<Self, WakapiError> {
let url = if let Some(range) = ¶ms.url.range {
client.build_url(
format!("/api/v1/users/:user/stats/{}", range).as_str(),
Some(serde_url_params::to_string(¶ms.params)?),
)
} else {
client.build_url(
"/api/v1/users/:user/stats",
Some(serde_url_params::to_string(¶ms.params)?),
)
};
// Debug : print url and body text
println!(
"url: {}\nbody: {}",
url,
reqwest::blocking::Client::new()
.get(&url)
.header("Authorization", client.get_auth_header())
.send()?
.text()?
);
let response = reqwest::blocking::Client::new()
.get(&url)
.header("Authorization", client.get_auth_header())
.send()?;
if response.status().is_success() {
let stats: Stats = response.json()?;
Ok(stats)
} else {
let error_message: ErrorMessage = response.json()?;
Err(WakapiError::ResponseError(error_message))
}
}
#[cfg(not(feature = "blocking"))]
/// Fetch the stats for the current user.
pub async fn fetch(client: &WakapiClient, params: StatsParams) -> Result<Self, WakapiError> {
let url = if let Some(range) = ¶ms.url.range {
client.build_url(
format!("/api/v1/users/:user/stats/{}", range).as_str(),
Some(serde_url_params::to_string(¶ms.params)?),
)
} else {
client.build_url(
"/api/v1/users/:user/stats",
Some(serde_url_params::to_string(¶ms.params)?),
)
};
// Debug : print url and body text
println!(
"url: {}\nbody: {}",
url,
reqwest::Client::new()
.get(&url)
.header("Authorization", client.get_auth_header())
.send()
.await?
.text()
.await?
);
let response = reqwest::Client::new()
.get(&url)
.header("Authorization", client.get_auth_header())
.send()
.await?;
if response.status().is_success() {
let stats: Stats = response.json().await?;
Ok(stats)
} else {
let error_message: ErrorMessage = response.json().await?;
Err(WakapiError::ResponseError(error_message))
}
}
}