Skip to content

Commit 7166590

Browse files
lwshangclaude
andauthored
feat: Anonymous usage telemetry (#386)
* docs: add telemetry design document Describes what data is collected, opt-out mechanisms, storage, batched transmission strategy, and implementation details for a simple per-command telemetry system. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * feat: implement telemetry collection and submission Replace the placeholder tracing-based telemetry layer with a full implementation that records anonymous usage data (command, flags, duration, outcome) to a local events.jsonl file and periodically ships batches to an ingestion endpoint via a detached background process. Add `icp settings telemetry` command and `telemetry_enabled` user setting for opt-out control. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * fix: detach telemetry send process from parent process group Use process_group(0) on Unix and CREATE_NO_WINDOW on Windows so the background send process survives if the parent CLI process is killed or the terminal is closed. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * feat: adopt clap-based argument sanitization for telemetry Replace raw flag-name collection with structured argument extraction using clap's ArgMatches introspection. Each argument now records its name, source (command-line vs environment variable), and value — but only when the value comes from a constrained possible_values set. Free-form values (paths, principals, etc.) are always null to prevent leaking sensitive data. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * feat: add batch ID and sequence numbers to telemetry payloads Generate a batch UUID when rotating events.jsonl and embed it in the batch filename (batch-<timestamp>-<uuid>.jsonl). At send time, inject the batch UUID and a per-record sequence number into each JSON line. This enables server-side deduplication on retried sends and preserves event ordering within a batch. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * refactor: move telemetry setup logic from main.rs into telemetry.rs Move setup_telemetry() and command_telemetry_name() into the telemetry module so main.rs only has a single-line call to telemetry::setup(). Narrow visibility of is_disabled_by_env, show_notice_if_needed, and collect_arguments to private since they are now internal to the module. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * feat: remove timestamp from telemetry records Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * feat: collect identity storage type in telemetry records Introduce a TelemetryData bag in Context that subsystems can write to during command execution. The identity loader now records the storage type (pem/keyring/hsm/anonymous) when loading an identity, and the telemetry session reads it at finish time to include in the record. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * feat: collect managed network info in telemetry records - Add `NetworkType` (managed/connected) to `TelemetryData`; set it in `Context::get_environment` and `Context::get_network` so any command that resolves a network contributes the value automatically. - Add `autocontainerize` setting value to `TelemetrySession`; captured once in `telemetry::setup` alongside the existing telemetry-enabled check, avoiding a second settings load. - Both fields are omitted from the JSON record when absent (`skip_serializing_if = "Option::is_none"`). Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * feat: collect canister count and registry recipes in telemetry records - Add `CanisterSource` field to `Canister` to track whether it was defined with explicit build/sync steps or via a recipe reference - Record `num_canisters` and `recipes` (registry recipe names) as flat fields on `TelemetryRecord` instead of a nested `CanistersTelemetry` object, making the data easier to query in the telemetry table Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * refactor: derive telemetry command name from clap subcommand traversal Replace the exhaustive `command_name()` match with automatic command name derivation by collecting subcommand names while walking ArgMatches, eliminating boilerplate that had to be updated for every new command. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * refactor: rename os to platform with WSL detection, reorder telemetry record fields Rename the `os` field to `platform` and detect WSL via the WSL_DISTRO_NAME env var so it reports "wsl" instead of "linux". Reorder TelemetryRecord fields into logical groups and update telemetry.md to match the current struct (add new fields, remove timestamp, fix recipes example). Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * test: add telemetry integration tests and settings telemetry tests - Change TELEMETRY_ENDPOINT to https://telemetry.invalid/v1/events (.invalid TLD per RFC 2606, never resolves, sends fail silently) - Add ICP_TELEMETRY_ENDPOINT env var override for integration testing - Add telemetry_tests.rs covering the full control-flow pipeline: opt-out env vars (DO_NOT_TRACK, ICP_TELEMETRY_DISABLED, CI), record append, first-run notice, time/size send triggers, no-rotation guard, batch delivery, stale/excess batch cleanup, and machine-id persistence across invocations - Add settings telemetry tests to settings_tests.rs mirroring the existing autocontainerize test set (default, set-false, set-true, persists) Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * fix * apply review suggestions * feat(telemetry): record UTC date per event for timeseries analysis Add a `date` field (YYYY-MM-DD, UTC) to TelemetryRecord so events can be analyzed as a timeseries. The ClickHouse schema should use the Date type (2 bytes) rather than DateTime. Test verifies the field is present and matches today's UTC date. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
1 parent 481f484 commit 7166590

File tree

18 files changed

+1708
-134
lines changed

18 files changed

+1708
-134
lines changed

CHANGELOG.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,10 @@
1818
* use `--follow` to continuously poll for new logs. `--interval <n>` to poll every `n` seconds
1919
* feat: Support `k`, `m`, `b`, `t` suffixes in `.yaml` files when specifying cycles amounts
2020
* feat: Add an optional root-key argument to canister commands
21+
* feat: Anonymous usage telemetry — collects command name, arguments, duration, and outcome
22+
* Enabled by default; opt out with `icp settings telemetry false`, `DO_NOT_TRACK=1`, or `ICP_TELEMETRY_DISABLED=1`
23+
* Automatically disabled in CI environments (`CI` env var set)
24+
* `icp settings telemetry` to view or change the current setting
2125

2226
# v0.1.0
2327

crates/icp-cli/Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,7 @@ tokio.workspace = true
6767
tracing-subscriber.workspace = true
6868
tracing.workspace = true
6969
url.workspace = true
70+
uuid.workspace = true
7071
wslpath2.workspace = true
7172

7273
[target.'cfg(unix)'.dependencies]

crates/icp-cli/src/commands/settings.rs

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,8 @@ pub(crate) struct SettingsArgs {
1818
enum Setting {
1919
/// Use Docker for the network launcher even when native mode is requested
2020
Autocontainerize(AutocontainerizeArgs),
21+
/// Enable or disable anonymous usage telemetry
22+
Telemetry(TelemetryArgs),
2123
}
2224

2325
#[derive(Debug, Args)]
@@ -26,9 +28,16 @@ struct AutocontainerizeArgs {
2628
value: Option<bool>,
2729
}
2830

31+
#[derive(Debug, Args)]
32+
struct TelemetryArgs {
33+
/// Set to true or false. If omitted, prints the current value.
34+
value: Option<bool>,
35+
}
36+
2937
pub(crate) async fn exec(ctx: &Context, args: &SettingsArgs) -> Result<(), anyhow::Error> {
3038
match &args.setting {
3139
Setting::Autocontainerize(sub_args) => exec_autocontainerize(ctx, sub_args).await,
40+
Setting::Telemetry(sub_args) => exec_telemetry(ctx, sub_args).await,
3241
}
3342
}
3443

@@ -65,3 +74,28 @@ async fn exec_autocontainerize(
6574
}
6675
}
6776
}
77+
78+
async fn exec_telemetry(ctx: &Context, args: &TelemetryArgs) -> Result<(), anyhow::Error> {
79+
let dirs = ctx.dirs.settings()?;
80+
81+
match args.value {
82+
Some(value) => {
83+
dirs.with_write(async |dirs| {
84+
let mut settings = Settings::load_from(dirs.read())?;
85+
settings.telemetry_enabled = value;
86+
settings.write_to(dirs)?;
87+
println!("Set telemetry to {value}");
88+
Ok(())
89+
})
90+
.await?
91+
}
92+
93+
None => {
94+
let settings = dirs
95+
.with_read(async |dirs| Settings::load_from(dirs))
96+
.await??;
97+
println!("{}", settings.telemetry_enabled);
98+
Ok(())
99+
}
100+
}
101+
}

0 commit comments

Comments
 (0)