docs/usage/TRACKING.md
Comprehensive documentation for RTK's tracking system, which records how much bash output each filtered command removed.
Everything the tracker stores is measured on bash output bytes, converted to estimated tokens. Bash output is one contributor to input tokens, alongside your prompt, the system prompt and conversation history, and input tokens are in turn only part of the bill, which also counts output tokens. savings_pct is therefore a bash output byte ratio — not a cost figure and not a share of your token bill.
See How RTK Savings Work for the full breakdown.
RTK's tracking system records every command execution to provide analytics on bash output reduction. The system:
rtk command execution
↓
TimedExecution::start()
↓
[command runs]
↓
TimedExecution::track(original_cmd, rtk_cmd, input, output)
↓
Tracker::record(original_cmd, rtk_cmd, input_tokens, output_tokens, exec_time_ms)
↓
SQLite database (~/.local/share/rtk/tracking.db)
↓
Aggregation APIs (get_summary, get_all_days, etc.)
↓
CLI output (rtk gain) or JSON/CSV export
~/.local/share/rtk/tracking.db~/Library/Application Support/rtk/tracking.db%APPDATA%\rtk\tracking.dbRecords older than 90 days are automatically deleted on each write operation to prevent unbounded database growth.
TrackerMain tracking interface for recording and querying command history.
pub struct Tracker {
conn: Connection, // SQLite connection
}
impl Tracker {
/// Create new tracker instance (opens/creates database)
pub fn new() -> Result<Self>;
/// Record a command execution
pub fn record(
&self,
original_cmd: &str, // Standard command (e.g., "ls -la")
rtk_cmd: &str, // RTK command (e.g., "rtk ls")
input_tokens: usize, // Estimated tokens from raw output (bytes / 4)
output_tokens: usize, // Estimated tokens after filtering (bytes / 4)
exec_time_ms: u64, // Execution time in milliseconds
) -> Result<()>;
/// Get overall summary statistics
pub fn get_summary(&self) -> Result<GainSummary>;
/// Get daily statistics (all days)
pub fn get_all_days(&self) -> Result<Vec<DayStats>>;
/// Get weekly statistics (grouped by week)
pub fn get_by_week(&self) -> Result<Vec<WeekStats>>;
/// Get monthly statistics (grouped by month)
pub fn get_by_month(&self) -> Result<Vec<MonthStats>>;
/// Get recent command history (limit = max records)
pub fn get_recent(&self, limit: usize) -> Result<Vec<CommandRecord>>;
}
GainSummaryAggregated statistics across all recorded commands.
pub struct GainSummary {
pub total_commands: usize, // Total commands recorded
pub total_input: usize, // Total input tokens
pub total_output: usize, // Total output tokens
pub total_saved: usize, // Total estimated tokens saved
pub avg_savings_pct: f64, // Average bash output reduction, in percent
pub total_time_ms: u64, // Total execution time (ms)
pub avg_time_ms: u64, // Average execution time (ms)
pub by_command: Vec<(String, usize, usize, f64, u64)>, // Top 10 commands
pub by_day: Vec<(String, usize)>, // Last 30 days
}
DayStatsDaily statistics (Serializable for JSON export).
#[derive(Debug, Serialize)]
pub struct DayStats {
pub date: String, // ISO date (YYYY-MM-DD)
pub commands: usize, // Commands executed this day
pub input_tokens: usize, // Total input tokens
pub output_tokens: usize, // Total output tokens
pub saved_tokens: usize, // Total tokens saved
pub savings_pct: f64, // Bash output reduction, in percent
pub total_time_ms: u64, // Total execution time (ms)
pub avg_time_ms: u64, // Average execution time (ms)
}
WeekStatsWeekly statistics (Serializable for JSON export).
#[derive(Debug, Serialize)]
pub struct WeekStats {
pub week_start: String, // ISO date (YYYY-MM-DD)
pub week_end: String, // ISO date (YYYY-MM-DD)
pub commands: usize,
pub input_tokens: usize,
pub output_tokens: usize,
pub saved_tokens: usize,
pub savings_pct: f64,
pub total_time_ms: u64,
pub avg_time_ms: u64,
}
MonthStatsMonthly statistics (Serializable for JSON export).
#[derive(Debug, Serialize)]
pub struct MonthStats {
pub month: String, // YYYY-MM format
pub commands: usize,
pub input_tokens: usize,
pub output_tokens: usize,
pub saved_tokens: usize,
pub savings_pct: f64,
pub total_time_ms: u64,
pub avg_time_ms: u64,
}
CommandRecordIndividual command record from history.
pub struct CommandRecord {
pub timestamp: DateTime<Utc>, // UTC timestamp
pub rtk_cmd: String, // RTK command used
pub saved_tokens: usize, // Estimated tokens saved
pub savings_pct: f64, // Bash output reduction, in percent
}
TimedExecutionHelper for timing command execution (preferred API).
pub struct TimedExecution {
start: Instant,
}
impl TimedExecution {
/// Start timing a command execution
pub fn start() -> Self;
/// Track command with elapsed time
pub fn track(&self, original_cmd: &str, rtk_cmd: &str, input: &str, output: &str);
/// Track passthrough commands (timing-only, no token counting)
pub fn track_passthrough(&self, original_cmd: &str, rtk_cmd: &str);
}
rtk gain estimates tokens as bytes / 4 (src/core/tracking.rs:1284). RTK ships no real tokenizer by design: embedding one would cost startup time and would require a tokenizer per model, or a per-session model lookup, which RTK does not implement. The same estimator is applied to raw and filtered output, so the percentage is reliable; the absolute token counts are approximate and will not match your provider's billing.
/// Estimate token count (~4 chars = 1 token)
pub fn estimate_tokens(text: &str) -> usize;
/// Format OsString args for display
pub fn args_display(args: &[OsString]) -> String;
/// Legacy tracking function (deprecated, use TimedExecution)
#[deprecated(note = "Use TimedExecution instead")]
pub fn track(original_cmd: &str, rtk_cmd: &str, input: &str, output: &str);
use rtk::tracking::{TimedExecution, Tracker};
fn main() -> anyhow::Result<()> {
// Start timer
let timer = TimedExecution::start();
// Execute command
let input = execute_original_command()?;
let output = execute_rtk_command()?;
// Track execution
timer.track("ls -la", "rtk ls", &input, &output);
Ok(())
}
use rtk::tracking::Tracker;
fn main() -> anyhow::Result<()> {
let tracker = Tracker::new()?;
// Get overall summary
let summary = tracker.get_summary()?;
println!("Total commands: {}", summary.total_commands);
println!("Total saved: {} tokens", summary.total_saved);
println!("Average savings: {:.1}%", summary.avg_savings_pct);
// Get daily breakdown
let days = tracker.get_all_days()?;
for day in days.iter().take(7) {
println!("{}: {} commands, {} tokens saved",
day.date, day.commands, day.saved_tokens);
}
// Get recent history
let recent = tracker.get_recent(10)?;
for cmd in recent {
println!("{}: {} saved {:.1}%",
cmd.timestamp, cmd.rtk_cmd, cmd.savings_pct);
}
Ok(())
}
For commands that stream output or run interactively (no output capture):
use rtk::tracking::TimedExecution;
fn main() -> anyhow::Result<()> {
let timer = TimedExecution::start();
// Execute streaming command (e.g., git tag --list)
execute_streaming_command()?;
// Track timing only (input_tokens=0, output_tokens=0)
timer.track_passthrough("git tag --list", "rtk git tag --list");
Ok(())
}
{
"date": "2026-02-03",
"commands": 42,
"input_tokens": 15420,
"output_tokens": 3842,
"saved_tokens": 11578,
"savings_pct": 75.08,
"total_time_ms": 8450,
"avg_time_ms": 201
}
{
"week_start": "2026-01-27",
"week_end": "2026-02-02",
"commands": 284,
"input_tokens": 98234,
"output_tokens": 19847,
"saved_tokens": 78387,
"savings_pct": 79.80,
"total_time_ms": 56780,
"avg_time_ms": 200
}
{
"month": "2026-02",
"commands": 1247,
"input_tokens": 456789,
"output_tokens": 91358,
"saved_tokens": 365431,
"savings_pct": 80.00,
"total_time_ms": 249560,
"avg_time_ms": 200
}
date,commands,input_tokens,output_tokens,saved_tokens,savings_pct,total_time_ms,avg_time_ms
2026-02-03,42,15420,3842,11578,75.08,8450,201
2026-02-02,38,14230,3557,10673,75.00,7600,200
2026-02-01,45,16890,4223,12667,75.00,9000,200
# .github/workflows/track-rtk-savings.yml
name: Track RTK Savings
on:
schedule:
- cron: '0 0 * * 1' # Weekly on Monday
workflow_dispatch:
jobs:
track-savings:
runs-on: ubuntu-latest
steps:
- name: Install RTK
run: cargo install --git https://github.com/rtk-ai/rtk --branch master
- name: Export weekly stats
run: |
rtk gain --weekly --format json > rtk-weekly.json
cat rtk-weekly.json
- name: Upload artifact
uses: actions/upload-artifact@v3
with:
name: rtk-metrics
path: rtk-weekly.json
- name: Post to Slack
if: success()
env:
SLACK_WEBHOOK: ${{ secrets.SLACK_WEBHOOK }}
run: |
SAVINGS=$(jq -r '.[0].saved_tokens' rtk-weekly.json)
PCT=$(jq -r '.[0].savings_pct' rtk-weekly.json)
curl -X POST -H 'Content-type: application/json' \
--data "{\"text\":\"📊 RTK Weekly: ${SAVINGS} tokens saved (${PCT}%)\"}" \
$SLACK_WEBHOOK
#!/usr/bin/env python3
"""
Export RTK metrics to Grafana/Datadog/etc.
"""
import json
import subprocess
from datetime import datetime
def get_rtk_metrics():
"""Fetch RTK metrics as JSON."""
result = subprocess.run(
["rtk", "gain", "--all", "--format", "json"],
capture_output=True,
text=True
)
return json.loads(result.stdout)
def export_to_datadog(metrics):
"""Send metrics to Datadog."""
import datadog
datadog.initialize(api_key="YOUR_API_KEY")
for day in metrics.get("daily", []):
datadog.api.Metric.send(
metric="rtk.tokens_saved",
points=[(datetime.now().timestamp(), day["saved_tokens"])],
tags=[f"date:{day['date']}"]
)
datadog.api.Metric.send(
metric="rtk.savings_pct",
points=[(datetime.now().timestamp(), day["savings_pct"])],
tags=[f"date:{day['date']}"]
)
if __name__ == "__main__":
metrics = get_rtk_metrics()
export_to_datadog(metrics)
print(f"Exported {len(metrics.get('daily', []))} days to Datadog")
// In your Cargo.toml
// [dependencies]
// rtk = { git = "https://github.com/rtk-ai/rtk" }
use rtk::tracking::{Tracker, TimedExecution};
use anyhow::Result;
fn main() -> Result<()> {
// Track your own commands
let timer = TimedExecution::start();
let input = run_expensive_operation()?;
let output = run_optimized_operation()?;
timer.track(
"expensive_operation",
"optimized_operation",
&input,
&output
);
// Query aggregated stats
let tracker = Tracker::new()?;
let summary = tracker.get_summary()?;
println!("Total savings: {} tokens ({:.1}%)",
summary.total_saved,
summary.avg_savings_pct
);
// Export to JSON for external tools
let days = tracker.get_all_days()?;
let json = serde_json::to_string_pretty(&days)?;
std::fs::write("metrics.json", json)?;
Ok(())
}
commandsCREATE TABLE commands (
id INTEGER PRIMARY KEY,
timestamp TEXT NOT NULL, -- RFC3339 UTC timestamp
original_cmd TEXT NOT NULL, -- Original command (e.g., "ls -la")
rtk_cmd TEXT NOT NULL, -- RTK command (e.g., "rtk ls")
input_tokens INTEGER NOT NULL, -- Estimated tokens from raw output (bytes / 4)
output_tokens INTEGER NOT NULL, -- Estimated tokens after filtering (bytes / 4)
saved_tokens INTEGER NOT NULL, -- input_tokens - output_tokens
savings_pct REAL NOT NULL, -- (saved/input) * 100, a bash output byte ratio
exec_time_ms INTEGER DEFAULT 0 -- Execution time in milliseconds
);
CREATE INDEX idx_timestamp ON commands(timestamp);
On every write operation (Tracker::record), records older than 90 days are deleted:
fn cleanup_old(&self) -> Result<()> {
let cutoff = Utc::now() - chrono::Duration::days(90);
self.conn.execute(
"DELETE FROM commands WHERE timestamp < ?1",
params![cutoff.to_rfc3339()],
)?;
Ok(())
}
The system automatically adds new columns if they don't exist (e.g., exec_time_ms was added later):
// Safe migration on Tracker::new()
let _ = conn.execute(
"ALTER TABLE commands ADD COLUMN exec_time_ms INTEGER DEFAULT 0",
[],
);
rtk init or rtk telemetry enable. Manage with rtk telemetry status/disable/forget. Override: RTK_TELEMETRY_DISABLED=1~/.local/share/rtk/tracking.db anytimeIf you see "database is locked" errors:
~/.local/share/rtk/tracking.dbrm ~/.local/share/rtk/tracking.db && rtk gainOlder databases may not have the exec_time_ms column. RTK automatically migrates on first use, but you can force it:
sqlite3 ~/.local/share/rtk/tracking.db \
"ALTER TABLE commands ADD COLUMN exec_time_ms INTEGER DEFAULT 0"
Token estimation uses ~4 chars = 1 token. This is approximate. For precise counts, integrate with your LLM's tokenizer API.
Planned improvements (contributions welcome):
cargo doc --open for API docs