Telemetry: first-run notice and events cover every invocation - #322
Conversation
…s survive (#320) After a successful self-update the binary on disk has been replaced; on Linux a lazy current_exe() then resolves to '... (deleted)', the send child fails to spawn, and the event for the update itself is dropped. telemetry::init() now snapshots the path at the top of main into a OnceLock, and spawn_send_child uses the snapshot — which after an update names the new binary. The hidden 'telemetry send' subcommand is pinned as a stable cross-version interface in cli.rs. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…ored lossy capture (#320) main now has exactly one process::exit, preceded by exactly one telemetry::finalize. Parse Err branches print via clap, keep their help/version update-notice behavior, and fall through as (invocation, exit_code) — so bare invocations, --help, --version, and usage errors all show the first-run notice and produce events under the unchanged consent state machine. Usage errors keep clap's exit 2 and spawn no update-cache refresh. The hidden 'telemetry send' child stays the sole early exit. Failed parses have no ArgMatches, so capture_lossy walks argv against the built clap Command tree: subcommand tokens descend and record the definition's name, --long tokens resolve to defined flags (value-taking flags consume the next token so a value equal to a sibling subcommand name is never misrecorded), and the walk stops at the first unmatched token — argv slices never enter the payload, same structural guarantee as capture(). The payload gains 'outcome' (closed vocabulary from clap's ErrorKind; 'ok' on success) and 'suggestion' (clap's definition-derived did-you-mean). The raw unmatched token is deliberately never recorded. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Module docs and the README telemetry section now state that help,
version, bare, and mistyped invocations are recorded as such — valid
prefix, outcome kind, and clap's definition-derived suggestion — and
that the unmatched token never leaves the machine. The first-run notice
text ('command name, flag names, never values or arguments') stays
true and unchanged.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…failures (#320) Adversarial review of this branch found that a usage error with a closed stderr panicked (exit 101) instead of exiting 2, bypassing the telemetry tail: e.print() was unwrapped where clap's own Error::exit deliberately swallows print failures, and the parse-error path newly exposes the first-run notice's eprintln! to the same panic. Both writes (plus the CHCTL_TELEMETRY_DEBUG dump) now ignore write errors, keeping finalize's 'never errors' contract on every invocation. The review also confirmed three pre-existing child-exit-code passthrough exits in the local handlers that bypass the finalize tail; acknowledged in the invariant comment and tracked as #321. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
|
Finding: short flags (other than The lossy walker in The CLI has real short flags today: This is privacy-safe (fails toward recording less, never toward leaking a value), and it diverges from |
|
The docs link the notice points users to has moved and needs updating.
That URL now redirects; the canonical location is: Since this PR makes the first-run notice fire on every first invocation (bare, |
Structural review (no runtime checks)Overall, the core direction is good: a common telemetry tail is the right shape, refusing to record unmatched tokens is the right privacy choice, and eagerly snapshotting Findings
Architectural and rollout notes
Recommendation: fix the |
There was a problem hiding this comment.
Pull request overview
Extends clickhousectl’s telemetry so every invocation (including --help, --version, bare invocation, and clap parse errors) participates in the existing consent flow and produces a structured event after consent, while keeping the “definition-derived names only” privacy guarantee.
Changes:
- Refactors
mainto a single common tail that always runstelemetry::finalize()and exits once, with parse errors handled viaError::print()+capture_lossy. - Adds
capture_lossy+outcome/suggestiontelemetry fields to represent parse outcomes without ever recording unmatched raw tokens; snapshotscurrent_exe()at startup for reliable child spawning post-self-update. - Updates README telemetry documentation and adds/extends telemetry E2E tests for help/bare/invalid invocations.
Reviewed changes
Copilot reviewed 5 out of 5 changed files in this pull request and generated 1 comment.
Show a summary per file
| File | Description |
|---|---|
| README.md | Documents new “every invocation counts” behavior and the new outcome/suggestion fields. |
| crates/clickhousectl/tests/telemetry_test.rs | Adds subprocess tests asserting notice + marker on first --help and telemetry events for bare/invalid invocations. |
| crates/clickhousectl/src/telemetry.rs | Implements lossy capture for parse errors, adds outcome/suggestion, and snapshots exe path at startup for send-child spawning. |
| crates/clickhousectl/src/main.rs | Restructures parsing/dispatch to always reach a single finalize+exit tail; collects argv once to support lossy capture. |
| crates/clickhousectl/src/cli.rs | Pins the hidden telemetry send subcommand as a stable cross-version interface. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
…ly-once guard (#320) Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…through gap (#320) Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…t candidate (#320) suggestion_for_error took .first() from clap's did-you-mean list, but clap sorts candidates by ascending similarity — the weakest one came first. It now takes the most similar candidate that resolves, and every recorded suggestion is validated by equality match against the built command tree (subcommand names and arg long names) with the definition's own string cloned, so the "never your input" invariant is enforced locally instead of inherited from clap internals. Flag suggestions are normalized to the bare canonical name (clap's leading "--" stripped) — free to change now, since the ingest worker is not yet deployed. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…ror", not "ok" (#320) Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The event-field list had grown code-level granularity (outcome vocabularies, parse-failure reconstruction, exit-code tables) that belongs in the docs page, not the README overview. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes using high effort and found 1 potential issue.
❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, have a team admin enable autofix in the Cursor dashboard.
Reviewed by Cursor Bugbot for commit 2f9e85c. Configure here.
| .filter(|&sub_name| sub_name == name) | ||
| .or_else(|| find_defined_name(sub, name)) | ||
| }) | ||
| }) |
There was a problem hiding this comment.
Suggestion lookup ignores flag aliases
Low Severity
find_defined_name only matches argument long names and subcommand names when anchoring clap’s did-you-mean text, but capture_lossy treats long-flag aliases (e.g. config-file, fg) as valid tokens. When clap’s suggestion uses an alias string, anchoring fails and suggestion is omitted even though a definition-owned canonical name could be recorded safely.
Reviewed by Cursor Bugbot for commit 2f9e85c. Configure here.


Closes #320
Based on
main, deliberately not stacked on #316: the two changesets are disjoint (#316 touches cloud models/commands and appends tocli_request_shape_test.rs; this PR touchesmain.rs,telemetry.rs,src/cli.rs,tests/telemetry_test.rs), so they merge cleanly in either order.What
Any invocation of the binary — bare,
--help,--version, mistyped commands — now shows the first-run notice on first run and produces a telemetry event on subsequent runs, under the unchanged consent state machine.main(Telemetry: first-run notice and events must cover every invocation (bare, --help, --version, parse errors) #320 §1): exactly oneprocess::exit, preceded by exactly onefinalize(). ParseErrbranchese.print()(clap keeps formatting), keep their existing help/version update-notice behavior, and fall through as(invocation, exit_code). Usage errors keep clap's exit 2 and spawn no update-cache refresh. The hiddentelemetry sendchild stays the sole intended early exit.capture_lossy(root, argv, error)walks argv against the built clapCommandtree — subcommand tokens descend and record the definition's name,--longtokens resolve to defined flags (value-taking flags consume the next token, so a value equal to a sibling subcommand name is never misrecorded), and the walk stops at the first unmatched token. Every recorded string is owned by the clap definitions; argv slices never enter the payload.outcome(closed vocabulary:ok | help | version | invalid_subcommand | unknown_argument | missing_subcommand | missing_required | invalid_value | other_parse_error) andsuggestion(clap's definition-derived did-you-mean). The raw unmatched token is deliberately never recorded — a typo is indistinguishable from a pasted secret.outcomein analytics and is Redefine exit codes: move 'cancelled' off 2 to resolve clash with clap usage errors #319's to fix shell-side.telemetry::init()at the top ofmainsnapshotscurrent_exe()into aOnceLock, fixing the Linux post-self-update dropped event; thetelemetry sendchild interface is pinned as stable cross-version incli.rs.Verification
capture_lossy: bare, root/nested help,-h/-V, typo + suggestion, unknown flag, flag-value-equals-subcommand-name, inline=values,--handling, hostile-argvSECRETnever on the wire.HOME): first--helpprints the notice and writes the marker; bare →missing_subcommand/exit 2; hallucinated subcommand →invalid_subcommandwith no raw token; typo →suggestion: "service".cargo test -p clickhousectl(429 unit + 18 e2e), clippy-D warnings,cargo fmt --check,cargo check --workspace --all-features, and the full suite with telemetry compiled out (--no-default-features).--flag=SECRET, closed pipes) and audited clap 4.6's suggestion contexts (SuggestedSubcommand/SuggestedArgare definition-derived; the input-embeddingSuggested*kinds are not read). Its findings are fixed in the final commit: a closed stderr no longer turns exit 2 into a panic (writes are swallowed like clap's ownError::exit), and the pre-existing child-exit-code passthrough exits it spotted are acknowledged and tracked as Local child-process passthrough exits bypass the telemetry tail #321.Rollout
The ingest worker must accept
outcome/suggestion(backward compatible — deploy before the next CLI release). The docs page update ('help/version/failed invocations are recorded as such') is step 3; the notice text stays true and unchanged.🤖 Generated with Claude Code