1use anyhow::Result;
4use chrono::Local;
5use rusqlite::{Connection, params};
6use std::path::PathBuf;
7
8pub struct Analytics {
10 conn: Connection,
11}
12
13impl Analytics {
14 pub fn new() -> Result<Self> {
16 let db_path = Self::get_db_path()?;
17 let conn = Connection::open(db_path)?;
18
19 conn.execute(
21 "CREATE TABLE IF NOT EXISTS commands (
22 id INTEGER PRIMARY KEY AUTOINCREMENT,
23 timestamp TEXT NOT NULL,
24 command TEXT NOT NULL,
25 success INTEGER NOT NULL,
26 working_dir TEXT,
27 session_id TEXT
28 )",
29 [],
30 )?;
31
32 conn.execute(
33 "CREATE TABLE IF NOT EXISTS sessions (
34 id TEXT PRIMARY KEY,
35 start_time TEXT NOT NULL,
36 end_time TEXT,
37 commands_count INTEGER DEFAULT 0
38 )",
39 [],
40 )?;
41
42 conn.execute(
43 "CREATE TABLE IF NOT EXISTS daily_stats (
44 date TEXT PRIMARY KEY,
45 commands_count INTEGER DEFAULT 0,
46 unique_commands INTEGER DEFAULT 0,
47 session_count INTEGER DEFAULT 0
48 )",
49 [],
50 )?;
51
52 Ok(Self { conn })
53 }
54
55 fn get_db_path() -> Result<PathBuf> {
57 let data_dir = dirs::data_local_dir()
58 .ok_or_else(|| anyhow::anyhow!("Could not find local data directory"))?;
59
60 let arct_dir = data_dir.join("arct");
61 if !arct_dir.exists() {
62 std::fs::create_dir_all(&arct_dir)?;
63 }
64
65 Ok(arct_dir.join("analytics.db"))
66 }
67
68 pub fn record_command(
70 &self,
71 command: &str,
72 success: bool,
73 working_dir: &str,
74 session_id: &str,
75 ) -> Result<()> {
76 let timestamp = Local::now().to_rfc3339();
77
78 self.conn.execute(
79 "INSERT INTO commands (timestamp, command, success, working_dir, session_id)
80 VALUES (?1, ?2, ?3, ?4, ?5)",
81 params![timestamp, command, success as i32, working_dir, session_id],
82 )?;
83
84 self.update_daily_stats()?;
86
87 Ok(())
88 }
89
90 fn update_daily_stats(&self) -> Result<()> {
92 let today = Local::now().format("%Y-%m-%d").to_string();
93
94 let commands_today: i64 = self.conn.query_row(
96 "SELECT COUNT(*) FROM commands WHERE DATE(timestamp) = ?1",
97 params![today],
98 |row| row.get(0),
99 )?;
100
101 let unique_today: i64 = self.conn.query_row(
103 "SELECT COUNT(DISTINCT command) FROM commands WHERE DATE(timestamp) = ?1",
104 params![today],
105 |row| row.get(0),
106 )?;
107
108 self.conn.execute(
110 "INSERT OR REPLACE INTO daily_stats (date, commands_count, unique_commands)
111 VALUES (?1, ?2, ?3)",
112 params![today, commands_today, unique_today],
113 )?;
114
115 Ok(())
116 }
117
118 pub fn get_total_commands(&self) -> Result<i64> {
120 let count: i64 = self.conn.query_row(
121 "SELECT COUNT(*) FROM commands",
122 [],
123 |row| row.get(0),
124 )?;
125 Ok(count)
126 }
127
128 pub fn get_commands_today(&self) -> Result<i64> {
130 let today = Local::now().format("%Y-%m-%d").to_string();
131 let count: i64 = self.conn.query_row(
132 "SELECT COUNT(*) FROM commands WHERE DATE(timestamp) = ?1",
133 params![today],
134 |row| row.get(0),
135 )?;
136 Ok(count)
137 }
138
139 pub fn get_unique_commands(&self) -> Result<i64> {
141 let count: i64 = self.conn.query_row(
142 "SELECT COUNT(DISTINCT command) FROM commands",
143 [],
144 |row| row.get(0),
145 )?;
146 Ok(count)
147 }
148
149 pub fn get_top_commands(&self, limit: usize) -> Result<Vec<(String, i64)>> {
151 let mut stmt = self.conn.prepare(
152 "SELECT command, COUNT(*) as count
153 FROM commands
154 GROUP BY command
155 ORDER BY count DESC
156 LIMIT ?1"
157 )?;
158
159 let commands = stmt.query_map(params![limit], |row| {
160 Ok((row.get(0)?, row.get(1)?))
161 })?
162 .collect::<Result<Vec<_>, _>>()?;
163
164 Ok(commands)
165 }
166
167 pub fn get_streak(&self) -> Result<i64> {
169 let mut streak = 0i64;
170 let mut current_date = Local::now().naive_local().date();
171
172 loop {
173 let date_str = current_date.format("%Y-%m-%d").to_string();
174 let count: i64 = self.conn.query_row(
175 "SELECT COUNT(*) FROM commands WHERE DATE(timestamp) = ?1",
176 params![date_str],
177 |row| row.get(0),
178 )?;
179
180 if count > 0 {
181 streak += 1;
182 current_date = current_date.pred_opt().unwrap_or(current_date);
183 } else if streak == 0 {
184 current_date = current_date.pred_opt().unwrap_or(current_date);
186 let date_str = current_date.format("%Y-%m-%d").to_string();
187 let yesterday_count: i64 = self.conn.query_row(
188 "SELECT COUNT(*) FROM commands WHERE DATE(timestamp) = ?1",
189 params![date_str],
190 |row| row.get(0),
191 )?;
192
193 if yesterday_count > 0 {
194 streak = 1;
195 current_date = current_date.pred_opt().unwrap_or(current_date);
196 } else {
197 break;
198 }
199 } else {
200 break;
201 }
202 }
203
204 Ok(streak)
205 }
206
207 pub fn get_skill_level(&self) -> Result<String> {
209 let unique = self.get_unique_commands()?;
210 let _total = self.get_total_commands()?; let level = if unique < 5 {
213 "Beginner"
214 } else if unique < 15 {
215 "Learning"
216 } else if unique < 30 {
217 "Intermediate"
218 } else if unique < 50 {
219 "Advanced"
220 } else {
221 "Expert"
222 };
223
224 Ok(level.to_string())
225 }
226
227 pub fn get_weekly_activity(&self) -> Result<Vec<(String, i64)>> {
229 let mut activity = Vec::new();
230
231 for days_ago in (0..7).rev() {
232 let date = Local::now().naive_local().date() - chrono::Duration::days(days_ago);
233 let date_str = date.format("%Y-%m-%d").to_string();
234
235 let count: i64 = self.conn.query_row(
236 "SELECT COUNT(*) FROM commands WHERE DATE(timestamp) = ?1",
237 params![date_str],
238 |row| row.get(0),
239 ).unwrap_or(0);
240
241 let day_name = date.format("%a").to_string();
242 activity.push((day_name, count));
243 }
244
245 Ok(activity)
246 }
247}
248
249#[derive(Debug, Clone)]
251pub struct AnalyticsSummary {
252 pub total_commands: i64,
253 pub commands_today: i64,
254 pub unique_commands: i64,
255 pub streak: i64,
256 pub skill_level: String,
257 pub top_commands: Vec<(String, i64)>,
258}
259
260impl Analytics {
261 pub fn get_summary(&self) -> Result<AnalyticsSummary> {
263 Ok(AnalyticsSummary {
264 total_commands: self.get_total_commands()?,
265 commands_today: self.get_commands_today()?,
266 unique_commands: self.get_unique_commands()?,
267 streak: self.get_streak()?,
268 skill_level: self.get_skill_level()?,
269 top_commands: self.get_top_commands(3)?,
270 })
271 }
272}