The rejected PR #1110 had a genuinely careful PII-free event design, but shipped
three things a local-first app can't: exception autocapture ON (raw tracebacks —
home paths, and in this codebase HF tokens out of exception messages — bypassing
core.failure.sanitize() entirely), no user consent or disclosure, and 3,069 lines
of PostHog wizard scaffolding. This is the same capability with those fixed.
core/analytics.py, three rules, each enforced and tested rather than promised:
1. OFF unless the user says yes. TWO gates must both be true: a build-provided
POSTHOG_PROJECT_TOKEN *and* the user's analytics_enabled pref, default False.
A default install transmits nothing, so "nothing leaves your machine" stays
literally true for everyone who doesn't opt in. A broken prefs file fails
CLOSED. OMNIVOICE_ANALYTICS_DISABLED=1 is a hard kill switch above both.
Withdrawing consent tears the client down immediately — no restart.
2. NO exception autocapture. Explicitly disabled; a test asserts the constructor
arg, because the SDK's default is the leak.
3. Metadata ONLY, by allowlist. Every property passes sanitize_properties(),
which DROPS any key not on _ALLOWED_PROPS and refuses long strings — so no
future caller can leak a take's text, a path, or a voice name by adding a
field. text_length is the LENGTH; the text itself has no way through.
The person id is a random per-install UUID — not hardware, hostname, or username.
UI: Settings → Privacy → "Help improve OmniVoice" states in the panel exactly
what is sent, exactly what never is, and that it can be turned off — rather than
burying it in a policy. No destination in the build (any source build) → the
toggle isn't shown, because an inert switch would be a lie.
Docs: README FAQ answers "does OmniVoice collect any data about me?" honestly.
Also fixed a bug I'd introduced in my own wiring: the generation event referenced
variables not in scope, and the call site's bare `except: pass` swallowed the
NameError — so the event would have silently never fired. The call site now logs.
12 tests (default-off / opt-in without token still can't transmit / both gates /
kill switch / consent withdrawal / prefs failure fails closed / allowlist drops
text+paths+names / long strings refused / autocapture OFF / never raises /
random install id). Backend 2936 passed; frontend 1211 passed.
Refs #1110
Co-authored-by: mergetest <nizam4103@gmail.com>