Analytics Event Contract

Owner: Engineering · Schema version: 1 · Last updated: 2026-08-12

This document is the single source of truth for every analytics event PinePaper sends. A release cannot add an event through the standard bridge (window.editorAnalytics.track / window.trackEditorEvent) without it being listed here — the Jest contract tests in __tests__/AnalyticsContract.test.js enforce this.

Principles

  1. One user action → one GA4 hit. All dispatch goes through window.editorAnalytics.track(); there are no direct gtag('event', …) calls in editor code or pwa-analytics.js.
  2. Consent first. analytics.isEnabled() short-circuits every event when the user has Do Not Track, a persisted local opt-out, or ?notrack=1.
  3. No PII. Events never carry canvas/image content, free text, prompts, filenames, emails, raw error strings, or raw/high-cardinality IDs. The PII-key guard strips these in dev (loudly) and the tests enforce the same whitelist in CI.
  4. Low cardinality. Every custom-dimension value is a governed enum. Numeric values are bucketed (e.g. under_10s, 10_30s), never raw.

Common context (auto-attached to every editor event)

Key Type Values
event_schema_version string "1"
surface enum editor
entry_point enum direct, landing_cta, template, recent_project, shared_link
platform_form_factor enum desktop, tablet, mobile
app_version string PinePaper release version (build-injected)
session_stage enum new, returning (sessionStorage marker, not a persistent profile)

Event taxonomy

Activation / entry

Event Trigger Params
editor_opened Editor fully usable, once per session entry_point, surface, app_version
start_path_selected User picks template / AI / import / blank start_path (template·ai·import·blank), ui_location
template_gallery_opened Gallery becomes visible entry_point
template_selected User explicitly chooses a template template_category, template_tier, selection_source
blank_canvas_started Blank project started canvas_preset, selection_source
import_started Image/file import begins import_type, ui_location

Milestones (one per session each)

Event Trigger Params
creation_milestone_reached First time a session reaches a level milestone (first_content·first_customization·first_outcome), creation_path
creation_abandoned User leaves before first_outcome (pagehide, once) last_milestone, active_duration_bucket, creation_path

Outcomes

Event Trigger Params
project_saved Local save / autosave after meaningful work save_mode (auto·manual), project_state (new·existing)
export_completed Export succeeds format, duration_bucket, size_bucket, creation_path
export_failed Export fails format, failure_stage, error_category
share_started Share flow opens share_destination
share_completed Share succeeds share_destination
continue_project_opened Returning user resumes a project project_age_bucket, entry_point

Existing interaction events (kept, not milestones)

item_created, animation_applied, undo, redo, tool_selected, item_deleted, import_dialog_opened, canvas_cleared, canvas_size_changed, image_uploaded, image_crop_applied, image_mask_applied, image_chroma_key_applied, lasso_*, gpu_filter_*, mobile_tab, magic_*, code_*, cloud_*, chat_handoff_opened, clientllm_failure, assistant_report_consent, and the PWA lifecycle events (pwa_install_eligible, pwa_install_choice, pwa_installed, pwa_launched_standalone).

Interaction events (item_selected, hovers, pane switches, slider drags, undo, canvas resize) are deliberately not milestones and never count toward activation.

Migration note

template_loaded remains during migration and is then renamed/mapped to the template_selected contract without double firing. During the overlap both fire (one legacy, one new), then template_loaded is removed.

Bucketing + error categorization

Never send raw durations / sizes / errors. js/core/analyticsCore.js exports pure helpers used by the editor bridge:

  • bucketDuration(s)under_10s · 10_30s · 30_60s · over_60s
  • bucketActiveDuration(s)under_30s · 30s_2m · 2_10m · over_10m
  • bucketSize(bytes)under_1mb · 1_5mb · 5_20mb · over_20mb
  • bucketProjectAge(days)under_1d · 1_7d · 7_30d · over_30d
  • categorizeExportError(e)unsupported_capability · encoding_rendering · memory_size · permission_download · unknown
  • creationPathFromStart(path)template · ai · import · blank

Event dispatch points

The bridge listens for these CustomEvents (emitted by the editor subsystems):

CustomEvent Emitted by GA4 events
itemCreated PinePaper.create item_created + first_content milestone
templateLoaded TemplateManager template_loaded + template_selected + first_content
imageUploaded ImageToolsManager image_uploaded + first_content
blankCanvasStarted onboarding picker (UX-01) blank_canvas_started + first_content
exportStarted / exportCompleted / exportFailed ExportEngine export lifecycle + first_outcome
projectSaved HistoryManager._persistToStorage project_saved + first_outcome
shareStarted / shareCompleted ExportEngine share dialog share lifecycle + first_outcome
projectResumed session-recovery restore continue_project_opened

Milestones (first_content, first_customization, first_outcome) are one-per-session via a sessionStorage flag; creation_abandoned fires on pagehide only when the user started but never reached first_outcome.

Banned parameter keys (stripped by the PII guard)

error, message, prompt, content, text, description, detail, filename, file_name, src, url, path, href, source, email, user_id, userid, uid, id, *_id.

Prefer bucketed categories (error_category, duration_bucket, size_bucket) over raw values.

Adding an event

  1. Add a row to this table with: owner, business question, trigger, parameters.
  2. Add a test case to __tests__/AnalyticsContract.test.js.
  3. Use trackEvent(name, params) (or window.trackEditorEvent) — never gtag directly.