use anyhow::Result;
use chrono::Local;
use rusqlite::{Connection, params};
use std::path::PathBuf;
pub struct Analytics {
conn: Connection,
}
impl Analytics {
pub fn new() -> Result<Self> {
let db_path = Self::get_db_path()?;
let conn = Connection::open(db_path)?;
conn.execute(
"CREATE TABLE IF NOT EXISTS commands (
id INTEGER PRIMARY KEY AUTOINCREMENT,
timestamp TEXT NOT NULL,
command TEXT NOT NULL,
success INTEGER NOT NULL,
working_dir TEXT,
session_id TEXT
)",
[],
)?;
conn.execute(
"CREATE TABLE IF NOT EXISTS sessions (
id TEXT PRIMARY KEY,
start_time TEXT NOT NULL,
end_time TEXT,
commands_count INTEGER DEFAULT 0
)",
[],
)?;
conn.execute(
"CREATE TABLE IF NOT EXISTS daily_stats (
date TEXT PRIMARY KEY,
commands_count INTEGER DEFAULT 0,
unique_commands INTEGER DEFAULT 0,
session_count INTEGER DEFAULT 0
)",
[],
)?;
Ok(Self { conn })
}
fn get_db_path() -> Result<PathBuf> {
let data_dir = dirs::data_local_dir()
.ok_or_else(|| anyhow::anyhow!("Could not find local data directory"))?;
let arct_dir = data_dir.join("arct");
if !arct_dir.exists() {
std::fs::create_dir_all(&arct_dir)?;
}
Ok(arct_dir.join("analytics.db"))
}
pub fn record_command(
&self,
command: &str,
success: bool,
working_dir: &str,
session_id: &str,
) -> Result<()> {
let timestamp = Local::now().to_rfc3339();
self.conn.execute(
"INSERT INTO commands (timestamp, command, success, working_dir, session_id)
VALUES (?1, ?2, ?3, ?4, ?5)",
params![timestamp, command, success as i32, working_dir, session_id],
)?;
self.update_daily_stats()?;
Ok(())
}
fn update_daily_stats(&self) -> Result<()> {
let today = Local::now().format("%Y-%m-%d").to_string();
let commands_today: i64 = self.conn.query_row(
"SELECT COUNT(*) FROM commands WHERE DATE(timestamp) = ?1",
params![today],
|row| row.get(0),
)?;
let unique_today: i64 = self.conn.query_row(
"SELECT COUNT(DISTINCT command) FROM commands WHERE DATE(timestamp) = ?1",
params![today],
|row| row.get(0),
)?;
self.conn.execute(
"INSERT OR REPLACE INTO daily_stats (date, commands_count, unique_commands)
VALUES (?1, ?2, ?3)",
params![today, commands_today, unique_today],
)?;
Ok(())
}
pub fn get_total_commands(&self) -> Result<i64> {
let count: i64 = self.conn.query_row(
"SELECT COUNT(*) FROM commands",
[],
|row| row.get(0),
)?;
Ok(count)
}
pub fn get_commands_today(&self) -> Result<i64> {
let today = Local::now().format("%Y-%m-%d").to_string();
let count: i64 = self.conn.query_row(
"SELECT COUNT(*) FROM commands WHERE DATE(timestamp) = ?1",
params![today],
|row| row.get(0),
)?;
Ok(count)
}
pub fn get_unique_commands(&self) -> Result<i64> {
let count: i64 = self.conn.query_row(
"SELECT COUNT(DISTINCT command) FROM commands",
[],
|row| row.get(0),
)?;
Ok(count)
}
pub fn get_top_commands(&self, limit: usize) -> Result<Vec<(String, i64)>> {
let mut stmt = self.conn.prepare(
"SELECT command, COUNT(*) as count
FROM commands
GROUP BY command
ORDER BY count DESC
LIMIT ?1"
)?;
let commands = stmt.query_map(params![limit], |row| {
Ok((row.get(0)?, row.get(1)?))
})?
.collect::<Result<Vec<_>, _>>()?;
Ok(commands)
}
pub fn get_streak(&self) -> Result<i64> {
let mut streak = 0i64;
let mut current_date = Local::now().naive_local().date();
loop {
let date_str = current_date.format("%Y-%m-%d").to_string();
let count: i64 = self.conn.query_row(
"SELECT COUNT(*) FROM commands WHERE DATE(timestamp) = ?1",
params![date_str],
|row| row.get(0),
)?;
if count > 0 {
streak += 1;
current_date = current_date.pred_opt().unwrap_or(current_date);
} else if streak == 0 {
current_date = current_date.pred_opt().unwrap_or(current_date);
let date_str = current_date.format("%Y-%m-%d").to_string();
let yesterday_count: i64 = self.conn.query_row(
"SELECT COUNT(*) FROM commands WHERE DATE(timestamp) = ?1",
params![date_str],
|row| row.get(0),
)?;
if yesterday_count > 0 {
streak = 1;
current_date = current_date.pred_opt().unwrap_or(current_date);
} else {
break;
}
} else {
break;
}
}
Ok(streak)
}
pub fn get_skill_level(&self) -> Result<String> {
let unique = self.get_unique_commands()?;
let _total = self.get_total_commands()?;
let level = if unique < 5 {
"Beginner"
} else if unique < 15 {
"Learning"
} else if unique < 30 {
"Intermediate"
} else if unique < 50 {
"Advanced"
} else {
"Expert"
};
Ok(level.to_string())
}
pub fn get_weekly_activity(&self) -> Result<Vec<(String, i64)>> {
let mut activity = Vec::new();
for days_ago in (0..7).rev() {
let date = Local::now().naive_local().date() - chrono::Duration::days(days_ago);
let date_str = date.format("%Y-%m-%d").to_string();
let count: i64 = self.conn.query_row(
"SELECT COUNT(*) FROM commands WHERE DATE(timestamp) = ?1",
params![date_str],
|row| row.get(0),
).unwrap_or(0);
let day_name = date.format("%a").to_string();
activity.push((day_name, count));
}
Ok(activity)
}
}
#[derive(Debug, Clone)]
pub struct AnalyticsSummary {
pub total_commands: i64,
pub commands_today: i64,
pub unique_commands: i64,
pub streak: i64,
pub skill_level: String,
pub top_commands: Vec<(String, i64)>,
}
impl Analytics {
pub fn get_summary(&self) -> Result<AnalyticsSummary> {
Ok(AnalyticsSummary {
total_commands: self.get_total_commands()?,
commands_today: self.get_commands_today()?,
unique_commands: self.get_unique_commands()?,
streak: self.get_streak()?,
skill_level: self.get_skill_level()?,
top_commands: self.get_top_commands(3)?,
})
}
}