feat(telemetry): opt-in anonymous boot and learning-funnel events - #94
Merged
Conversation
…he first validator run
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 of Changes
Adds strictly opt-in, anonymous product telemetry, sent as Plausible-format custom events to a build-time-configurable endpoint (
VITE_TELEMETRY_ENDPOINT/VITE_TELEMETRY_DOMAIN; an empty endpoint hard-disables telemetry entirely). Eleven events, mirrored verbatim in the README's Telemetry table:app_started(once per page load), then one Docker readiness probe reported asruntime_check_started→runtime_ready|runtime_failed { reason }.GET /healthnow returns a stable, non-identifyingchecks.docker.reason(socket_not_found,permission_denied,connection_refused,timeout,unknown); the client addsbackend_unreachableandtimeout(10 s cap on the probe).image_pull_failed { node }fires when a node can't be created because its image failed to download (backendIMAGE_NOT_FOUND).first_validator_run { roadmap, step }fires once per install, on the first validation ever run.roadmap_started,step_validated,step_failed,roadmap_completed,roadmap_abandoned.unset/accepted/declined, localStorage keytorollo_telemetry_consent): anything other than an explicit accept means zero network requests — not even the boot health probe. A one-time, non-blocking consent card on the home screen asks the question; a header toggle makes the choice reversible at any time. Accepting mid-session fires the boot funnel right away.TelemetryEvents— props are catalogue ids or closed enums only, plus the app version and a random locally-generated install id. No socket paths, no error messages, no project names. Revoking consent deletes the install id and the "first time" markers tied to it, so re-enabling starts a fresh identity.step_failedonly counts pedagogical failures (engineerrorresults are not counted);roadmap_startedonly fires when the backend progress store is confirmed empty;roadmap_completedfires once, on the validation that completes the roadmap;roadmap_abandonedfires once per play-through on close/unmount/pagehide (withkeepalive).probeDockerHealth()extracted fromuseDockerHealth(with the timeout, so the briefing page's Docker check can no longer hang),readApiError()next toreadErrorMessage()to surface the backend's error code.Types of Changes
Verification & Testing
Automated Checks
npm run lintsuccessfully with no errorsnpm run buildsuccessfully with no compilation errorsnpm testsuccessfully (all tests pass)Backend 456 tests (new:
classifyDaemonFailurematrix,/healthreason contract). Frontend 340 tests, new suites: consent store (tri-state, install-id + milestone lifecycle, storage-disabled fallback),trackEvent(zero fetch when declined/unset, exact prop allowlist per event, URL routing, keepalive passthrough, never throws),claimMilestone,probeDockerHealth(ok / reason passthrough / unknown reason / backend unreachable / timeout),useBootTelemetry(nothing without consent — not even the probe; fires once per page load; fires on mid-session accept),useContainers(IMAGE_NOT_FOUND→image_pull_failed, other codes don't), and the player hook wiring (first_validator_runonce per install, not on a failed request, plus the five roadmap events with their negatives).Manual Verification
E2E against the real backend + real Docker, with a local Plausible-shaped capture server and Playwright driving the UI, across three backend states (23/23 checks):
/healthprobe; accepting on the card →app_started+runtime_check_started+runtime_ready; consent already accepted → the same three on load,app_startedexactly once (StrictMode); opening a roadmap and validating for real →first_validator_run(roadmap + step ids only) before the verdict event, a second Validate doesn't repeat it, the milestone is persisted, a reload replays the boot funnel but not the milestone.DOCKER_HOSTpointing at a missing socket):/healthanswers 503 withreason: socket_not_found; the app reportsruntime_failed { reason: 'socket_not_found' }.runtime_failed { reason: 'backend_unreachable' }.image_pull_failedis covered by tests on both sides of the contract (backend 502IMAGE_NOT_FOUNDon a failed pull; client code → event); a live repro would require removing a host image, so it was not run against real Docker.Checklist
🤖 Generated with Claude Code