Skip to content

feat(telemetry): opt-in anonymous usage sharing [roadmap:v0.10.6] - #75

Merged
tcballard merged 10 commits into
mainfrom
claude/mcp-telemetry-opt-in-0nucfg
Jun 12, 2026
Merged

feat(telemetry): opt-in anonymous usage sharing [roadmap:v0.10.6]#75
tcballard merged 10 commits into
mainfrom
claude/mcp-telemetry-opt-in-0nucfg

Conversation

@tcballard

@tcballard tcballard commented Jun 12, 2026

Copy link
Copy Markdown
Collaborator

Summary

Implements rac/roadmaps/v0.10.x-guide/v0.10.6-anonymous-usage-sharing.md.

Adds:

  • Opt-in anonymous remote usage sharing: with explicit consent, rac mcp sends at most one daily ping to PostHog (EU) — a random install id, the RAC version, and a 30-day active-repo count; never paths, queries, arguments, or repository content
  • Consent surfaces: rac telemetry on|off|status, plus the CLI's first interactive prompt — one honest question after a successful rac init, TTY-gated, default No, asked at most once per machine
  • ADR-041 (amending ADR-040's "no network code" clause), a 31-case ping battery with a new network-isolation rule, suite-wide test hermeticity, and a rewrite of every doc surface that claimed nothing leaves the machine

Note: this milestone was authored as v0.10.5 and renumbered to v0.10.6 after the bundled-skills milestone (#73) claimed v0.10.5 on main mid-flight; the branch is rebased onto that merge.

Roadmap / ADR Trace

Roadmap:

  • rac/roadmaps/v0.10.x-guide/v0.10.6-anonymous-usage-sharing.md

Relevant ADRs:

  • rac/decisions/adr-041-anonymous-usage-ping.md (new — consented daily ping, pinned payload, PostHog as third-party sink)
  • rac/decisions/adr-040-guide-local-telemetry.md (amended: one relationship line notes ADR-041 narrows its "RAC contains no network code" clause; local recording and the share-URL flow are unchanged)
  • rac/decisions/adr-032-guide-stateless-reads.md (untouched — the ping runs outside the request/response contract; tool responses stay byte-identical)
  • rac/decisions/adr-035-byo-ai-credentials.md (honored — the dependency is optional by consent and inert without a key)

Scope

Included

  • src/rac/consent.py (stdlib-only, outside rac.mcp so rac init/rac telemetry never import the MCP SDK): corruption-tolerant consent record at ~/.config/rac/telemetry.json; random secrets.token_hex(16) install id minted at opt-in and preserved across off→on toggles; a local-only salt for repo digests; persisted declines so the init question is ask-once. The PostHog constants live here (EU capture endpoint plus the public project write key) — blanking the key is a kill switch that disables all sending
  • src/rac/mcp/ping.py — RAC's entire network surface, the only module permitted to import urllib.request (new isolation rule): daemon thread started by run_server, at most one attempt per 24h via a local marker, 3-second timeout, every failure swallowed, no retries, payload pinned in full
  • Active-repo tracking: salted digests with last-seen dates in ~/.local/state/rac/active-repos.json, pruned to 30 days; only the count crosses the wire and the salt never does
  • Server hook _maybe_start_sharing in run_server — independent of the local --telemetry flag, announced on stderr (stdout stays protocol-only), silent without consent
  • Suite-wide hermeticity: an autouse conftest fixture isolates XDG_CONFIG_HOME/XDG_STATE_HOME for every test, committed before the live key landed — no test run can find a developer's consent record and phone home

Excluded

  • Opt-out-by-default telemetry, or any prompt that asks twice
  • A second event type, per-tool remote metrics, latency reporting, or transmitting any part of the local telemetry log — per-tool analytics stay local (rac mcp-stats, v0.10.4)
  • The PostHog SDK as a dependency; the POST is plain stdlib
  • Retries, offline queueing, or batching of failed pings
  • A self-hosted collector (no domain/infrastructure yet — recorded as the revisit alternative in ADR-041; the sink hides behind one constant)

Product / Architecture Decisions

  • The payload is the entire transmission and is pinned field-for-field — adding a field is a new ADR, enforced by exact key-set assertions at both payload levels
  • The install id is random, not a salted hash of machine attributes: derivable is the opposite of anonymous; a random token derives from nothing (ADR-041 records the reasoning)
  • The payload follows PostHog's documented capture contract: distinct_id rides inside properties, and "$process_person_profile": false marks events anonymous so PostHog creates no person profiles — cheaper, and a sink-side enforcement of the anonymity posture
  • Consent and the PostHog constants live outside rac.mcp because importing that package pays the eager MCP SDK import; the ping module imports them from rac.consent
  • Sharing is independent of --telemetry: each is its own opt-in; consenting without serving sends nothing, and recording locally never implies sharing
  • The 24h marker is written after each attempt regardless of outcome — a failing endpoint costs one attempt per day, never a retry storm
  • The rac init prompt is deliberately the CLI's first interactive prompt; the ADR records the precedent and its gates (TTY both ends, no --json, no existing record, EOF/empty means No)

User-Facing Contract

CLI

rac telemetry           # status (default): what is shared, and whether sending is possible
rac telemetry on        # opt in; mints a random install id
rac telemetry off       # opt out
rac init                # asks the one-time consent question on a real terminal

Human Output

  • rac init (TTY, no prior answer): Share anonymous usage to help shape Lore? [y/N] — default No, either answer persisted
  • rac telemetry status: sharing on/off, install id, consent timestamp and file path, what is shared daily, and an explicit note when no endpoint key is configured (in that state nothing is sent)
  • rac mcp with consent on announces on stderr: at most one daily ping, what it contains, and how to disable — sharing is never silent

Wire Contract (the entire transmission)

{
  "api_key": "phc_... (the public project write key)",
  "event": "lore-daily-ping",
  "timestamp": "2026-06-12T20:37:04Z",
  "properties": {
    "distinct_id": "cd03eed3dd98b784834485e27a436914 (random install id)",
    "$process_person_profile": false,
    "schema_version": "1",
    "rac_version": "0.10.6",
    "active_repos": 2
  }
}

Exit Codes

  • 0: telemetry consent shown or changed (on/off/status, including when no key is configured); init behavior unchanged
  • 2: invalid telemetry action (argparse choices)

Verification

Ran

python -m pytest                      # 1040 passed (re-run after rebasing onto the skills merge)
ruff check .                          # clean
mypy                                  # no new errors (51 pre-existing missing-stub noise)
rac validate rac/                     # 120 valid, exit 0
rac relationships rac/ --validate     # 321 checked, 0 issues, exit 0
rac review rac/                       # no priority 1-2 findings

Manual end-to-end against a local HTTP sink standing in for PostHog: rac telemetry onrac mcp startup produced the stderr notice and exactly one POST whose body matched the pinned payload byte-for-byte (real key, distinct_id and $process_person_profile: false inside properties, active_repos: 1); a second startup within 24h sent nothing; after rac telemetry off the restart was fully silent; the state file contained only a salted digest, no path.

Covered

  • Consent: round-trip, corruption tolerance (garbage/non-dict/missing → no consent), unwritable-dir saves never raise, ids minted once and preserved across toggles
  • Init prompt: shown only on a TTY with no record and no --json; y records consent; empty/n/EOF persist a decline; a second init never asks; non-TTY never reaches input()
  • Payload: exact key sets pinned at both levels; no value contains a path, the repo root, or the salt; wire capture checks endpoint, JSON body, content type, and the 3s timeout
  • Failure posture: raised urlopen swallowed with exactly one attempt; marker written after failure (no retry within 24h); should_ping honors fresh/stale/corrupt markers
  • Gating: no thread without consent, install id, or key; thread is daemon=True; _maybe_start_sharing silent without consent, announces with key, names the kill switch without one
  • Active repos: salted digests differ under different salts, raw paths never stored, 30-day pruning, corrupt file tolerated
  • Isolation: network client modules (urllib.request, http.client) importable only by rac/mcp/ping.py; all existing batteries pass unchanged, including the v0.10.4 byte-identical-response guards

Review Path

  1. rac/decisions/adr-041-anonymous-usage-ping.md and the roadmap — the contract, including the ADR-040 amendment
  2. src/rac/consent.py — consent record, ids, kill-switch constants
  3. src/rac/mcp/ping.py — the one network module: payload, dedupe, failure posture
  4. src/rac/mcp/server.py (_maybe_start_sharing) and src/rac/cli.py (cmd_telemetry, the init prompt)
  5. tests/test_mcp_ping.py, tests/test_init.py, tests/test_mcp_isolation.py, tests/conftest.py — the battery, prompt gates, network rule, hermeticity
  6. README.md, docs/mcp.md, docs/cli.md, CHANGELOG.md — the honesty rewrite

Notes For Reviewer

  • This PR arms the channel: the live EU endpoint and project write key are in src/rac/consent.py, so once released, consenting installs actually transmit. The key is a public write-only key (standard for client telemetry); blanking it disables all sending. The hermeticity fixture was committed before the key so no point in history has a live key with non-hermetic tests.
  • The branch is rebased onto the skills merge (feat(skill): bundled agent skills and growth programme corpus [roadmap:v0.10.5] #73); the milestone renumbered v0.10.5 → v0.10.6 in a dedicated commit, and the CLI reference's subcommand count was reconciled (telemetry and skill both landed → twenty-two).
  • The README bullet that said "Lore contains no network code" is rewritten — the new claim ("opt-in twice over; the network surface is a single readable module") is the one the code now keeps.
  • Data starts flowing only when all four hold: released, user updated, user consented, user runs rac mcp. Retention = distinct distinct_id per day on the lore-daily-ping event.
  • Per-tool analytics deliberately stay local; promoting any of them into the ping is a new ADR by construction.

Implementation Process

Implemented with AI assistance under the roadmap contract.

Final scope, review, and acceptance decisions were made by the maintainer.

tcballard added 10 commits June 12, 2026 20:47
…v0.10.6]

Implements rac/roadmaps/v0.10.x-guide/v0.10.6-anonymous-usage-sharing.md
(contract): consented, anonymous, pinned daily ping; consent surfaces;
honesty-surface rewrite; contract battery with a network isolation rule.
Pins the phase-2 telemetry shape: opt-in twice over, pinned payload
(install id, version, active-repo count — never content or paths),
PostHog as a swappable third-party sink, empty-key kill switch, and a
one-module network surface. Amends ADR-040's no-network-code clause;
the amendment is noted in ADR-040's relationship section.
Implements rac/roadmaps/v0.10.x-guide/v0.10.6-anonymous-usage-sharing.md.

Adds rac.consent: corruption-tolerant consent record under XDG config,
random install id minted at opt-in and preserved across toggles,
local-only repo-digest salt, persisted declines for ask-once, and the
PostHog constants with the empty-key kill switch.
Implements rac/roadmaps/v0.10.x-guide/v0.10.6-anonymous-usage-sharing.md.

Adds rac.mcp.ping — the only network code in RAC: pinned payload,
24-hour marker dedupe, salted active-repo digests (count only crosses
the wire), 3s timeout, failures swallowed, no retries, daemon thread.
run_server starts it only with recorded consent and a configured key,
announcing on stderr; tool responses stay byte-identical.
…:v0.10.6]

Implements rac/roadmaps/v0.10.x-guide/v0.10.6-anonymous-usage-sharing.md.

Adds rac telemetry [on|off|status] and the CLI's first interactive
prompt: one honest usage-sharing question after a successful rac init,
TTY-gated, default No, never with --json, persisted either way so it
is asked at most once per machine.
…v0.10.6]

Implements rac/roadmaps/v0.10.x-guide/v0.10.6-anonymous-usage-sharing.md.

Adds:
- ping battery: pinned payload key sets, wire capture, swallowed
  failures with one attempt per day, 24h dedupe, salted active-repo
  digests, thread gating, server-hook notices
- init prompt battery: TTY gating, default No, ask-once, --json purity
- isolation rule: network client modules importable only by rac.mcp.ping
- CI matrix entry for the new battery
…0.10.6]

Implements rac/roadmaps/v0.10.x-guide/v0.10.6-anonymous-usage-sharing.md.

Rewrites every surface that claimed nothing leaves the machine: the
README trust bullet becomes opt-in twice over, docs/mcp.md shows the
entire pinned payload verbatim, docs/cli.md documents rac telemetry
and the one-time init question, and the changelog names the kill
switch.
…ap:v0.10.6]

An autouse fixture points XDG_CONFIG_HOME and XDG_STATE_HOME at temp
directories suite-wide, so no test can read a developer's consent
record or real state — a precondition for carrying a live PostHog key
in source.
…:v0.10.6]

Fills the live project write key and EU capture endpoint, and aligns
the pinned payload with PostHog's documented contract: distinct_id
rides inside properties, and $process_person_profile: false marks the
event anonymous so the sink creates no person profile. ADR-041, the
roadmap contract, the docs example, and the battery are re-pinned in
the same change.
…0.10.6]

v0.10.5 was claimed by the bundled-skills milestone merged on main
while this branch was in flight; the anonymous usage-sharing roadmap
and every reference to it move to v0.10.6. Also reconciles the CLI
subcommand count (telemetry and skill both landed). No behavior
change.
@tcballard
tcballard force-pushed the claude/mcp-telemetry-opt-in-0nucfg branch from 80af15d to b470534 Compare June 12, 2026 20:50
@tcballard tcballard changed the title feat(telemetry): opt-in anonymous usage sharing [roadmap:v0.10.5] feat(telemetry): opt-in anonymous usage sharing [roadmap:v0.10.6] Jun 12, 2026
@tcballard
tcballard merged commit a76a87e into main Jun 12, 2026
2 checks passed
@tcballard
tcballard deleted the claude/mcp-telemetry-opt-in-0nucfg branch June 15, 2026 18:37
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