feat(cli): anonymous opt-out product analytics (PostHog via cloud pulse proxy) - #133
Open
azlekov wants to merge 7 commits into
Open
feat(cli): anonymous opt-out product analytics (PostHog via cloud pulse proxy)#133azlekov wants to merge 7 commits into
azlekov wants to merge 7 commits into
Conversation
… facts, event model, queue table Adds the dira_core::telemetry module: TelemetryKnobs (default-on, telemetry.enabled knob + KNOBS row), lazily minted anonymous install id + 32-byte salt in meta (never derived from the device key), pure repo-facts capture (host classification + salted HMAC-SHA256 of the canonical remote — never the plaintext), the closed TelemetryEvent enum with per-variant no-stray-field wire tests, serde-only wire types with a deterministic batch id, and the append-only telemetry_events queue table (migration 0006) with id-cursor store methods. Wire types deliberately live outside /contract: telemetry is best-effort and versioned independently (v: 1), so taxonomy changes never ride the contract release train. Why: local zod/serde pair instead of a contract type; unsigned batches so unlinked installs can report. Rejected: reusing the Ed25519 device identity as distinct_id (couples analytics to the signing identity; forbidden by design). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Signed-off-by: Asen Lekov <asenlekoff@gmail.com>
…d pulse endpoint
Adds Request::IngestTelemetry and the telemetry_sync channel: control-socket ingest
appends to the local queue (consent re-checked daemon-side), and a knowledge_sync-shaped
background task drains (cursor, until] chunks to {cloud_url}/api/v1/pulse over the
shared TLS-pinned client, advancing the cursor per accepted chunk on its own 2xx and
backing off via the shared ladder. Gated on cloud_url + telemetry.enabled only — never
device linkage, so unlinked installs report anonymously. Daemon start/stop lifecycle
events enqueue through the same path; nuke clears the queue, cursor, and health.
Why: 400 responses advance past the poison chunk (loudly, rows kept for inspection)
so a malformed batch can never wedge the queue; 404 is a quiet endpoint-missing skip
for older clouds.
Rejected: signing telemetry batches — would couple analytics to the device identity.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Signed-off-by: Asen Lekov <asenlekoff@gmail.com>
…d the TELEMETRY.md disclosure Adds the DIRASH-0030-shaped consent step: TELEMETRY_DISCLOSURE names exactly what is sent and what never is, shown on every path before any branching; the confirm defaults to on (opt-out), a decline writes telemetry.enabled=false through config_cmd, and an answer equal to the current state writes nothing. --telemetry <on|off> skips the prompt like --knowledge; --yes keeps the default. docs/TELEMETRY.md is the user-facing disclosure (taxonomy, privacy guarantees, EU residency, kill switches) with a README pointer. A wording test pins the disclosure to the shipped property categories. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Signed-off-by: Asen Lekov <asenlekoff@gmail.com>
…etry Thin main() now times the dispatched command and records cli_command_executed (top-level command name only, duration, success, closed error-kind classification — never message text) through a 150ms-budgeted UDS fire-and-forget that can never slow or fail a command. TelemetryGate makes the documented kill switches real: CI, dev builds, DO_NOT_TRACK, DIRA_TELEMETRY_ENABLED=0, and the config knob. The first interactive run prints the disclosure to stderr once (marker file). Consent changes emit cli_consent_recorded from both the onboarding step and config set; device link fetches the daemon-minted install id (gate-checked, never blocking the link) so the cloud can alias install->device at claim. The canonical repo ref crosses only the local socket: the daemon hashes it with the per-install salt before anything is stored, so plaintext repo identity never lands in the queue. Why: disabling telemetry still emits one final consent_recorded(enabled=false) so the opt-out itself is measurable; every other switch suppresses even that. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Signed-off-by: Asen Lekov <asenlekoff@gmail.com>
The decision pins the structural rules — opt-out with tested disclosure, random install identity never derived from the device key, daemon-side salted repo hashing, unsigned consent-gated channel — and RULES.md gains the matching standing rule. Spec: telemetry Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Signed-off-by: Asen Lekov <asenlekoff@gmail.com>
… never blocking ingest telemetry_sync::ingest now resolves public|private for github.com and gitlab.com remotes via an unauthenticated GET to the forge that already hosts the repo (200 -> public, 404 -> private-or-absent, anything else -> unknown), 2s timeout, dirad/<ver> user agent, no tokens or cookies ever. Answers cache 24h (10min for unknown) keyed by the salted repo hash — the plaintext ref is never a cache key — capped at 256 entries with in-flight bounding. A cold cache stores the first event as unknown and a detached probe warms the cache for the next; stored rows are never retro-updated. Bitbucket/self-hosted short-circuit to unknown with no request. Records DIRASH-0034: the probe sends the repo name only to the forge that hosts it, never to Dira's cloud — an explicit carve-out under DIRASH-0033's anonymity guarantee. Spec: telemetry Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Signed-off-by: Asen Lekov <asenlekoff@gmail.com>
…dentity race, error taxonomy, queue cap Review pass fixes: the disclosure (TELEMETRY.md + TELEMETRY_DISCLOSURE + wording test) now states that public/private is checked with the repo own forge, never with Dira (DIRASH-0034); install identity is minted once through a OnceCell on AppState, fixing a first-run salt race between concurrent ingests and removing two SQLite reads per event; classify_error no longer tags any "failed" message as daemon_error (closed marker list + SilentExit downcast); the generic daemon-failure path returns a SilentExit sentinel so failed commands record telemetry before exiting 1; the local queue is capped at 5000 rows so no-cloud installs stay bounded; dead ConsentSource variants (env/default) are removed along with the doc promise they implied; "config set telemetry.enabled off" no longer records its own invocation (fresh gate for config commands); socket budgets and the on/off parser are shared instead of copied; the ULID-ordering-dependent visibility test now identifies rows by payload. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Signed-off-by: Asen Lekov <asenlekoff@gmail.com>
Contributor
Author
|
Companion cloud PR: dodi-smart/dirahq-cloud#146 |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Adds anonymous, opt-out product analytics to the CLI: which commands run, how long they take, whether they succeed, and coarse facts about the repo they run in (host class, public/private, a per-install-salted hash) — flushed through the daemon to the Dira cloud's
/api/v1/pulseproxy and forwarded server-side to PostHog Cloud EU. Full user-facing disclosure indocs/TELEMETRY.md; structural rules recorded as DIRASH-0033/0034 with atelemetryliving spec.How it works
cli/dira/src/telemetry.rs): thinmain()wraps dispatch with a timer and aTelemetryGate(config knob,DIRA_TELEMETRY_ENABLED,DO_NOT_TRACK,CI, dev builds — any one suppresses). Emission is a 150ms-budgeted fire-and-forget over the control socket; the CLI never does network I/O for telemetry (D-0006 generalized).cli/dirad/src/telemetry_sync.rs): appends to a capped (5000-row) SQLite queue, hashes the canonical repo ref with a per-install salt before anything is stored, and drains in a knowledge_sync-shaped loop — per-chunk cursor advance on 2xx (D-0020), shared backoff ladder (DIRASH-0031), shared TLS-pinned client (D-0011). Gated on consent +cloud_url, never on device linking, so unlinked installs report anonymously.cli/dirad/src/repo_visibility.rs): unauthenticated probe of github.com/gitlab.com (200→public, 404→private, else unknown), cached 24h keyed by the salted hash. The repo name goes only to the forge that already hosts it — never to Dira (DIRASH-0034); stated in the disclosure.TELEMETRY_DISCLOSUREshown on every path and a wording test pinning it to the shipped fields; one-time first-run notice on stderr;--telemetry <on|off>; consent changes emitcli_consent_recorded.meta, never derived from the Ed25519 device key.dira device linkpasses the install id so the cloud can alias install → device at claim.Privacy invariants (test-enforced)
Never sent: argv, paths, repo names, git identity, error text. The wire enum is closed with per-variant no-stray-field tests;
error_kindis a closed taxonomy classified from error types. Changing what ships requires changing the disclosure in the same commit (standing rule).Companion
dodi-smart/dirahq-cloudPR adds the/api/v1/pulseproxy + server-side allowlist. No hard merge-order constraint: the daemon treats a missing endpoint as a quiet skip, and the cloud ignores a missinginstallId.Testing
just cigreen (no contract drift — telemetry types deliberately live outside/contract, versionedv: 1). 1196 workspace tests, 0 failures, clippy-D warningsclean. An 8-angle review pass ran over the branch; all 10 findings fixed in the final commit.🤖 Generated with Claude Code