app_version was attached only to feedback events; every other allowlist stripped it, so the install base could not be segmented by release. One property on the once-per-boot census event covers it. Fixes #674.
5.6 KiB
Telemetry event dictionary
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. 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.
- Event names:
packages/shared/src/analytics/events.ts(ANALYTICS_EVENTS) - Server property allowlist:
apps/api/src/lib/analytics-allowlist.ts - Client property allowlist: the
ALLOWEDmap inapps/web/src/lib/analytics.ts - Feedback enum values:
packages/shared/src/analytics/feedback.ts
Invariants
- 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_idrides events as a property, not anidentify()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 liveenabledflag 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, app_version |
auth_login |
A login succeeds | method (password or oidc) |
auth_login_failed |
A login attempt fails | method (password or oidc) |
tool_used |
A tool job finishes | tool_id, status, duration_ms, category, is_ai_tool, is_batch, input_format, output_format, bytes_in, bytes_out, execution_hint, error_code, error_kind |
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.
| Event | Fires when | Key properties |
|---|---|---|
feedback_submitted |
A user submits genuine feedback (nav button, tool result, failed job, search miss, admin installer card) | source, sentiment, feedback_type, message, survey_id, prompt_variant, tool_id, search_query, job_status, error_category, contact_ok, and, only with consent, contact_email / contact_name / company |
onboarding_survey_submitted |
A user completes the onboarding usage survey (source: onboarding) |
usage_type, prior_tool, selfhost_motivation, discovery_source, survey_id, prompt_variant |
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.
| Event | Fires when | Key properties |
|---|---|---|
tool_opened |
A tool page opens | tool_id, category, modality |
file_added |
Files are added | file_count |
tool_started |
Processing starts | tool_id, is_batch, file_count |
batch_processed |
A batch run finishes | tool_id, file_count, status |
result_downloaded |
A result is downloaded | tool_id |
result_saved |
A result is saved to the library | tool_id |
search |
A tool search runs | results_count, clicked_tool_id |
tool_client_error |
The React error boundary catches a crash | error_name |
ai_bundle_prompted |
An AI install prompt is shown | bundle_id |
editor_opened |
The image editor opens | none |
editor_tool_used |
An editor tool is selected | editor_tool |
editor_exported |
An editor export runs | output_format |
pipeline_opened |
The Automate page opens | none |
pipeline_step_added |
A step is added to a pipeline | tool_id |
pipeline_saved |
A pipeline is saved | step_count |
pipeline_template_selected |
A pipeline template is picked | template_id |
sponsor_clicked |
The sponsor link is clicked | none |
feedback_prompt_shown |
A feedback surface becomes visible (usage survey, per-job prompt, admin install card, nav dialog, search miss) | source, survey_id, prompt_variant |
feedback_prompt_dismissed |
A feedback surface is dismissed without submitting | source, survey_id, prompt_variant, dismiss_kind (close, dont_ask_again, or snooze) |
SDK-generated events
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.