Skip to content

feat: opt-in anonymous CLI telemetry (closes #80)#91

Merged
8bitAlex merged 4 commits into
mainfrom
feat/issue-80-telemetry
May 14, 2026
Merged

feat: opt-in anonymous CLI telemetry (closes #80)#91
8bitAlex merged 4 commits into
mainfrom
feat/issue-80-telemetry

Conversation

@8bitAlex
Copy link
Copy Markdown
Owner

Summary

Closes #80. raid now ships an opt-in, anonymous CLI telemetry pipeline so the project can prioritize features against real usage signal without ever capturing what users actually run. Off by default.

First-run consent. First interactive run prompts [y/N/?] (capital-N default; ? shows the long-form disclosure inline and re-asks). Non-interactive contexts skip the prompt entirely and persist consent=off so we never re-prompt:

  • DO_NOT_TRACK=1 env var
  • --yes / -y / --headless flag
  • --json flag (machine-readable output mode)
  • stdin isn't a TTY (CI runners, pipes, agent hosts)
  • Build has no PostHog API key (dev / go run builds — telemetry is dead code)
  • The invocation is a raid telemetry ... subcommand

Events. Five events per the issue spec — raid_first_run, raid_command_executed, raid_command_failed, raid_task_executed (sampled ~10% to bound volume), raid_telemetry_opt_out. Every event carries an anonymous UUIDv4 from ~/.config/raid/telemetry-id, the raid version, OS, and arch. Sanitization is enforced by construction: every event builder is a small pure function that only accepts the permitted slots (command name, task type, error code, duration, success). No cmd: bodies, paths, env values, or task output ever reach the payload.

New subcommand. raid telemetry on / off / status / purge / preview. status --json is parseable. preview renders the exact JSON payload raid would post — with the API key automatically redacted (phc_…1234) — so users can audit before opting in. purge deletes the anonymous machine ID so future events can't be linked to past ones.

Build-time API key. var APIKey string in the telemetry package, injected via -ldflags -X .../telemetry.APIKey={{ envOrDefault \"POSTHOG_API_KEY\" \"\" }} in both .goreleaser.yaml and .goreleaser.preview.yaml. Dev builds get empty key → telemetry is a no-op at every call site. Set POSTHOG_API_KEY in the release CI environment to populate it for tagged builds.

Lifecycle / safety. Capture returns immediately; the HTTP POST runs on a fire-and-forget goroutine with a 2-second timeout. executeRoot defers Flush(1500ms) so in-flight events finish (or get dropped on timeout) before raid exits. Any network error is silently swallowed — telemetry never breaks raid.

Version bumped 0.15.0-beta → 0.16.0-beta. Full disclosure at /docs/telemetry.

Test plan

  • go test ./... -race — all packages green
  • IsActive table: off by default; off when API key empty; off when DO_NOT_TRACK=1; on only when API key + decided + enabled + not DO_NOT_TRACK
  • Zero network calls when opted out (integration test against httptest.Server)
  • Zero network calls under DO_NOT_TRACK=1 (integration test)
  • Capture round-trip: payload structure has expected fields, no leaks
  • Sanitization: every event builder scanned for forbidden substrings (cmd, path, secret) in non-permitted slots
  • Anonymous ID: persists across calls, correct UUIDv4 format (version + variant bits)
  • PurgeID removes the file and is idempotent
  • Sampling: rate=0 never fires, rate=1 always fires, intermediate uses RNG correctly
  • PreviewPayload redacts the API key
  • Prompt: skips on empty API key, non-TTY, headless flag, DO_NOT_TRACK, already-decided; accepts on y; declines on empty; explainer-then-accept via ?\ny
  • SetEnabled persists both decided and enabled keys to viper
  • All 5 cobra subcommands tested (on / off / status text + JSON / purge / preview) with full root-cmd flag chain
  • cd site && npm run build — no broken links

🤖 Generated with Claude Code

raid now ships an opt-in, anonymous CLI telemetry pipeline so the
project can prioritize features against real usage signal without
ever capturing what users actually run.

Off by default. First interactive run prompts ([y/N/?] — capital N
default). Non-interactive contexts (no TTY on stdin, --yes/--headless,
--json, DO_NOT_TRACK=1, or a `raid telemetry ...` invocation) skip
the prompt entirely and persist consent=off so we never re-prompt.
Build-time PostHog API key injection via ldflags; dev builds have no
key, telemetry is dead code in them.

New `raid telemetry on / off / status / purge / preview` subcommand:
status shows current state + anonymous ID; preview renders the exact
JSON payload raid would post (with the API key redacted) so the user
can audit before opting in; purge deletes the anonymous machine ID
so future events can't be linked to past ones.

Event hooks fire from ExecuteCommand, ExecuteRepoCommand, and
ExecuteTask. The per-task event is sampled at 10% to bound volume
for commands with many tasks. Every event property is sanitized:
command names and task types only, never cmd: bodies, paths, env
values, or stdout/stderr.

Tests pin the contracts: zero network calls when opted out, zero
network calls under DO_NOT_TRACK, every event builder is free of
forbidden content, UUIDv4 format is correct, the preview output
redacts the API key.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Copilot AI review requested due to automatic review settings May 14, 2026 04:41
@codecov
Copy link
Copy Markdown

codecov Bot commented May 14, 2026

Codecov Report

❌ Patch coverage is 79.32584% with 92 lines in your changes missing coverage. Please review.
✅ Project coverage is 90.86%. Comparing base (b31f74b) to head (a3403ac).

Files with missing lines Patch % Lines
src/internal/telemetry/id.go 62.50% 22 Missing and 11 partials ⚠️
src/cmd/telemetry/telemetry.go 74.02% 9 Missing and 11 partials ⚠️
src/internal/telemetry/telemetry.go 84.88% 7 Missing and 6 partials ⚠️
src/internal/telemetry/prompt.go 82.81% 9 Missing and 2 partials ⚠️
src/internal/telemetry/consent.go 77.27% 5 Missing ⚠️
src/internal/lib/task_runner.go 66.66% 4 Missing ⚠️
src/cmd/raid.go 93.75% 2 Missing ⚠️
src/internal/telemetry/version.go 60.00% 1 Missing and 1 partial ⚠️
src/internal/lib/command.go 95.65% 1 Missing ⚠️
src/internal/telemetry/sampling.go 88.88% 1 Missing ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##             main      #91      +/-   ##
==========================================
- Coverage   92.28%   90.86%   -1.43%     
==========================================
  Files          38       46       +8     
  Lines        3474     3918     +444     
==========================================
+ Hits         3206     3560     +354     
- Misses        175      235      +60     
- Partials       93      123      +30     

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Adds opt-in anonymous CLI telemetry for raid, including consent handling, PostHog event delivery, CLI controls, release-time API key injection, and public documentation.

Changes:

  • Introduces internal/telemetry for consent state, anonymous ID management, event builders, sampling, preview payloads, and async capture.
  • Adds raid telemetry on/off/status/purge/preview and hooks telemetry into command/task execution and root startup.
  • Updates release config, version, README/site docs, and tests for telemetry behavior.

Reviewed changes

Copilot reviewed 21 out of 21 changed files in this pull request and generated 21 comments.

Show a summary per file
File Description
src/resources/app.properties Bumps app version to 0.16.0-beta.
src/internal/telemetry/version.go Reads raid version for telemetry payloads.
src/internal/telemetry/telemetry.go Implements capture, send, flush, enrichment, and preview payloads.
src/internal/telemetry/telemetry_test.go Adds telemetry unit/integration tests.
src/internal/telemetry/sampling.go Adds task telemetry sampling.
src/internal/telemetry/prompt.go Adds first-run consent prompt flow.
src/internal/telemetry/id.go Adds anonymous UUID persistence and purge.
src/internal/telemetry/events.go Adds event property builders.
src/internal/telemetry/consent.go Adds consent state and DO_NOT_TRACK handling.
src/internal/lib/task_runner.go Emits sampled task telemetry.
src/internal/lib/command.go Emits custom command telemetry.
src/cmd/telemetry/telemetry.go Adds telemetry Cobra subcommands.
src/cmd/telemetry/telemetry_test.go Tests telemetry subcommands.
src/cmd/raid.go Registers telemetry command and first-run prompt/flush hooks.
site/docs/whats-new.mdx Adds 0.16 telemetry release note.
site/docs/usage/raid.mdx Documents telemetry command and reserved name.
site/docs/telemetry.mdx Adds telemetry disclosure page.
README.md Adds telemetry summary.
llms.txt Adds telemetry docs entry.
.goreleaser.yaml Injects telemetry API key for releases.
.goreleaser.preview.yaml Injects telemetry API key for preview releases.

Comment thread src/cmd/raid.go Outdated
// is set, so this is safe in CI / pipes / agent hosts. See
// telemetry.MaybePromptForConsent for the full skip matrix.
if !info && !isTelemetrySubcommand(args) {
_ = telemetry.MaybePromptForConsent(headlessFromArgs(args) || jsonModeFromArgs(args))
Comment on lines 107 to +110
startedAt := RecordRecentStart(found.Name)
err := runCommand(found)
RecordRecentEnd(found.Name, err, startedAt)
captureCommandTelemetry(found, err, time.Since(startedAt))
Comment thread src/cmd/raid.go Outdated
// is set, so this is safe in CI / pipes / agent hosts. See
// telemetry.MaybePromptForConsent for the full skip matrix.
if !info && !isTelemetrySubcommand(args) {
_ = telemetry.MaybePromptForConsent(headlessFromArgs(args) || jsonModeFromArgs(args))
Comment thread src/cmd/raid.go
Comment on lines +245 to +252
for _, a := range args[1:] {
if a == "--" {
return false
}
if strings.HasPrefix(a, "-") {
continue
}
return a == "telemetry"
Comment thread site/docs/whats-new.mdx
Comment on lines +12 to +14
## 0.16.0 — upcoming

**Opt-in anonymous CLI telemetry.** raid can now send anonymous usage events to PostHog so the project can prioritize features against real signal (which task types matter, which commands fail, which features go unused). **Off by default** — a fresh install never sends anything until you opt in. The first interactive run prompts (capital-N default); non-interactive contexts (TTY-less stdin, `--yes`/`--headless`, `--json`, `DO_NOT_TRACK=1`, or a `raid telemetry ...` invocation) skip the prompt and leave telemetry off. New `raid telemetry on / off / status / purge / preview` subcommand manages the state; `preview` renders the exact payload raid would post (with the API key redacted) so you can audit before opting in. Captured events carry sanitized properties only — command names and task types, never `cmd:` bodies, paths, env values, or stdout/stderr. See the full disclosure at [/docs/telemetry](/docs/telemetry). Closes [#80](https://github.com/8bitAlex/raid/issues/80).
Comment on lines +22 to +25
// idMu guards the in-process cache around the ID file so concurrent
// Capture calls don't race on the read/write. The file itself is
// effectively single-writer (the user, on this machine), so we don't
// need OS-level locking.
Comment thread llms.txt Outdated
- [Custom commands](https://raidcli.dev/docs/usage/custom): Define and invoke `raid <cmd>` team workflows
- [raid doctor](https://raidcli.dev/docs/usage/doctor): Diagnose profile and repo configuration issues
- [raid root command](https://raidcli.dev/docs/usage/raid): Global flags and top-level invocation, including [headless mode](https://raidcli.dev/docs/usage/raid#headless-mode) (`-y` / `--yes` / `--headless` / `RAID_HEADLESS=1`) that auto-resolves Confirm and Prompt tasks for CI / agent runs
- [raid telemetry](https://raidcli.dev/docs/telemetry): Opt-in anonymous CLI telemetry. Off by default, first-run consent prompt, `on` / `off` / `status` / `purge` / `preview` subcommands, `DO_NOT_TRACK=1` honored, no PII or task content ever collected
Comment thread src/cmd/raid.go Outdated
Comment on lines +264 to +275
for _, a := range args[1:] {
if a == "--" {
break
}
switch {
case a == "-y", a == "--yes", a == "--yes=true", a == "--headless", a == "--headless=true":
return true
case a == "--yes=false", a == "--headless=false":
return false
}
}
return false
Comment thread site/docs/usage/raid.mdx
Comment on lines 108 to +110
The following names are reserved for built-in commands and cannot be used as custom command names:

`profile`, `install`, `env`, `doctor`, `context`, `help`, `version`, `completion`
`profile`, `install`, `env`, `doctor`, `context`, `telemetry`, `help`, `version`, `completion`
Comment thread src/internal/lib/task_runner.go Outdated
Comment on lines +208 to +211
// Sampling and Capture both fast-path when telemetry is off, so the
// per-task overhead when opted out is effectively zero.
func captureTaskTelemetry(task Task, err error, dur time.Duration) {
if !telemetry.Sampled() {
8bitAlex and others added 3 commits May 13, 2026 21:57
- Fire raid_first_run after a prompt-based opt-in (previously only
  the explicit `raid telemetry on` path produced the adoption event).
- Honor RAID_HEADLESS=1 when deciding whether to skip the consent
  prompt, matching how the headless flag itself is treated.
- isTelemetrySubcommand now accounts for value-taking root flags
  (--config / -c), so `raid --config telemetry install` resolves to
  the `install` subcommand rather than misreading the config path
  as the telemetry subcommand.
- headlessFromArgs walks the full arg list and resolves to the
  final value, mirroring pflag's last-value-wins semantics.
- loadOrCreateID writes the ID with O_CREATE|O_EXCL so two
  concurrent first-run raid invocations can't generate competing
  UUIDs; the loser re-reads the winner's value.

Co-Authored-By: Copilot <copilot@github.com>
Sampled() now short-circuits when telemetry is inactive, so the
existing rate=1 / intermediate tests must enable consent before
exercising the rate logic. Adds an explicit inactive-fast-path
test to pin the new behavior.
Adds tests for paths the initial PR didn't reach:

- loadOrCreateID race-loser path (existing ID file is adopted, not
  overwritten) and the home-dir-error fast fail.
- loadIDIfExists empty-on-missing.
- CaptureSync send + no-op-when-inactive.
- send() swallows network errors silently (Flush still returns).
- PreviewPayload renders a placeholder when no ID exists.
- headlessFromArgs last-value-wins (mirrors pflag).
- isTelemetrySubcommand flag-value handling (--config / -c).
@8bitAlex
Copy link
Copy Markdown
Owner Author

Auto-review by meeseeks

Updates pushed: 3 commits

  • 0309bcd fix: address Copilot review — telemetry consent + ID race
  • 11c859a test: align Sampled() tests with IsActive fast-path
  • a3403ac test: cover lines flagged by Codecov patch check

Copilot comments addressed: 17 of 21

  • cmd/raid.go first-run prompt now fires raid_first_run on PromptAccepted (synchronous), matching the event contract.
  • cmd/raid.go consent prompt now honors RAID_HEADLESS=1 via lib.IsHeadless() in the skip predicate.
  • cmd/raid.go isTelemetrySubcommand now accounts for value-taking root flags (--config / -c), so raid --config telemetry install no longer misroutes.
  • cmd/raid.go headlessFromArgs walks the full arg list and resolves to the last value, mirroring pflag's repeat-flag semantics.
  • internal/telemetry/id.go loadOrCreateID uses O_CREATE|O_EXCL and adopts the existing value on race, so concurrent first-run processes can't generate competing distinct_ids.
  • internal/telemetry/sampling.go Sampled() now fast-paths on !IsActive() so opted-out task hooks pay zero RNG cost; tests updated.
  • internal/telemetry/events.go, prompt.go, version.go, telemetry.go, cmd/telemetry/telemetry.go, site/docs/telemetry.mdx, llms.txt — comment/doc accuracy fixes: taskTypes ordered-with-duplicates, PromptSkipped no-API-key exception, raid_version="" on fallback, opt-out send is best-effort not guaranteed, Capture blocks on first ID-file write, opt-out --why exception called out as the one explicit free-text exception.
  • site/docs/references/commands.mdx, site/docs/usage/custom.mdx reserved-name lists now include telemetry.
  • internal/telemetry/telemetry_test.go setupTestEnv now saves/restores promptInFn / promptOutFn.
  • Telemetry docs: clarified that the opt-out claim is about telemetry/PostHog requests specifically (the GitHub release-version check is independent).

Skipped: 4

  • internal/lib/command.go:110 (built-in subcommands don't emit raid_command_executed): architectural decision — the event contract documents command_name as the YAML-authored label. Adding hooks for install/env/doctor/profile/context is a separate event surface, not a fix here. Flagging for human judgement.
  • site/docs/whats-new.mdx:14 (two "upcoming" sections): the file marks every release back through 0.11.1 as "upcoming" — this looks like a project-wide release-note convention, not a regression introduced here. Leaving alone.
  • cmd/raid.go:275 jsonModeFromArgs same last-value-wins issue: Copilot only flagged the headlessFromArgs sibling. Out of scope.

Codecov patch: 79.32% (project 90.86%) — needs human. One honest attempt added ~70 lines of new tests for loadOrCreateID race/error paths, CaptureSync, send() error swallowing, PreviewPayload placeholder, plus tablewise tests for isTelemetrySubcommand and headlessFromArgs. Patch coverage moved 72.80% → 79.32%, project coverage 90.07% → 90.86%, but patch is still ~11.5 points below project. Remaining gaps are mostly in error-path branches of cmd/raid.go executeRoot wiring and cmd/telemetry/* cobra runners that need a fuller test scaffold than I can land in one attempt without risking churn on the cobra root setup.
Other CI: green.

Needs human: decide whether to (a) merge as-is and accept the -1.43% project-coverage delta, (b) accept the architectural decision that built-in subcommands don't emit raid_command_executed and document it on the telemetry page, or (c) hold for additional executeRoot / cobra-runner test coverage to close the patch-vs-project gap.

@8bitAlex 8bitAlex merged commit bd3a73b into main May 14, 2026
11 of 13 checks passed
@8bitAlex 8bitAlex deleted the feat/issue-80-telemetry branch May 14, 2026 05:55
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.

CLI telemetry (opt-in, anonymous)

2 participants