Skip to content

fix: the disclosure card claimed telemetry was on while local-only forced it off - #419

Merged
JasonYeYuhe merged 1 commit into
mainfrom
fix-disclosure-card-localonly
Aug 7, 2026
Merged

fix: the disclosure card claimed telemetry was on while local-only forced it off#419
JasonYeYuhe merged 1 commit into
mainfrom
fix-disclosure-card-localonly

Conversation

@JasonYeYuhe

Copy link
Copy Markdown
Collaborator

Second of the findings from the v1.45 telemetry audit (first is #418, independent — different files, either can merge first).

The problem

privacy.localOnlyMode overrides the telemetry switch inside UserDefaultsAnonymousTelemetryStore.isEnabled. Deliberate: the master switch promises to skip all cross-app data sources, and a second switch would make the first one a lie.

So the switch's value is not what the app does. Settings › Privacy handles that — disables the control, says "Off — local-only mode covers this too."

The first-launch disclosure card did not. To a local-only user it stated in the present tense that CLI Pulse "reports two things: that it was installed, and whether it ever found a CLI to track" — above a switch reading ON that sent nothing.

Two surfaces in the same app disagreed about whether data was leaving the machine, and the wrong one was the one making the disclosure. That card is the entire justification for the switch defaulting ON, which makes it the worst place in the app to be inaccurate about collection.

The fix

Same treatment as Settings: the explanation describes collection conditionally and leads with "Local-only mode is on, so CLI Pulse is sending nothing at all"; the toggle is disabled with the same wording.

Why a guard script and not just a test

Both surfaces are SwiftUI views in the app target, which has no test bundle — the same blind spot that let the activation-latch defect in #418 ship. Two complementary checks:

Check Pins
test_aShutGateAlwaysHasAUserVisibleReason the flag is correct — across all 4 combinations of localOnlyMode × user switch, advertised state must equal the real gate, and a gate shut for an unshown reason must set telemetrySuppressedByLocalOnly
scripts/check_telemetry_switch_discloses_suppression.sh every surface uses it — any file binding the switch two-way must reference the flag

The test asserts the invariant rather than any wording, so a future third surface is covered by the same rule. The script was verified in both directions: passes here, fails on the v1.45 card. It also fails if it ever matches fewer than two surfaces, so a rename can't quietly make it vacuous — this repo has had a guard that was wired into nothing and sat idle.

Wired into the existing "Helper guards" CI job.

Verification

  • Guard: passes with the fix, fails with the card reverted (both runs shown in the commit).
  • swift testAnonymousTelemetryDisclosureGateTests + PrivacySettingsTests pass, 13 tests.
  • xcodebuild -scheme "CLI Pulse Bar" -configuration Release — BUILD SUCCEEDED.

Note on wording

The v1.45 telemetry UI strings are English literals not present in any .lproj catalog, so the new string follows the same pattern and falls back like its neighbours. Localizing the telemetry UI as a whole is a separate decision, not smuggled in here.

🤖 Generated with Claude Code

…rced it off

`privacy.localOnlyMode` overrides the anonymous-telemetry switch inside
`UserDefaultsAnonymousTelemetryStore.isEnabled`. That is deliberate: the master
switch promises to skip all cross-app data sources, and making someone find a
second switch to stop a second kind of network call would make the first switch
a lie.

The consequence is that the switch's own value is not what the app does, and
every surface that renders it has to say so. Settings › Privacy does: it
disables the control and reads "Off — local-only mode covers this too."

The first-launch disclosure card did not. To a local-only user it stated, in
the present tense, that CLI Pulse "reports two things: that it was installed,
and whether it ever found a CLI to track" — above a switch showing ON that sent
nothing. Two surfaces in the same app disagreed about whether data was leaving
the machine, and the one that was wrong was the one making the disclosure. That
card is the entire justification for the switch defaulting ON, so it is the
worst place in the app to be inaccurate about what is collected.

The card now takes the same treatment as Settings: the explanation says nothing
is being sent and describes the collection conditionally, and the toggle is
disabled with the same wording.

WHY A GUARD SCRIPT AND NOT A TEST

Both surfaces are SwiftUI views in the app target, which has no test bundle —
the same blind spot that let the activation-latch defect ship. So this adds
two complementary checks:

  * AnonymousTelemetryDisclosureGateTests.test_aShutGateAlwaysHasAUserVisibleReason
    pins that the flag is CORRECT: across all four combinations of
    localOnlyMode x the user's switch, the advertised state must equal the real
    gate, and a gate shut for a reason the switch does not show must set
    telemetrySuppressedByLocalOnly. It asserts the invariant rather than any
    wording, so a future third surface is covered by the same rule.

  * scripts/check_telemetry_switch_discloses_suppression.sh pins that every
    surface USES it: any file binding the switch two-way must reference
    telemetrySuppressedByLocalOnly. Verified in both directions — it passes on
    this branch and fails on the v1.45 card. It also fails if it ever matches
    fewer than two surfaces, so a rename cannot quietly make it vacuous.

Wired into the existing "Helper guards" CI job.

swift test: passes. xcodebuild Release: BUILD SUCCEEDED.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@JasonYeYuhe
JasonYeYuhe force-pushed the fix-disclosure-card-localonly branch from d283d15 to b7cd6e1 Compare August 7, 2026 10:25
@JasonYeYuhe
JasonYeYuhe merged commit 1fa7b38 into main Aug 7, 2026
30 of 32 checks passed
@JasonYeYuhe
JasonYeYuhe deleted the fix-disclosure-card-localonly branch August 7, 2026 10:43
JasonYeYuhe added a commit that referenced this pull request Aug 7, 2026
Two fixes to the v1.45 anonymous-install telemetry, both merged already:

  #418  activation was never sent on the launch that shows the disclosure
        card — the coordinator's latch recorded having TRIED rather than
        having SUCCEEDED, so the "Got it" tap hit a stale guard. It hit
        upgrading users hardest, which is the cohort dominating v1.45's
        first days, and biased the funnel pessimistically.

  #419  the disclosure card told local-only users, in the present tense,
        that statistics were being sent. They were not — localOnlyMode
        already forces telemetry off inside the store. Settings said so;
        the card did not.

DEVID + Homebrew only. No App Store submission is prepared here, for two
reasons: iOS 1.45.0 is still WAITING_FOR_REVIEW and a second version cannot
sit in review beside it, and release-surface changes are the owner's call
(feedback_appstore_update). macOS 1.45.0 is already READY_FOR_SALE, so MAS
users keep the v1.45 behaviour until a separate submission is made.

Android versionName/versionCode bumped in step with Apple to satisfy the
v1.21 G6 drift gate; no Android release is implied.

Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
JasonYeYuhe added a commit that referenced this pull request Aug 7, 2026
Ships the helper-agent self-heal (#425), which missed the 1.46.0 cut by three
commits. That is the most user-visible fix of the day and 1.46.0 does not have
it: HelperAgentHealth.swift is on main but absent from the published 1.46.0 DMG.

WHY IT MATTERS MORE THAN THE TELEMETRY FIXES IT TRAVELS WITH

`yyh.CLI-Pulse.helper.agent` can be registered and still have never executed.
Measured on a real machine: 22,138 consecutive failed spawns over ~11 days,
while every signal the app used said healthy — SMAppService `.status` returned
`.enabled`, `register()` returned without throwing, and `launchctl kickstart`
exited 0 on a job that failed to exec milliseconds later.

A dead helper means no provider data, which means CLI Pulse shows nothing at
all. That is the first-value failure the v1.45 telemetry was built to measure,
happening silently, to users who then have no reason to come back.

ALSO IN THIS RELEASE (both already shipped in 1.46.0, restated for users who
skip it): the disclosure card no longer claims statistics are being sent while
local-only mode suppresses them (#419), and activation is no longer lost on the
launch that shows the disclosure card (#418).

DEVID + Homebrew only. iOS 1.45.0 is still WAITING_FOR_REVIEW and a second
version cannot sit in review beside it; App Store submission stays the owner's
call.

swift test: 2678 passed, 0 failures, 4 skipped — including 27 helper-agent
tests. Version drift gate passes (Apple 1.47.0 == Android 1.47.0, code 66).

Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant