diff --git a/src/core/telemetry.rs b/src/core/telemetry.rs index 8a60d45b29..72d330b3da 100644 --- a/src/core/telemetry.rs +++ b/src/core/telemetry.rs @@ -77,7 +77,9 @@ fn send_ping() -> Result<(), Box> { let install_method = detect_install_method(); // Get stats from tracking DB (single connection for both basic + enriched) - let tracker = tracking::Tracker::new().ok(); + let tracker = tracking::tracking_enabled() + .then(|| tracking::Tracker::new().ok()) + .flatten(); let (commands_24h, top_commands, savings_pct, tokens_saved_24h, tokens_saved_total) = match &tracker { Some(t) => get_stats(t), diff --git a/src/core/tracking.rs b/src/core/tracking.rs index 359e4f6885..7774cf6fa0 100644 --- a/src/core/tracking.rs +++ b/src/core/tracking.rs @@ -35,6 +35,7 @@ use rusqlite::{params, Connection}; use serde::Serialize; use std::ffi::OsString; use std::path::PathBuf; +use std::sync::LazyLock; use std::time::Instant; // ── Project path helpers ── // added: project-scoped tracking support @@ -63,6 +64,24 @@ fn project_filter_params(project_path: Option<&str>) -> (Option, Option< use super::constants::{DEFAULT_HISTORY_DAYS, HISTORY_DB, RTK_DATA_DIR}; +/// Whether persistent command-history tracking is enabled. +/// +/// `RTK_TRACK=0` is a one-shot override for automation and isolated launches. +/// Configuration errors preserve the existing enabled-by-default behavior. +pub(crate) fn tracking_enabled() -> bool { + static ENABLED: LazyLock = LazyLock::new(|| { + if std::env::var("RTK_TRACK").ok().as_deref() == Some("0") { + return false; + } + + crate::core::config::Config::load() + .map(|config| config.tracking.enabled) + .unwrap_or(true) + }); + + *ENABLED +} + /// Main tracking interface for recording and querying command history. /// /// Manages SQLite database connection and provides methods for: @@ -407,6 +426,10 @@ impl Tracker { output_tokens: usize, exec_time_ms: u64, ) -> Result<()> { + if !tracking_enabled() { + return Ok(()); + } + let saved = input_tokens.saturating_sub(output_tokens); let pct = if input_tokens > 0 { (saved as f64 / input_tokens as f64) * 100.0 @@ -469,6 +492,10 @@ impl Tracker { error_message: &str, fallback_succeeded: bool, ) -> Result<()> { + if !tracking_enabled() { + return Ok(()); + } + self.conn.execute( "INSERT INTO parse_failures (timestamp, raw_command, error_message, fallback_succeeded) VALUES (?1, ?2, ?3, ?4)", @@ -1257,6 +1284,10 @@ pub struct ParseFailureSummary { /// Record a parse failure without ever crashing. /// Silently ignores all errors — used in the fallback path. pub fn record_parse_failure_silent(raw_command: &str, error_message: &str, succeeded: bool) { + if !tracking_enabled() { + return; + } + if let Ok(tracker) = Tracker::new() { let _ = tracker.record_parse_failure(raw_command, error_message, succeeded); } @@ -1354,6 +1385,10 @@ impl TimedExecution { /// timer.track("ls -la", "rtk ls", input, output); /// ``` pub fn track(&self, original_cmd: &str, rtk_cmd: &str, input: &str, output: &str) { + if !tracking_enabled() { + return; + } + let elapsed_ms = self.start.elapsed().as_millis() as u64; let input_tokens = estimate_tokens(input); let output_tokens = estimate_tokens(output); @@ -1390,6 +1425,10 @@ impl TimedExecution { /// timer.track_passthrough("git tag", "rtk git tag"); /// ``` pub fn track_passthrough(&self, original_cmd: &str, rtk_cmd: &str) { + if !tracking_enabled() { + return; + } + let elapsed_ms = self.start.elapsed().as_millis() as u64; // input_tokens=0, output_tokens=0 won't dilute savings statistics if let Ok(tracker) = Tracker::new() { diff --git a/tests/tracking_disabled_test.rs b/tests/tracking_disabled_test.rs new file mode 100644 index 0000000000..1426ad2aca --- /dev/null +++ b/tests/tracking_disabled_test.rs @@ -0,0 +1,85 @@ +use std::fs; +use std::path::{Path, PathBuf}; +use std::process::Command; + +fn run_log(root: &Path, tracking_enabled: bool, track_override: Option<&str>) -> PathBuf { + let config_home = root.join("config"); + let data_home = root.join("data"); + let config_dir = config_home.join("rtk"); + fs::create_dir_all(&config_dir).expect("create isolated config directory"); + fs::create_dir_all(&data_home).expect("create isolated data directory"); + fs::write( + config_dir.join("config.toml"), + format!( + "[tracking]\nenabled = {tracking_enabled}\nhistory_days = 90\n\n\ + [telemetry]\nenabled = false\n" + ), + ) + .expect("write isolated config"); + + let input = root.join("input.log"); + fs::write(&input, "repeated line\nrepeated line\n").expect("write input"); + + let mut command = Command::new(env!("CARGO_BIN_EXE_rtk")); + command + .args(["log", input.to_str().expect("UTF-8 temp path")]) + .env("XDG_CONFIG_HOME", &config_home) + .env("XDG_DATA_HOME", &data_home) + .env("RTK_TELEMETRY_DISABLED", "1") + .env_remove("RTK_DB_PATH") + .env_remove("RTK_TRACK"); + if let Some(value) = track_override { + command.env("RTK_TRACK", value); + } + + let output = command.output().expect("run rtk log"); + assert!( + output.status.success(), + "rtk log failed: {}", + String::from_utf8_lossy(&output.stderr) + ); + + data_home.join("rtk").join("history.db") +} + +#[test] +fn disabled_tracking_never_creates_history_database() { + let config_disabled = tempfile::tempdir().expect("config-disabled tempdir"); + let db_path = run_log(config_disabled.path(), false, None); + assert!( + !db_path.exists(), + "tracking.enabled=false created {}", + db_path.display() + ); + let parse_failure = Command::new(env!("CARGO_BIN_EXE_rtk")) + .arg("rtk-command-that-does-not-exist") + .env("XDG_CONFIG_HOME", config_disabled.path().join("config")) + .env("XDG_DATA_HOME", config_disabled.path().join("data")) + .env("RTK_TELEMETRY_DISABLED", "1") + .env_remove("RTK_DB_PATH") + .env_remove("RTK_TRACK") + .output() + .expect("run fallback parse failure"); + assert_eq!(parse_failure.status.code(), Some(127)); + assert!( + !db_path.exists(), + "disabled parse-failure tracking created {}", + db_path.display() + ); + + let env_disabled = tempfile::tempdir().expect("env-disabled tempdir"); + let db_path = run_log(env_disabled.path(), true, Some("0")); + assert!( + !db_path.exists(), + "RTK_TRACK=0 created {}", + db_path.display() + ); + + let enabled = tempfile::tempdir().expect("enabled tempdir"); + let db_path = run_log(enabled.path(), true, None); + assert!( + db_path.is_file(), + "enabled tracking did not create {}", + db_path.display() + ); +}