Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion src/core/telemetry.rs
Original file line number Diff line number Diff line change
Expand Up @@ -77,7 +77,9 @@ fn send_ping() -> Result<(), Box<dyn std::error::Error>> {
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),
Expand Down
39 changes: 39 additions & 0 deletions src/core/tracking.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -63,6 +64,24 @@ fn project_filter_params(project_path: Option<&str>) -> (Option<String>, 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<bool> = 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:
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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)",
Expand Down Expand Up @@ -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);
}
Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -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() {
Expand Down
85 changes: 85 additions & 0 deletions tests/tracking_disabled_test.rs
Original file line number Diff line number Diff line change
@@ -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()
);
}