1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
//! Telemetry — opt-in, off by default, content-free.
//!
//! Sparrow does not collect telemetry unless the user explicitly enables it
//! via `sparrow telemetry enable`. This module provides the skeleton so the
//! rest of the codebase can call `telemetry::event(..)` unconditionally
//! without polluting the event stream when telemetry is off (the default).
//!
//! When enabled, the only data ever sent is:
//! - schema version
//! - sparrow version
//! - OS/arch
//! - event name (one of a closed enum, see `EventKind`)
//! - counters (latency_ms, success bool, tool name from a closed list)
//!
//! Never sent: prompts, tool outputs, file contents, file paths, provider
//! responses, identifiers other than a locally-generated random install UUID
//! the user can rotate with `sparrow telemetry rotate-id`.
//!
//! See PRIVACY.md for the full policy.
use serde::{Deserialize, Serialize};
use std::path::PathBuf;
/// Closed enumeration of every event kind that can ever be sent.
/// Adding a variant here is a deliberate policy decision and must be
/// documented in PRIVACY.md.
#[derive(Debug, Clone, Copy, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum EventKind {
/// CLI was invoked. No subcommand or args.
Startup,
/// A run reached the success path.
RunSuccess,
/// A run was aborted by the user (Ctrl+C, /stop).
RunCancelled,
/// A run hit a hard stop (budget, wall-clock, tokens).
RunHardStop,
/// A run failed with an unrecoverable error.
RunFailed,
}
/// Stored telemetry preference, read from `~/.sparrow/telemetry.toml`.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct TelemetryConfig {
/// Master switch. False until the user runs `sparrow telemetry enable`.
pub enabled: bool,
/// Random per-install id, regenerated by `sparrow telemetry rotate-id`.
pub install_id: String,
}
impl Default for TelemetryConfig {
fn default() -> Self {
Self {
enabled: false,
install_id: String::new(),
}
}
}
impl TelemetryConfig {
pub fn path(state_dir: &std::path::Path) -> PathBuf {
state_dir.join("telemetry.toml")
}
pub fn load(state_dir: &std::path::Path) -> Self {
let p = Self::path(state_dir);
if !p.exists() {
return Self::default();
}
std::fs::read_to_string(&p)
.ok()
.and_then(|s| toml::from_str(&s).ok())
.unwrap_or_default()
}
pub fn save(&self, state_dir: &std::path::Path) -> anyhow::Result<()> {
std::fs::create_dir_all(state_dir).ok();
let p = Self::path(state_dir);
std::fs::write(p, toml::to_string_pretty(self)?)?;
Ok(())
}
}
/// Fire a telemetry event. No-op unless telemetry is explicitly enabled.
/// This function is intentionally infallible and side-effect-free in the
/// default (off) state so callers do not need to guard at every call site.
pub fn event(_state_dir: &std::path::Path, _kind: EventKind, _success: Option<bool>) {
// The default build ships with telemetry compiled in as a no-op.
// A future build flag (`--features telemetry-collector`) will route the
// closed-enum events to an opt-in endpoint documented in PRIVACY.md.
// For now the function exists so the rest of the codebase can call it
// without conditional compilation.
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn default_is_disabled() {
let cfg = TelemetryConfig::default();
assert!(!cfg.enabled);
assert!(cfg.install_id.is_empty());
}
#[test]
fn round_trip_through_tempdir() {
let dir = tempfile::tempdir().unwrap();
let cfg = TelemetryConfig {
enabled: false,
install_id: "abcdef".into(),
};
cfg.save(dir.path()).unwrap();
let back = TelemetryConfig::load(dir.path());
assert_eq!(back.install_id, "abcdef");
assert!(!back.enabled);
}
#[test]
fn event_is_noop_when_disabled() {
let dir = tempfile::tempdir().unwrap();
// Should not panic or fail — telemetry off by default.
event(dir.path(), EventKind::Startup, None);
event(dir.path(), EventKind::RunSuccess, Some(true));
}
}