Repository navigation
fix(telemetry): distinguish "consent not given" from missing salt in status (#1656) - #2105
Conversation
4abd4ba to
02dc8d3
Compare
|
Read the diff against Gating proof verified. So the proposed mapping is right: Tests lock the routing. All three are pure-function tests over One observation, not a blocker. Follow-up #2106 is a separate fix in scope and is genuinely orthogonal — the test-pollution writes a real |
02dc8d3 to
56f6afa
Compare
|
Good catch — adopted in |
|
Confirmed against |
|
Friendly check-in — this one's been quiet for a few weeks and looks ready from my side:
No action needed from reviewers if it's just in the queue — just flagging it as merge-ready. Happy to rebase onto the latest |
|
Gentle bump for @aeppling 🙏 — this one's been quiet for a few weeks and looks ready:
Anything I can do to help move it along? Happy to rebase on |
pszymkowiak
left a comment
There was a problem hiding this comment.
Tested all three consent states live with the compiled binary — None, Some(false), and Some(true) (no salt) each render exactly as described. cargo fmt, cargo clippy --all-targets, cargo test --all all clean (1950/1950). Appreciate the precise scoping and the separate follow-up issue (#2106) instead of scope creep.
56f6afa to
1337e1c
Compare
|
Rebased onto latest
|
|
@aeppling — this one looks ready to land whenever you have a moment:
No rush on my side — just flagging that there's nothing left blocking it technically. Happy to rebase again if |
|
Friendly ping — this one has an approval and is a single-file change, so I think it just needs a merge. Recapping for whoever picks it up: the telemetry path currently can't tell "the user declined consent" apart from "the consent setting was never written", so a declined install gets treated as unconfigured on every subsequent run. The fix distinguishes the two states; one file, no behavior change for users who already consented. Also — separate note, and the more useful half of this comment: I'm going to reduce my own open-PR count here. I opened seven PRs across 26–28 May, which in hindsight was too many at once from someone with no track record in this repo. I'm closing the four that have had no review activity (#2143, #2141, #2140, #2122) so the queue reflects what actually matters, and keeping this one plus #2142 (which has a real user report behind it — #1830 documents No urgency on either. Just didn't want to leave a wall of stale PRs sitting on your review list. |
KuSh
left a comment
There was a problem hiding this comment.
Requesting changes. The split is the right idea and the tests are well-targeted, but it keys off consent_given alone — and consent is only one of five gates on salt creation. Three of the others produce "consent granted + no salt" legitimately, and in each of those rtk telemetry status now reports a filesystem failure that isn't happening. That's the same false-alarm class #1656 is about, moved onto a different set of users.
The gates, in the order maybe_ping() applies them (src/core/telemetry.rs:22-67):
TELEMETRY_URL.is_none()— no endpoint compiled into this build (:23-26)telemetry_disabled_by_env()—RTK_TELEMETRY_DISABLED=1(:29-30)consent_given != Some(true)(:38-41) — the only one this PR models!cfg.telemetry.enabled(:44-46)- the 23 h interval check (
:50-58), with the salt itself written on a detached thread (:65-67)
run_status() already holds everything needed for 1, 2 and 4: env_override is computed at line 88, config.telemetry.enabled at 82, and send_erasure_request (line 176) already reads option_env!("RTK_TELEMETRY_URL") directly, so no new accessor in telemetry.rs is required.
Suggested fix
Give the label the rest of the state, and order the checks the way maybe_ping() orders its gates so the message names the one that actually fired:
fn salt_missing_label(
endpoint_configured: bool,
env_override: bool,
enabled: bool,
consent_given: Option<bool>,
) -> &'static str {
// Mirrors the gate order in `telemetry::maybe_ping`.
if !endpoint_configured {
return "(not applicable; this build has no telemetry endpoint)";
}
if env_override {
return "(blocked by RTK_TELEMETRY_DISABLED)";
}
match consent_given {
Some(false) | None => "(telemetry not enabled; run `rtk telemetry enable` to opt in)",
Some(true) if !enabled => "(telemetry disabled in config.toml)",
Some(true) => "(no salt file yet; written on the first ping)",
}
}and at the call site:
println!(
"{}",
device_hash_line(
option_env!("RTK_TELEMETRY_URL").is_some(),
env_override,
config.telemetry.enabled,
config.telemetry.consent_given,
hash.as_deref(),
)
);Five positional params is a lot — passing &config plus env_override is a fine alternative if you'd rather keep the signature narrow; the existing tests adapt either way.
The last arm is deliberately no longer an accusation: (no salt file yet; written on the first ping) is true both when the ping simply hasn't landed and when it genuinely failed, which is as far as the available state lets us go.
Everything else checks out: the hash.as_deref() plumbing is correct, the 01234567...89abcdef expectation matches &h[..8] / &h[56..] on the sample, the salt-exists path is byte-identical to pre-PR output as the description claims, and the branch still applies cleanly on develop after the July rebase.
Review feedback from @KuSh on rtk-ai#2105: `consent_given` is only one of the five gates on salt creation, and three of the others produce "consent granted, no salt" legitimately. Those states kept the old `(no salt file)` string, i.e. `rtk telemetry status` still reported a filesystem failure that is not happening — the same false-alarm class rtk-ai#1656 is about, just narrowed rather than removed. Changes ------- - `salt_missing_label` now takes `endpoint_configured`, `env_override` and `enabled` alongside `consent_given`, with its arms ordered the way `telemetry::maybe_ping` applies its gates (src/core/telemetry.rs:24-26, :29-31, :40-43, :46-48) so the label names the gate that actually fired: * `(not applicable; this build has no telemetry endpoint)` * `(blocked by RTK_TELEMETRY_DISABLED)` * `(telemetry not enabled; run `rtk telemetry enable` to opt in)` * `(telemetry disabled in config.toml)` * `(no salt file yet; written on the first ping)` - The last arm deliberately stops accusing the filesystem: it is true both when no ping has landed (the 23 h interval, or a run that touched the marker before the detached thread wrote the salt) and when a write genuinely failed, which is as far as the available state allows. - `device_hash_line` plumbs the extra state through. The endpoint is read at the call site with `option_env!("RTK_TELEMETRY_URL")`, which `send_erasure_request` already does, so this stays a single-file change with no new accessor in `telemetry.rs`. - The four existing tests move to the new signatures; a new `salt_missing_label_names_the_gate_that_actually_fired` pins the gate ordering, and the routing test gains one case per new arm. ASCII-only punctuation in the new labels, matching the existing rows. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011EuBXCpt6MgTPEMxtAN29Z
|
Thanks @KuSh — the gap is real and I'm adopting your fix. One correction on the framing first, then what changed. This doesn't move the false alarm onto a different set of users; it shrinks the set and then stops short. Before this PR, every no-salt install printed Verified your gate list against
And the state really is all in hand at the call site ( Adopted Two notes on the result, neither of which changes the shape:
The four existing tests moved to the new signatures, the routing test gained one case per new arm, and a new and green again once reverted. Validation on the new head:
|
…status (rtk-ai#1656) A fresh `brew install rtk` followed by `rtk telemetry status` (without ever running `rtk init`) prints: device hash: (no salt file) which reads as a failure. It is actually expected: `send_ping()` is the only production caller of `generate_device_hash()` that creates a salt, and it is gated by `consent_given == Some(true)` at src/core/telemetry.rs:38-41 — so the salt is never written when consent has not been granted. `get_or_create_salt()` at telemetry.rs:175-177 already creates the parent dir, so there is no fs bug to fix here — only the status message conflating two distinct states. Changes ------- - Extract `salt_missing_label(consent_given) -> &'static str` returning: * `(no salt file)` when `consent_given == Some(true)` — a real failure worth investigating (fs permission, full disk, …). * `(telemetry not enabled; run \`rtk telemetry enable\` to opt in)` otherwise — actionable hint so users do not interpret the design as a bug. `Some(false)` and `None` collapse because the remediation is identical. - Extract `device_hash_line(consent_given, hash) -> String` formatting the entire ` device hash: ...` row. `run_status()` builds the hash `Option<String>` from `salt_path.exists()` and delegates the rest. The leaf label and the routing are now both unit tested. - The doc comment on `salt_missing_label` lists every production caller of `generate_device_hash()` so future refactors can verify the gating chain without re-running ripgrep. ASCII-only punctuation in the new label (semicolon, not em-dash) to match the surrounding `println!` lines and stay grep / pipe friendly.
Review feedback from @KuSh on rtk-ai#2105: `consent_given` is only one of the five gates on salt creation, and three of the others produce "consent granted, no salt" legitimately. Those states kept the old `(no salt file)` string, i.e. `rtk telemetry status` still reported a filesystem failure that is not happening — the same false-alarm class rtk-ai#1656 is about, just narrowed rather than removed. Changes ------- - `salt_missing_label` now takes `endpoint_configured`, `env_override` and `enabled` alongside `consent_given`, with its arms ordered the way `telemetry::maybe_ping` applies its gates (src/core/telemetry.rs:24-26, :29-31, :40-43, :46-48) so the label names the gate that actually fired: * `(not applicable; this build has no telemetry endpoint)` * `(blocked by RTK_TELEMETRY_DISABLED)` * `(telemetry not enabled; run `rtk telemetry enable` to opt in)` * `(telemetry disabled in config.toml)` * `(no salt file yet; written on the first ping)` - The last arm deliberately stops accusing the filesystem: it is true both when no ping has landed (the 23 h interval, or a run that touched the marker before the detached thread wrote the salt) and when a write genuinely failed, which is as far as the available state allows. - `device_hash_line` plumbs the extra state through. The endpoint is read at the call site with `option_env!("RTK_TELEMETRY_URL")`, which `send_erasure_request` already does, so this stays a single-file change with no new accessor in `telemetry.rs`. - The four existing tests move to the new signatures; a new `salt_missing_label_names_the_gate_that_actually_fired` pins the gate ordering, and the routing test gains one case per new arm. ASCII-only punctuation in the new labels, matching the existing rows. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011EuBXCpt6MgTPEMxtAN29Z
`option_env!` yields `Some("")` when the build environment exports the
variable with an empty value, which the release workflow's `env:` block
does whenever the repository variable is unset. `.is_some()` then reports
a build as able to ping when it cannot, and `rtk telemetry status` blames
the salt for a gate that had already closed.
Add `utils::env_is_some` for compile-time env checks and funnel every
`RTK_TELEMETRY_URL` read through `telemetry::endpoint_url`, so the ping
path, the status label and the erasure request cannot disagree about
whether an endpoint exists.
Render a present-but-malformed salt as `(malformed device hash)` instead
of denying the file exists, and check the hex alphabet alongside the
length so `&h[..8]` and `&h[56..]` stay on char boundaries.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
1255088 to
bb85017
Compare
There was a problem hiding this comment.
Approving on bb85017. The gate-order rewrite in f40f0a1 addressed every blocking point — salt_missing_label now names the gate that actually fired, and the ordering is pinned by a test. Nice work.
Two housekeeping things I did directly on the branch, since you'd enabled maintainer edits:
Rebased onto current develop (4f3c9fe). The branch was ~300 commits behind and wouldn't have been mergeable. One conflict, in src/core/utils.rs, where develop added tests at the same spot as mine — resolved by keeping both. Your two commits are unchanged apart from the new base and keep their authorship.
Pushed bb85017 for the three remaining review points:
option_env!("RTK_TELEMETRY_URL").is_some()treated an empty URL as a configured endpoint..github/workflows/release.ymlpasses the URL through anenv:block fed byvars.RTK_TELEMETRY_URL, and an unset repository variable is exported as the empty string — sooption_env!compiles toSome("")and a release that can never ping would still have blamed the salt. Newutils::env_is_somecovers the compile-time env check generally, and everyRTK_TELEMETRY_URLread now goes throughtelemetry::endpoint_url, so the ping path, the status label and the erasure request can't drift apart.- A present-but-malformed salt reported
(no salt file yet; written on the first ping)in the one branch whererun_statusalready knows the file exists.device_hash_linenow has three arms — well-formed, malformed, absent. - The
len() == 64guard was a byte check, so a 64-byte multi-byte string passed it and then panicked on&h[..8].is_device_hashchecks the alphabet too; there's a regression test for that exact input.
Verified on the rebased head: cargo fmt --all --check clean, cargo clippy --all-targets zero warnings, cargo test --all 2984 passed / 0 failed.
Summary
Fixes #1656.
rtk telemetry statusshowsdevice hash: (no salt file)for two very different states:The single label reads as an error in case 1 and obscures actual failures in case 2.
Root cause
Verified against
developHEAD (perrg generate_device_hashand @truffle-dev's analysis):get_or_create_salt()already callscreate_dir_all(parent)atsrc/core/telemetry.rs:175-177— no fs bug.generate_device_hash()has three production call sites:send_ping()attelemetry.rs:71(the only one that creates the salt; gated byconsent_given == Some(true)attelemetry.rs:38-41), andrun_status()/run_forget()(both only call it aftersalt_path.exists()).brew install rtk+rtk gainnever triggers consent → no salt →(no salt file)is expected, not a failure.The gating chain is documented inline on the new helper so future refactors don't have to re-derive it.
Fix
src/core/telemetry_cmd.rs:salt_missing_label(consent_given) -> &'static strreturns:(no salt file)whenconsent_given == Some(true)— real failure, original label preserved.(telemetry not enabled; run \rtk telemetry enable` to opt in)otherwise — actionable.Some(false)(explicit opt-out) andNone` (never prompted) collapse because the remediation is identical.device_hash_line(consent_given, hash) -> Stringformats the fulldevice hash: ...line.run_status()builds theOption<String>hash fromsalt_path.exists()and delegates.println!and stay grep / pipe friendly.Tests
cargo fmt --all --check && cargo clippy --all-targets -- -D warnings && cargo test— 1949 passed (3 new intelemetry_cmd::tests)salt_missing_label_distinguishes_consent_not_given_from_real_failure— all 3 consent statesdevice_hash_line_renders_truncated_hash_when_salt_exists— consent-agnostic salt-present pathdevice_hash_line_routes_to_salt_missing_label_when_no_hash— locks the routing so inlining or removing the helper without copying its literals fails CIrtk telemetry statusoutput forsalt_path.exists()users stays byte-identical (only theelsebranch changed)Scope
Single-file change, ~85 LOC (test included). Deliberately does not:
get_or_create_salt()/send_ping()/ consent prompt flow — no fs bug exists.prompt_telemetry_consent()fromrtk gain/rtk telemetry status— UX surface change, separate discussion.Follow-up
Filed separately as #2106:
cargo testwrites a real.device_saltto the user's data dir because the unit tests insrc/core/telemetry.rs(test_salt_is_persisted,test_device_hash_is_stable,test_device_hash_is_valid_hex) call the production functions with no path override. Hides the symptom of this issue on dev machines after the test suite runs. Not bundled here per CONTRIBUTING scope rules.