Skip to content
Merged
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
56 changes: 48 additions & 8 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -412,26 +412,66 @@ Generate this value outside the codebase using your preferred secure workflow, s

### Telemetry & Privacy

starforge collects **anonymous telemetry** to help us improve the CLI. **No personal data is collected** — only command names, success/failure status, and execution time.
starforge collects **anonymous telemetry** to help us improve the CLI. **No personal data is collected.**

#### Disable Telemetry
#### Telemetry Schema (v1)

If you prefer not to participate:
Every event is stored as a single JSON line in `~/.starforge/data/telemetry.log`:

```json
{
"schema_version": 1,
"timestamp": "2025-01-01T12:00:00Z",
"command": "wallet",
"duration_ms": 42,
"success": true,
"anonymous_id": "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx"
}
```

| Field | Type | Description |
|-------|------|-------------|
| `schema_version` | `u8` | Schema version — bumped on breaking changes |
| `timestamp` | ISO 8601 string | UTC time of the command |
| `command` | string | Top-level command name (e.g. `wallet`, `deploy`) |
| `duration_ms` | integer | Execution time in milliseconds |
| `success` | boolean | Whether the command completed without error |
| `anonymous_id` | UUIDv4 string | Random ID generated once per install, never changes |

The log is capped at **10,000 entries or 5 MB**, whichever comes first. Oldest entries are pruned automatically on each write. You can query it directly with `jq`:

```bash
# Show all failed commands
jq 'select(.success == false)' ~/.starforge/data/telemetry.log

# Count commands by name
jq -r '.command' ~/.starforge/data/telemetry.log | sort | uniq -c | sort -rn
```

#### Manage Telemetry

```bash
# Show the last 20 events in a table
starforge telemetry show

# Show the last 50 events
starforge telemetry show --limit 50

# Wipe the log entirely
starforge telemetry clear

# Check status and log stats
starforge telemetry status

# Permanently disable telemetry
starforge config set telemetry false
starforge telemetry disable

# Or use an environment variable (useful for CI/CD)
export STARFORGE_TELEMETRY=0
```

**What's collected**: Command name, timestamp, success status, duration (milliseconds), and a random anonymous ID.

**What's NOT collected**: Wallet addresses, secret keys, contract code, configuration values, error messages, or personal information.

See [Privacy & Telemetry](#privacy--telemetry) for opt-out options.

---

| Template | Description |
Expand Down
154 changes: 137 additions & 17 deletions src/commands/telemetry.rs
Original file line number Diff line number Diff line change
@@ -1,40 +1,160 @@
use crate::utils::{config, print as p, telemetry};
use anyhow::Result;
use clap::Subcommand;
use colored::*;

#[derive(Subcommand)]
pub enum TelemetryCommands {
/// Enable telemetry collections
/// Enable telemetry collection
Enable,
/// Disable telemetry collections
/// Disable telemetry collection
Disable,
/// Show current telemetry status
/// Show current telemetry status and log stats
Status,
/// Pretty-print the last N telemetry events
Show {
/// Number of recent events to display (default: 20)
#[arg(short, long, default_value_t = 20)]
limit: usize,
},
/// Wipe the telemetry log
Clear {
/// Skip confirmation prompt
#[arg(long)]
yes: bool,
},
}

pub fn handle(cmd: TelemetryCommands) -> Result<()> {
match cmd {
TelemetryCommands::Enable => {
telemetry::set_telemetry_enabled(true)?;
p::success("Telemetry collections enabled.");
p::success("Telemetry collection enabled.");
}
TelemetryCommands::Disable => {
telemetry::set_telemetry_enabled(false)?;
p::success("Telemetry collections disabled.");
p::success("Telemetry collection disabled.");
}
TelemetryCommands::Status => {
let cfg = config::load()?;
let enabled = cfg.telemetry_enabled.unwrap_or(true);
let env_override = std::env::var("STARFORGE_TELEMETRY").ok();

p::header("Telemetry Status");
p::separator();
p::kv("Configured Enabled", &enabled.to_string());
if let Some(env_val) = env_override {
p::kv("Environment Override (STARFORGE_TELEMETRY)", &env_val);
}
p::separator();
TelemetryCommands::Status => handle_status()?,
TelemetryCommands::Show { limit } => handle_show(limit)?,
TelemetryCommands::Clear { yes } => handle_clear(yes)?,
}
Ok(())
}

// ── Handlers ──────────────────────────────────────────────────────────────────

fn handle_status() -> Result<()> {
let cfg = config::load()?;
let enabled = cfg.telemetry_enabled.unwrap_or(true);
let env_override = std::env::var("STARFORGE_TELEMETRY").ok();
let count = telemetry::event_count()?;
let size = telemetry::log_size_bytes()?;

p::header("Telemetry Status");
p::separator();
p::kv(
"Collection",
if enabled { "enabled" } else { "disabled" },
);
p::kv("Schema Version", &telemetry::TELEMETRY_SCHEMA_VERSION.to_string());
p::kv("Events Stored", &count.to_string());
p::kv("Log Size", &format!("{:.1} KB", size as f64 / 1024.0));
p::kv(
"Limits",
&format!(
"{} entries / 5 MB",
telemetry::MAX_ENTRIES
),
);
if let Some(val) = env_override {
p::kv("Env Override (STARFORGE_TELEMETRY)", &val);
}
p::separator();
p::info("Use `starforge telemetry show` to inspect stored events.");
p::info("Use `starforge telemetry clear` to wipe the log.");
Ok(())
}

fn handle_show(limit: usize) -> Result<()> {
let events = telemetry::read_events(limit)?;

if events.is_empty() {
p::info("No telemetry events recorded yet.");
return Ok(());
}

p::header(&format!("Last {} Telemetry Events", events.len()));
p::separator();
println!(
" {:<26} {:<18} {:<8} {:<6} {}",
"Timestamp (UTC)".dimmed(),
"Command".dimmed(),
"Duration".dimmed(),
"Status".dimmed(),
"Schema".dimmed(),
);
println!(" {}", "─".repeat(72).dimmed());

for ev in &events {
let ts = ev
.timestamp
.format("%Y-%m-%d %H:%M:%S")
.to_string();
let status = if ev.success {
"✓ ok".green().to_string()
} else {
"✗ fail".red().to_string()
};
let duration = format!("{}ms", ev.duration_ms);

println!(
" {:<26} {:<18} {:<8} {:<6} v{}",
ts.dimmed(),
ev.command.cyan(),
duration.white(),
status,
ev.schema_version,
);
}

println!(" {}", "─".repeat(72).dimmed());
println!(
"\n {} {} total events stored. Use {} to see more.\n",
"ℹ".cyan(),
telemetry::event_count()?.to_string().white(),
"--limit N".cyan(),
);
Ok(())
}

fn handle_clear(yes: bool) -> Result<()> {
let count = telemetry::event_count()?;

if count == 0 {
p::info("Telemetry log is already empty.");
return Ok(());
}

if !yes {
println!();
print!(
" This will delete {} telemetry events. Proceed? [y/N] ",
count
);
use std::io::BufRead;
let line = std::io::stdin()
.lock()
.lines()
.next()
.unwrap_or(Ok(String::new()))?;
if !matches!(line.trim().to_lowercase().as_str(), "y" | "yes") {
p::info("Clear cancelled.");
return Ok(());
}
}

telemetry::clear_log()?;
p::success(&format!("Telemetry log cleared ({} events removed).", count));
Ok(())
}
14 changes: 14 additions & 0 deletions src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -131,6 +131,20 @@ fn main() {
print_banner();
}

// On first run after a schema version change, re-display the telemetry notice.
if !cli.quiet {
if let Ok(true) = utils::telemetry::schema_version_changed() {
eprintln!(
"\n {} Telemetry schema updated to v{}. starforge stores only: \
schema_version, timestamp, command, duration_ms, success, anonymous_id. \
No code, keys, or personal data. Run `starforge telemetry show` to audit \
or `starforge telemetry disable` to opt out.\n",
"ℹ".cyan(),
utils::telemetry::TELEMETRY_SCHEMA_VERSION,
);
}
}

let command_name = match &cli.command {
Commands::Wallet(_) => "wallet",
Commands::New(_) => "new",
Expand Down
Loading
Loading