Skip to content

Commit e09a53e

Browse files
Merge pull request #1181 from rtk-ai/rgpd-telemetry
2 parents 24875a2 + 8156081 commit e09a53e

12 files changed

Lines changed: 390 additions & 73 deletions

File tree

‎Cargo.lock‎

Lines changed: 0 additions & 12 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

‎Cargo.toml‎

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,6 @@ chrono = "0.4"
2828
tempfile = "3"
2929
sha2 = "0.10"
3030
ureq = "2"
31-
hostname = "0.4"
3231
getrandom = "0.4"
3332
flate2 = "1.0"
3433
quick-xml = "0.37"

‎README.md‎

Lines changed: 10 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -409,7 +409,7 @@ brew uninstall rtk # If installed via Homebrew
409409

410410
## Privacy & Telemetry
411411

412-
RTK collects **anonymous, aggregate usage metrics** once per day, **enabled by default**. This data helps us build a better product: identifying which commands need filters, which filters need improvement, and how much value RTK delivers. For the full list of fields, data handling, and contributor guidelines, see **[docs/TELEMETRY.md](docs/TELEMETRY.md)**.
412+
RTK can collect **anonymous, aggregate usage metrics** once per day. Telemetry is **disabled by default** and requires **explicit opt-in consent** (GDPR Art. 6, 7) during `rtk init` or via `rtk telemetry enable`. This data helps us build a better product: identifying which commands need filters, which filters need improvement, and how much value RTK delivers. For the full list of fields, data handling, and contributor guidelines, see **[docs/TELEMETRY.md](docs/TELEMETRY.md)**.
413413

414414
**What is collected and why:**
415415

@@ -430,14 +430,17 @@ All data is **aggregate counts or anonymized command names** (first 3 words, no
430430

431431
**What is NOT collected:** source code, file paths, command arguments, secrets, environment variables, personal data, or repository contents.
432432

433-
**Opt-out** (any of these):
433+
**Manage telemetry:**
434434
```bash
435-
# Environment variable
436-
export RTK_TELEMETRY_DISABLED=1
435+
rtk telemetry status # Check current consent state
436+
rtk telemetry enable # Give consent (interactive prompt)
437+
rtk telemetry disable # Withdraw consent — stops all collection immediately
438+
rtk telemetry forget # Withdraw consent + delete all local data + request server-side erasure
439+
```
437440

438-
# Or in config file (~/.config/rtk/config.toml)
439-
[telemetry]
440-
enabled = false
441+
**Override via environment:**
442+
```bash
443+
export RTK_TELEMETRY_DISABLED=1 # Blocks telemetry regardless of consent
441444
```
442445

443446
## Star History

‎docs/TELEMETRY.md‎

Lines changed: 49 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,11 @@
11
# Telemetry
22

3-
RTK collects anonymous, aggregate usage metrics once per day to help improve the product. Telemetry is **enabled by default** and can be disabled at any time.
3+
RTK collects anonymous, aggregate usage metrics once per day to help improve the product. Telemetry is **disabled by default** and requires explicit consent during `rtk init` or `rtk telemetry enable`.
4+
5+
## Data Collector
6+
7+
**Entity**: `RTK AI Labs`
8+
**Contact**: contact@rtk-ai.app
49

510
## Why we collect telemetry
611

@@ -29,7 +34,7 @@ This data directly drives our roadmap. For example, if telemetry shows that 40%
2934

3035
| Field | Example | Purpose |
3136
|-------|---------|---------|
32-
| `device_hash` | `a3f8c9...` (64 hex chars) | Count unique installations. Salted SHA-256 of hostname + username with a per-device random salt stored locally (`~/.local/share/rtk/.device_salt`). Not reversible. |
37+
| `device_hash` | `a3f8c9...` (64 hex chars) | Count unique installations. SHA-256 of a per-device random salt stored locally (`~/.local/share/rtk/.device_salt`). Not reversible. No hostname or username included. |
3338

3439
### Environment
3540

@@ -109,32 +114,62 @@ This data directly drives our roadmap. For example, if telemetry shows that 40%
109114
- Secrets, API keys, or environment variable values
110115
- Repository names or URLs
111116
- Personally identifiable information
112-
- IP addresses (not logged server-side)
117+
- IP addresses (not stored in telemetry pings; stored temporarily in erasure audit log for accountability, anonymized after 6 months)
113118

114-
## Opt-out
119+
## Consent
115120

116-
Telemetry can be disabled instantly with either method:
121+
Telemetry requires explicit opt-in consent (GDPR Art. 6, 7). Consent is requested during `rtk init` or via `rtk telemetry enable`. Without consent, no data is sent.
117122

118123
```bash
119-
# Environment variable (per-session or in shell profile)
120-
export RTK_TELEMETRY_DISABLED=1
124+
rtk telemetry status # Check current consent state
125+
rtk telemetry enable # Give consent (interactive prompt)
126+
rtk telemetry disable # Withdraw consent
127+
rtk telemetry forget # Withdraw consent + delete local data + request server erasure
128+
```
121129

122-
# Or permanently in config file
123-
# ~/.config/rtk/config.toml
124-
[telemetry]
125-
enabled = false
130+
Environment variable override (blocks telemetry regardless of consent):
131+
```bash
132+
export RTK_TELEMETRY_DISABLED=1
126133
```
127134

128-
When disabled, `rtk init` shows `[info] Anonymous telemetry is disabled`. No data is sent, no background thread is spawned, no network requests are made.
135+
## Retention Policy
136+
137+
- **Server-side**: telemetry records are retained for a maximum of **12 months**, then automatically purged (periodic task every 24 hours).
138+
- **Server-side (erasure log)**: IP addresses in the erasure audit log are **anonymized after 6 months** (GDPR — IP is personal data).
139+
- **Client-side**: the local SQLite database (`~/.local/share/rtk/tracking.db`) retains data for **90 days** by default (configurable via `tracking.history_days` in `config.toml`). Deleted entirely by `rtk telemetry forget`.
129140

130-
## Data handling
141+
## Your Rights (GDPR)
142+
143+
Under the EU General Data Protection Regulation, you have the right to:
144+
145+
- **Access** your data: `rtk telemetry status` shows your device hash; the telemetry payload is fully documented above.
146+
- **Rectification**: since data is anonymous and aggregate, rectification is not applicable.
147+
- **Erasure** (Art. 17): run `rtk telemetry forget` to delete local data and send an erasure request to the server. Alternatively, email contact@rtk-ai.app with your device hash.
148+
- **Restriction of processing**: `rtk telemetry disable` stops all data collection immediately.
149+
- **Portability**: the local SQLite database at `~/.local/share/rtk/tracking.db` contains all locally stored data.
150+
- **Objection**: `rtk telemetry disable` or `export RTK_TELEMETRY_DISABLED=1`.
151+
152+
## Erasure Procedure
153+
154+
1. Run `rtk telemetry forget` — this disables telemetry, deletes your device salt, ping marker, and local tracking database (`history.db`), then sends an erasure request to the server.
155+
2. If the server is unreachable, the CLI prints your full device hash and fallback instructions to email contact@rtk-ai.app for manual erasure.
156+
3. You can also email contact@rtk-ai.app directly to request manual erasure.
157+
158+
## Data Handling
131159

132160
- Telemetry endpoint URL and auth token are injected at **compile time** via `option_env!()` — they are not in the source code
133-
- The server is hosted on GCP Cloud Run with TLS
161+
- All communications use HTTPS (TLS)
134162
- Data is used exclusively for RTK product improvement
135163
- No data is sold or shared with third parties
136164
- Aggregate statistics may be published (e.g. "70% of RTK users are on macOS")
137165

166+
### Server-side Requirements
167+
168+
The telemetry server must implement:
169+
- `POST /erasure` endpoint accepting `{"device_hash": "...", "action": "erasure"}`, authenticated via `X-RTK-Token`
170+
- Automatic periodic purge of telemetry records older than 12 months
171+
- Audit log for erasure requests (GDPR Art. 17(2) accountability) with IP anonymization after 6 months
172+
138173
## For contributors
139174

140175
The telemetry implementation lives in `src/core/telemetry.rs`. Key design decisions:

‎docs/usage/FEATURES.md‎

Lines changed: 17 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1322,7 +1322,9 @@ max_files = 20 # Rotation : garder les N derniers fichiers
13221322
# directory = "/custom/tee/path" # Chemin personnalise (optionnel)
13231323

13241324
[telemetry]
1325-
enabled = true # Telemetrie anonyme (1 ping/jour, opt-out possible)
1325+
enabled = false # Telemetrie anonyme (1 ping/jour, requiert consentement)
1326+
# consent_given = true # Defini automatiquement par `rtk init` ou `rtk telemetry enable`
1327+
# consent_date = "..." # Date du consentement (RFC 3339)
13261328

13271329
[hooks]
13281330
exclude_commands = [] # Commandes a exclure de la recriture automatique
@@ -1371,21 +1373,26 @@ FAILED: 2/15 tests
13711373

13721374
## Telemetrie
13731375

1374-
RTK envoie un ping anonyme une fois par jour (23h d'intervalle) pour des statistiques d'utilisation.
1376+
RTK peut envoyer un ping anonyme une fois par jour (23h d'intervalle) pour des statistiques d'utilisation. La telemetrie est **desactivee par defaut** et requiert un consentement explicite (RGPD Art. 6, 7).
13751377

1376-
**Donnees envoyees :** hash de device, version, OS, architecture, nombre de commandes/24h, top commandes, pourcentage d'economies.
1378+
**Donnees envoyees :** hash de device (SHA-256 d'un sel aleatoire), version, OS, architecture, nombre de commandes/24h, top commandes, pourcentage d'economies.
13771379

1378-
**Desactiver :**
1380+
**Responsable du traitement :** `RTK AI Labs`, contact@rtk-ai.app
1381+
1382+
**Gerer la telemetrie :**
13791383
```bash
1380-
# Via variable d'environnement
1381-
export RTK_TELEMETRY_DISABLED=1
1384+
rtk telemetry status # Voir l'etat du consentement
1385+
rtk telemetry enable # Donner son consentement (prompt interactif)
1386+
rtk telemetry disable # Retirer son consentement
1387+
rtk telemetry forget # Retirer + supprimer donnees locales + demande d'effacement serveur
1388+
```
13821389

1383-
# Via config.toml
1384-
[telemetry]
1385-
enabled = false
1390+
**Desactiver via variable d'environnement :**
1391+
```bash
1392+
export RTK_TELEMETRY_DISABLED=1
13861393
```
13871394

1388-
Aucune donnee personnelle, aucun contenu de commande, aucun chemin de fichier n'est transmis.
1395+
Aucune donnee personnelle, aucun contenu de commande, aucun chemin de fichier n'est transmis. Conservation serveur : 12 mois max. Details : [docs/TELEMETRY.md](../TELEMETRY.md)
13891396

13901397
---
13911398

‎docs/usage/TRACKING.md‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -539,7 +539,7 @@ let _ = conn.execute(
539539
## Security & Privacy
540540

541541
- **Local storage only**: Tracking database never leaves the machine
542-
- **Telemetry enabled by default**: RTK sends a daily anonymous usage ping (version, OS, command counts, token savings). Device identity is a salted SHA-256 hash. Opt out with `RTK_TELEMETRY_DISABLED=1` or `[telemetry] enabled = false` in `~/.config/rtk/config.toml`
542+
- **Telemetry requires consent**: RTK can send a daily anonymous usage ping (version, OS, command counts, token savings). Disabled by default, requires explicit consent via `rtk init` or `rtk telemetry enable`. Manage with `rtk telemetry status/disable/forget`. Override: `RTK_TELEMETRY_DISABLED=1`
543543
- **User control**: Users can delete `~/.local/share/rtk/tracking.db` anytime
544544
- **90-day retention**: Old data automatically purged
545545

‎src/core/config.rs‎

Lines changed: 40 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -88,15 +88,13 @@ impl Default for FilterConfig {
8888
}
8989
}
9090

91-
#[derive(Debug, Serialize, Deserialize)]
91+
#[derive(Debug, Default, Serialize, Deserialize)]
9292
pub struct TelemetryConfig {
9393
pub enabled: bool,
94-
}
95-
96-
impl Default for TelemetryConfig {
97-
fn default() -> Self {
98-
Self { enabled: true }
99-
}
94+
#[serde(default, skip_serializing_if = "Option::is_none")]
95+
pub consent_given: Option<bool>,
96+
#[serde(default, skip_serializing_if = "Option::is_none")]
97+
pub consent_date: Option<String>,
10098
}
10199

102100
#[derive(Debug, Serialize, Deserialize)]
@@ -130,11 +128,6 @@ pub fn limits() -> LimitsConfig {
130128
Config::load().map(|c| c.limits).unwrap_or_default()
131129
}
132130

133-
/// Check if telemetry is enabled in config. Returns None if config can't be loaded.
134-
pub fn telemetry_enabled() -> Option<bool> {
135-
Config::load().ok().map(|c| c.telemetry.enabled)
136-
}
137-
138131
impl Config {
139132
pub fn load() -> Result<Self> {
140133
let path = get_config_path()?;
@@ -220,4 +213,39 @@ history_days = 90
220213
let config: Config = toml::from_str(toml).expect("valid toml");
221214
assert!(config.hooks.exclude_commands.is_empty());
222215
}
216+
217+
#[test]
218+
fn test_old_toml_without_consent_fields() {
219+
let toml = r#"
220+
[telemetry]
221+
enabled = true
222+
"#;
223+
let config: Config = toml::from_str(toml).expect("valid toml");
224+
assert!(config.telemetry.enabled);
225+
assert!(config.telemetry.consent_given.is_none());
226+
assert!(config.telemetry.consent_date.is_none());
227+
}
228+
229+
#[test]
230+
fn test_telemetry_default_disabled() {
231+
let config = Config::default();
232+
assert!(!config.telemetry.enabled);
233+
assert!(config.telemetry.consent_given.is_none());
234+
}
235+
236+
#[test]
237+
fn test_telemetry_consent_roundtrip() {
238+
let toml = r#"
239+
[telemetry]
240+
enabled = true
241+
consent_given = true
242+
consent_date = "2026-04-10T12:00:00Z"
243+
"#;
244+
let config: Config = toml::from_str(toml).expect("valid toml");
245+
assert_eq!(config.telemetry.consent_given, Some(true));
246+
assert_eq!(
247+
config.telemetry.consent_date.as_deref(),
248+
Some("2026-04-10T12:00:00Z")
249+
);
250+
}
223251
}

‎src/core/mod.rs‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ pub mod filter;
77
pub mod runner;
88
pub mod tee;
99
pub mod telemetry;
10+
pub mod telemetry_cmd;
1011
pub mod toml_filter;
1112
pub mod tracking;
1213
pub mod utils;

‎src/core/telemetry.rs‎

Lines changed: 16 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -28,8 +28,20 @@ pub fn maybe_ping() {
2828
return;
2929
}
3030

31+
// Load config once (avoid double disk read)
32+
let cfg = match config::Config::load() {
33+
Ok(c) => c,
34+
Err(_) => return,
35+
};
36+
37+
// RGPD: require explicit consent before any telemetry
38+
match cfg.telemetry.consent_given {
39+
Some(true) => {}
40+
Some(false) | None => return,
41+
}
42+
3143
// Check opt-out: config.toml
32-
if let Some(false) = config::telemetry_enabled() {
44+
if !cfg.telemetry.enabled {
3345
return;
3446
}
3547

@@ -140,21 +152,10 @@ fn send_ping() -> Result<(), Box<dyn std::error::Error>> {
140152
Ok(())
141153
}
142154

143-
fn generate_device_hash() -> String {
155+
pub fn generate_device_hash() -> String {
144156
let salt = get_or_create_salt();
145-
let hostname = hostname::get()
146-
.map(|h| h.to_string_lossy().to_string())
147-
.unwrap_or_default();
148-
let username = std::env::var("USER")
149-
.or_else(|_| std::env::var("USERNAME"))
150-
.unwrap_or_default();
151-
152157
let mut hasher = Sha256::new();
153158
hasher.update(salt.as_bytes());
154-
hasher.update(b":");
155-
hasher.update(hostname.as_bytes());
156-
hasher.update(b":");
157-
hasher.update(username.as_bytes());
158159
format!("{:x}", hasher.finalize())
159160
}
160161

@@ -204,7 +205,7 @@ fn random_salt() -> String {
204205
})
205206
}
206207

207-
fn salt_file_path() -> PathBuf {
208+
pub fn salt_file_path() -> PathBuf {
208209
dirs::data_local_dir()
209210
.unwrap_or_else(|| PathBuf::from("/tmp"))
210211
.join("rtk")
@@ -430,7 +431,7 @@ fn install_method_from_path(path: &str) -> &'static str {
430431
}
431432
}
432433

433-
fn telemetry_marker_path() -> PathBuf {
434+
pub fn telemetry_marker_path() -> PathBuf {
434435
let data_dir = dirs::data_local_dir()
435436
.unwrap_or_else(|| PathBuf::from("/tmp"))
436437
.join(RTK_DATA_DIR);

0 commit comments

Comments
 (0)