Skip to content

feat(telemetry): opt-in scaffold, default OFF (spec 20) - #97

Merged
gustavobertoi merged 1 commit into
mainfrom
feat/telemetry-optin
Jul 1, 2026
Merged

feat(telemetry): opt-in scaffold, default OFF (spec 20)#97
gustavobertoi merged 1 commit into
mainfrom
feat/telemetry-optin

Conversation

@gustavobertoi

Copy link
Copy Markdown
Contributor

What

Implements the opt-in, privacy-first usage-telemetry scaffold from spec 20. It is default OFF and, critically, ship-empty: no real network endpoint is wired. The only sink actually used is the no-op sink, so even when a user opts in, nothing leaves the machine.

Changes

  • internal/telemetry (new):
    • Event — the exhaustive allowlist of coarse, non-identifying fields (command path, flag names, ok/error outcome, error-category enum, bucketed duration, os/arch, is_wsl2, tool version, random install id). No map[string]string escape hatch, so PII is structurally impossible to attach (asserted by a reflection test).
    • Sink interface + NoopSink (the only wired sink; DefaultSink() returns it, DefaultEndpoint is empty).
    • HTTPSink — a pluggable transport that is unused and disabled by default (empty endpoint = no-op), with a clearly documented TODO to wire a pure-Go OTLP/HTTP exporter once a project-operated collector + privacy policy exist.
    • Recorder — the consent gate; when disabled it short-circuits before ever touching the sink (different branch from "sampled out"), and swallows sink errors (best-effort).
    • CategorizeError — maps wrapped-error signatures to a closed enum; the raw err.Error() (which can embed paths/IPs/usernames) is never returned or transmitted.
  • internal/store: telemetry consent persisted in the global $DEVSTACK_HOME/config.yaml (never committed, never in the ledger). Default false; enable mints a UUIDv4 install id + consentAt; disable clears them.
  • internal/cli: real telemetry status | enable | disable | show group, each with a --json non-TTY equivalent. show prints, byte-for-byte, the exact allowlisted event that would be sent and performs zero network I/O. Removed only the telemetry line from stubs.go.

Why

Lets a small OSS team eventually learn coarse usage/failure patterns without ever betraying trust — but ships the trust primitives (consent, allowlist, redaction, radical-transparency show) first and defers any real endpoint.

Ship-empty — no endpoint wired

There is no real network endpoint in this change. DefaultEndpoint is empty and DefaultSink() is the no-op sink; the HTTPSink exists only as a disabled, documented placeholder. Enabling telemetry currently sends nothing.

How tested

CGO_ENABLED=0 go build ./..., CGO_ENABLED=1 go test ./internal/..., gofmt -l (clean), go vet ./... — all green. New tests cover: disabled Recorder never calls a mock sink; enable→disable round-trips in the global config (and doesn't clobber shared services); Event is allowlist-only and redacts a raw error string (no path/IP/secret leak in the marshaled payload); duration bucketing; error categorization; --json status; and telemetry show emits only allowlisted keys.

🤖 Generated with Claude Code

Ship-empty, privacy-first telemetry scaffold. No real network endpoint is
wired: the only sink used is the no-op sink, so even when enabled nothing
leaves the machine.

- internal/telemetry: allowlist-only Event model (no free-form map / escape
  hatch), Sink interface, NoopSink (the only wired sink), a pluggable-but-
  unused HTTPSink (disabled, empty default endpoint, clear TODO), a Recorder
  consent gate that short-circuits before touching the sink when disabled, and
  CategorizeError mapping wrapped-error signatures to a closed enum (never the
  raw err text).
- store: telemetry consent persisted in $DEVSTACK_HOME config.yaml (default
  false); enable mints a UUIDv4 install id + consentAt, disable clears them.
- cli: `telemetry status|enable|disable|show`, each with --json; removed the
  telemetry stub line from stubs.go and wired the real command.

Tests: disabled Recorder never calls the sink (mock); enable->disable
round-trips in the global config; Event is allowlist-only and redacts raw
error strings (no path/IP/secret leak); duration bucketing; error categorize;
--json status; `telemetry show` prints only allowlisted keys and does no I/O.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@gustavobertoi
gustavobertoi merged commit eea55d4 into main Jul 1, 2026
4 checks passed
@gustavobertoi
gustavobertoi deleted the feat/telemetry-optin branch July 1, 2026 12:41
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