Bring help, version, and bare invocations into the telemetry lifecycle - #310
Bring help, version, and bare invocations into the telemetry lifecycle#310sdairs wants to merge 1 commit into
Conversation
Help (--help/-h/subcommand help), --version, bare `clickhousectl`, and incomplete command groups previously exited inside the clap Err match before the telemetry module ran: no first-run notice, no telemetry.json marker, no event, ever. They now participate in the normal lifecycle under the same consent rules, and the update notice is printed consistently across --help and the bare invocation (which keeps its usage-error exit code 2). No ArgMatches exists on the clap error path, so a new telemetry::capture_from_args re-derives the invocation from raw argv against the built clap definitions. The names-only discipline holds structurally: every recorded string is cloned from clap metadata (Command::get_name / Arg::get_long), never from a user-supplied token; unresolvable tokens, values after value-taking flags, and everything after `--` are skipped. Genuinely mistyped invocations (unknown commands or flags) keep the intentional no-event behavior. DO_NOT_TRACK, the fail-open unwritable config dir, and the hidden `telemetry send` child are unchanged. The payload schema is unchanged: help/version surface as flag names, a bare invocation as an empty command path, incomplete groups as a path no successful run can produce. Closes #309 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 7257eb3. Configure here.
| // Non-UTF-8 tokens can only be values; skip them. | ||
| let Some(token) = token.to_str() else { | ||
| continue; | ||
| }; |
There was a problem hiding this comment.
Non-UTF8 breaks value skip
Low Severity
In capture_from_args, when a value-taking flag sets skip_next_value and the next argv token is non-UTF-8, the loop continues without clearing that flag. The following token is then treated as a flag value and skipped, so later flags (e.g. help) can be omitted from telemetry even though clap still handled the invocation.
Reviewed by Cursor Bugbot for commit 7257eb3. Configure here.
| // Non-UTF-8 tokens can only be values; skip them. | ||
| let Some(token) = token.to_str() else { | ||
| continue; | ||
| }; |
There was a problem hiding this comment.
🟡 Medium src/telemetry.rs:293
capture_from_args discards a token after a non-UTF-8 value. When a value-taking flag is followed by a non-UTF-8 OsString, the token.to_str() check at line 294 hits continue while skip_next_value stays true, so the next real token is wrongly consumed as the value. For example, cloud --api-key <non-UTF-8> --help records api-key but omits help, producing an inaccurate telemetry event. Clear skip_next_value before the UTF-8 check so the flag's value is skipped regardless of whether it is valid UTF-8.
| // Non-UTF-8 tokens can only be values; skip them. | |
| let Some(token) = token.to_str() else { | |
| continue; | |
| }; | |
| if skip_next_value { | |
| skip_next_value = false; | |
| continue; | |
| } | |
| // Non-UTF-8 tokens can only be values; skip them. | |
| let Some(token) = token.to_str() else { |
🚀 Reply "fix it for me" or copy this AI Prompt for your agent:
In file @crates/clickhousectl/src/telemetry.rs around lines 293-296:
`capture_from_args` discards a token after a non-UTF-8 value. When a value-taking flag is followed by a non-UTF-8 `OsString`, the `token.to_str()` check at line 294 hits `continue` while `skip_next_value` stays `true`, so the next real token is wrongly consumed as the value. For example, `cloud --api-key <non-UTF-8> --help` records `api-key` but omits `help`, producing an inaccurate telemetry event. Clear `skip_next_value` before the UTF-8 check so the flag's value is skipped regardless of whether it is valid UTF-8.
Review: structural assessmentReviewed the issue (#309) and the full diff. Overall this is a sound approach, not a hack — the scope is justified and the privacy invariants hold. A few items below. The approach is correctThe fundamental constraint is that clap gives no The The names-only privacy invariant is maintained by construction in both implementations: every recorded string is cloned from clap definitions ( Should fix before mergeNon-UTF-8 + if skip_next_value {
skip_next_value = false;
continue;
}
// Non-UTF-8 tokens can only be values; skip them.
let Some(token) = token.to_str() else {
continue;
};This is exactly the kind of edge case a second parser implementation invites — clap's battle-tested parser handles this, the hand-rolled one doesn't. Deployment itemThe PR notes the ingest worker should be confirmed to tolerate Minor / optional
|
Structural reviewI reviewed this change specifically for whether it's a sound foundation or a hacky fix, given how wide-ranging it looks. Verdict: the structure is sound. The "wide" feel is mostly tests — of the 434 added lines, roughly 300 are unit/integration tests. The production change is ~130 lines of new capture logic plus a Why some new mechanism was unavoidableThe root cause is a clap design decision we can't route around: help, version, and incomplete-subcommand invocations are modeled as parse errors, so no
Why the shadow parser is acceptable here
The Requests
Minor warts, fine to leave
🤖 Generated with Claude Code |


Closes #309
What
--help/-h/subcommand help,--version, bareclickhousectl, and incomplete command groups previously exited inside the clapErrmatch before the telemetry module ran — no first-run notice, notelemetry.jsonmarker, no event, ever. They now join the normal telemetry lifecycle under the same consent rules, and the update notice is printed consistently across--helpand the bare invocation (which keeps clap's usage-error exit code 2).How
telemetry::capture_from_args— noArgMatchesexists on the clap error path, so the invocation is re-derived from raw argv against the built clap definitions. The names-only discipline holds structurally: every recorded string is cloned from clap metadata (Command::get_name/Arg::get_long), never from a user-supplied token. Unresolvable tokens, values following value-taking flags, and everything after--are skipped.main.rsErr arm —DisplayHelp/DisplayVersion(exit 0) andDisplayHelpOnMissingArgumentOrSubcommand/MissingSubcommand(exit 2, clap 4 splits flag-less vs. flag-bearing incomplete groups into these two kinds) all fall through to the update notice +telemetry::finalize. Genuinely mistyped invocations (typos, unknown flags) keep the intentional no-event_ => e.exit()behavior.["help"]/["version"]), a bare invocation ascommand:"", incomplete groups as a group path no successful run can produce, all with the real exit code.Invariants preserved
DO_NOT_TRACKoverrides everything; unwritable config dir fails open to silent; the hiddentelemetry sendchild never produces its own event.Tests
capture_from_args(path depth, value/positional non-leakage incl.--flag=value, value tokens matching subcommand names,--terminator, unknown tokens, non-UTF-8 tokens). The test helper attempts the same failing parse beforebuild()so partially-built clap state matches production exactly.telemetry_test.rscovering first-run notice/marker on--help, event shapes for help/version/bare/incomplete, update-notice consistency on the bare invocation, typo remaining event- and marker-free, and DNT on the help path.Note for deployment
The ingest worker (separate repo) should be confirmed to tolerate
command: ""(bare invocation events) — the schema is otherwise unchanged.🤖 Generated with Claude Code