Owner-sanctioned reversal: the publishable write-only PostHog client key is
committed as the in-repo default in backend/core/analytics.py and
frontend/src/utils/analytics.ts (env / baked release token still wins), so
source builds show the same first-run consent ask as installers — skip = off,
nothing is ever sent without an explicit yes. Adds an install_channel property
(installer / docker / source) to lifecycle events, stamped by the desktop shell
via OMNIVOICE_INSTALL_CHANNEL and by the Docker image's existing
OMNIVOICE_SERVER_MODE marker. Guard tests now pin the two-canonical-files
allowlist + same-token invariant, and the uninstall-ping info file works on the
default token.
Fixes#1193
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
New content-free events on the same rails as everything else (dual gate:
build token AND explicit consent; allowlisted metadata only; never raises):
- app_installed — once per install, fired the first time the install is both
consented and configured (consent lands mid-first-run, so the marker is
only burned when the event was actually sent).
- app_updated — persisted last-version marker vs current; from_version /
to_version. The marker advances on every startup, consented or not, so a
later opt-in never replays pre-consent history.
- app_crashed — ONE authoritative source: the backend run sentinel (#1164).
The desktop shell's markers describe the same deaths (its watcher restarts
the backend, whose next startup finds the sentinel), so the frontend never
emits a crash event — no double-count, pinned by test. Props: exit_kind,
coarse stage, BUCKETED uptime (never raw seconds), version, os.
- error_occurred — hooked into core.error_journal.record: error_class +
route-head stage only, deduped by journal fingerprint, hard cap 10/session.
Allowlist extended (from_version/to_version/exit_kind/uptime_bucket/
error_class/stage) in backend and frontend alike, now pinned equal by a
locked mirror test. Also writes DATA_DIR/analytics_info.json (present iff
analytics is enabled; removed on opt-out) so the uninstall scripts can send
a consent-gated app_uninstalled ping without any baked token.