Contributor reference for every analytics event SnapOtter can emit, what each carries, and where it fires. For the user-facing summary and opt-out steps, see the published [telemetry guide](apps/docs/guide/telemetry.md). For the privacy stance, see the in-app privacy policy.
Product analytics is on by default and set instance-wide by an admin under Settings > System > Privacy. Nothing is sent when it is off.
## Source of truth
The code is authoritative; this doc is the map. A drift test (`tests/unit/shared/telemetry-doc-drift.test.ts`) asserts every event name in `ANALYTICS_EVENTS` appears here, so add a row when you add an event.
- Every event passes a strict per-event property allowlist before it leaves the process, on both the client and the server. Anything not listed is dropped, so filenames, tool settings, and free text cannot leak.
- We never send file names, paths, contents, OCR text, EXIF, extracted document text, IP address, or account identity. The single exception is feedback contact details (email, name, company), and only when the user ticks the contact-consent box.
-`instance_id` rides events as a property, not an `identify()` call. Events stay anonymous and person-less while still rolling up per instance.
- Autocapture and session replay are off. Exceptions go to Sentry, not PostHog.
- One opt-out gate stops all egress: `analyticsEnabled()` on the server, the live `enabled` flag on the client, and a build-time bake (`SNAPOTTER_ANALYTICS=off`) that can strip it entirely.
## Server events
Emitted from `apps/api` through `trackEvent()`; properties are filtered by `analytics-allowlist.ts`.
| Event | Fires when | Key properties |
| --- | --- | --- |
| `instance_started` | Once per boot | `arch`, `os_platform`, `deploy_mode`, `gpu_present` |
| `auth_login` | A login succeeds | `method` (`password` or `oidc`) |
| `auth_login_failed` | A login attempt fails | `method` (`password` or `oidc`) |
| `pipeline_executed` | A pipeline run finishes | `step_count`, `tool_ids`, `is_batch`, `file_count`, `duration_ms`, `status` |
| `ai_bundle_action` | An AI bundle is installed, uninstalled, reset, or imported | `bundle_id`, `action`, `duration_ms` |
### Feedback events
Both ride `POST /api/v1/feedback` and go through `cleanFeedbackProperties()`, a cleaner separate from the allowlist above. Enum values live in `feedback.ts`.
The onboarding survey is a profiling questionnaire, not feedback, so it gets its own event. Splitting the two keeps onboarding responses from swamping feedback metrics.
## Client events
Emitted from `apps/web` through `track()`; properties are filtered by the `ALLOWED` map in `analytics.ts`.
| `feedback_prompt_dismissed` | A feedback surface is dismissed without submitting | `source`, `survey_id`, `prompt_variant`, `dismiss_kind` (`close`, `dont_ask_again`, or `snooze`) |
posthog-js also captures `$pageview` and `$pageleave` on route changes, plus `$web_vitals`. These skip the `track()` allowlist, so the `before_send` hook that strips query strings and fragments from URLs is the boundary for them.