open-design/packages/contracts/src/analytics/events.ts
elihahah666 1e9be2fdb5
Add tracking for Comment panel save/send actions (#3098)
Track "Save comment" and "Send to chat" button clicks in the comment
popover with a new `comment_popover` area, so we can measure the
distribution of save vs send-to-chat usage.

Co-authored-by: qiongyu1999 <2694684348@qq.com>
2026-05-27 09:23:19 +00:00

2349 lines
69 KiB
TypeScript

// Typed catalog for the v2 analytics schema. The wire format collapses to
// four core event names (`page_view`, `ui_click`, `surface_view`, plus the
// `*_result` family) and identifies the surface through the
// `page_name` + `area` + `element` triplet rather than the v1 per-page event
// names. Configure-state triplet (`has_available_configure_cli` /
// `configure_type` / `configure_availability`) is supplied via the global
// register in `apps/web/src/analytics/client.ts`; it does NOT appear in the
// per-event prop types below.
import type {
TrackingConfigureAvailability,
TrackingConfigureType,
} from './public-params.js';
// ---- Event names ---------------------------------------------------------
export type AnalyticsEventName =
// Core triad
| 'page_view'
| 'ui_click'
| 'surface_view'
// Project lifecycle
| 'project_create_result'
| 'plugin_replacement_result'
// Run lifecycle (daemon authoritative)
| 'run_created'
| 'run_finished'
// Packaged updater lifecycle
| 'update_install_result'
| 'update_apply_observed'
// File manager
| 'file_upload_result'
// Artifact
| 'artifact_export_result'
// Feedback
| 'feedback_submit_result'
| 'assistant_feedback_click'
| 'assistant_feedback_reason_view'
| 'assistant_feedback_reason_click'
| 'assistant_feedback_reason_submit'
// Settings
| 'settings_view'
| 'settings_cli_test_result'
| 'settings_byok_test_result'
| 'settings_connector_auth_result'
// Onboarding-only result events. UI clicks + page_views inside the
// onboarding flow reuse the generic `ui_click` / `page_view` shapes
// with `page_name=onboarding`; the three `onboarding_*` names below
// capture lifecycle moments that don't fit a click or a view.
| 'onboarding_runtime_scan_result'
| 'onboarding_complete_result'
// Design-system lifecycle. Clicks + page_views inside DS surfaces
// reuse `ui_click` / `page_view`; the five names below capture
// ingest / create / review / status / picker-apply moments.
| 'design_system_source_ingest_result'
| 'design_system_create_result'
| 'design_system_review_result'
| 'design_system_status_result'
| 'design_system_apply_result';
// ---- Pages ---------------------------------------------------------------
export type TrackingPageName =
| 'home'
| 'projects'
| 'automations'
| 'plugins'
| 'design_systems'
// `design_system_project` is the per-DS surface (preview / generation
// dialog inside a specific design system). Distinct from the
// `design_systems` list page.
| 'design_system_project'
| 'integrations'
| 'chat_panel'
| 'file_manager'
| 'artifact'
| 'onboarding'
// `studio` is the in-project workspace that hosts the chat composer and
// the design system picker. Reported when a DS picker / module renders
// inside a project.
| 'studio'
| 'settings';
// Alias kept for backwards-compatibility inside the contracts file; v2 wire
// format uses the field name `page_name` for settings events too.
export type TrackingSettingsPage = 'settings';
// ---- Shared enums --------------------------------------------------------
export type TrackingProjectKind =
| 'prototype'
| 'live_artifact'
| 'slide_deck'
| 'template'
| 'image'
| 'video'
| 'audio'
// `design_system` covers DS-as-project runs (creation + regeneration).
// The dashboard reads it on run_created / run_finished to split the
// DS generation funnel from regular artifact runs.
| 'design_system'
| 'other';
// Where a project originated. Matches CSV row 9 / row 17 enum.
export type TrackingProjectSource =
| 'create_button'
| 'import_claude_design_zip'
| 'open_folder'
| 'template'
| 'chat_composer'
| 'unknown';
// The six tabs inside the New project modal (CSV row 7 tab_name).
export type TrackingNewProjectTab =
| 'prototype'
| 'live_artifact'
| 'slide_deck'
| 'from_template'
| 'media'
| 'other';
export type TrackingFidelity =
| 'wireframe'
| 'high_fidelity'
| 'not_applicable';
export type TrackingExecutionMode = 'local_cli' | 'byok';
// v2 BYOK provider catalogue (CSV row 65). Replaces v1's
// `anthropic|openai|azure|ollama|google`. `senseaudio` was added on
// `main` after the v2 doc was published; we forward it verbatim so
// dashboards can split it out even though the product CSV does not yet
// list it.
export type TrackingByokProviderId =
| 'anthropic'
| 'openai'
| 'azure_openai'
| 'google_gemini'
| 'ollama_cloud'
| 'senseaudio';
// v2 CLI provider catalogue (CSV row 63 + image 59). Adds `qoder_cli` and
// `kilo` over v1.
export type TrackingCliProviderId =
| 'claude_code'
| 'codex_cli'
| 'devin_for_terminal'
| 'gemini_cli'
| 'opencode'
| 'hermes'
| 'kimi_cli'
| 'cursor_agent'
| 'qwen_code'
| 'qoder_cli'
| 'github_copilot_cli'
| 'pi'
| 'kilo'
| 'other';
export type TrackingArtifactKind =
| 'html'
| 'markdown'
| 'image'
| 'video'
| 'audio'
| 'doc'
| 'unknown';
export type TrackingExportFormat =
| 'pdf'
| 'pptx'
| 'zip'
| 'html'
| 'markdown'
| 'template'
| 'vercel'
| 'cloudflare_pages';
export type TrackingResult = 'success' | 'failed';
export type TrackingRunResult = 'success' | 'failed' | 'cancelled';
export type TrackingExportResult = 'success' | 'failed' | 'cancelled';
export type TrackingTestResult = 'success' | 'failed' | 'timeout';
export type TrackingFeedbackRating = 'positive' | 'negative';
// Click events emit `none` when the user clears a previously-set rating, so
// `rating` (post-state) and `rating_before` (pre-state) on click both use
// this widened union. Reason events still require a concrete rating.
export type TrackingFeedbackRatingWithNone = 'positive' | 'negative' | 'none';
export type TrackingFeedbackAction =
| 'submit_feedback_rating'
| 'clear_feedback_rating';
// Mirrors ChatMessageFeedbackReasonCode in packages/contracts/src/api/chat.ts.
// Kept independent so the analytics wire format can evolve without forcing
// a contract bump on the chat persistence shape.
export type TrackingFeedbackReasonCode =
| 'matched_request'
| 'strong_visual'
| 'useful_structure'
| 'easy_to_continue'
| 'followed_design_system'
| 'missed_request'
| 'weak_visual'
| 'incomplete_output'
| 'hard_to_use'
| 'missed_design_system'
| 'other';
// Product confirmed on 2026-05-13: custom_reason ships the raw text so
// analysts can read the actual feedback. The earlier length-bucket approach
// from the tracking doc draft is no longer in effect.
export type TrackingTokenCountSource =
| 'provider_usage'
| 'estimated'
| 'unknown';
export type TrackingDesignSystemSource =
| 'default'
| 'user_selected'
| 'template_inherited'
| 'project_saved'
| 'not_applicable'
| 'unknown';
export type TrackingFileType =
| 'image'
| 'video'
| 'audio'
| 'pdf'
| 'zip'
| 'folder'
| 'other';
export type TrackingFileSizeBucket =
| '0_1mb'
| '1_10mb'
| '10_100mb'
| '100mb_plus';
// ---- page_view ------------------------------------------------------------
// `source` is supplied only by the chat_panel page_view (CSV row 41), where
// it records which surface launched the studio.
export type TrackingChatPanelPageViewSource =
| 'new_project'
| 'chat_composer'
| 'recent_project'
| 'projects_list'
| 'template'
| 'automation'
| 'deeplink'
| 'reload'
| 'unknown';
// --- Onboarding page_view (welcome flow) ---
//
// CSV row "Onboarding / page_view". Fires once per step exposure inside the
// welcome flow. The current first-run flow is Connect → About you; the
// design-system and generation literals remain in the contract for historical
// rows and a future reintroduction. Each step's `step_index` / `step_name`
// must match the enum pairs below. `onboarding_session_id` is generated once
// per session so dashboards can stitch the funnel.
export type TrackingOnboardingArea =
| 'runtime'
| 'about_you'
| 'design_system'
| 'generation_progress';
// Mixed string enum: numeric steps render as the strings `'1' | '2' | '3'`
// and the generation phase as `'progress'`. Mirrors the v2 doc literally.
export type TrackingOnboardingStepIndex = '1' | '2' | '3' | 'progress';
export type TrackingOnboardingStepName =
| 'connect'
| 'about_you'
| 'design_system'
| 'generation';
// How the user chose to connect to a model provider. `amr_cloud` is the
// hosted offering the doc references; today the UI ships only
// `local_cli` (Local Coding Agent) and `byok` (own model key). `none`
// stamps the click events fired before any runtime was picked.
export type TrackingOnboardingRuntimeType =
| 'amr_cloud'
| 'local_cli'
| 'byok'
| 'none';
// What kind of source material the user pinned in the design-system
// step. `text` covers the brand description textarea; `mixed` is
// reserved for batches that combined more than one type.
export type TrackingOnboardingSourceType =
| 'text'
| 'github_repo'
| 'local_code'
| 'fig'
| 'assets'
| 'mixed'
| 'none';
// `completed`: user clicked through every step (with or without a DS).
// `skipped`: user clicked Skip from any step. `cancelled`: user closed
// the onboarding tab / navigated away without finishing.
// `failed`: terminal error before completion.
export type TrackingOnboardingCompletionResult =
| 'completed'
| 'skipped'
| 'cancelled'
| 'failed';
export type TrackingOnboardingCompletionType =
| 'completed_with_design_system'
| 'completed_without_design_system'
| 'skipped';
// CLI scan terminal state. `success`: at least one CLI was detected;
// `failed`: scan errored or detected nothing; `timeout`: scan didn't
// settle inside the budget. Daemon doesn't currently surface timeouts
// for this scan; left in the type so a future watchdog can use it
// without a contract change.
export type TrackingOnboardingScanResult = 'success' | 'failed' | 'timeout';
// About-you step values. Surfaces from `onboardingRole*` /
// `onboardingOrg*` / `onboardingUse*` / `onboardingSource*` i18n
// keys; this enum is the wire-format shape the dashboard groups on.
// Kept as `string` rather than literal union because the product
// catalogue extends (e.g. add a new role) more often than the wire
// shape: a stricter union would force a contract bump for every new
// option. `unknown` covers the "user didn't pick / picked Other"
// path explicitly.
export type TrackingOnboardingOrganizationSize = string;
export type TrackingOnboardingUseCase = string;
export type TrackingOnboardingDiscoverySource = string;
// User's self-identified role. Same wire shape and rationale as the
// other About-you survey strings — `pm`, `designer`, `engineer`,
// `marketing`, `growth`, `ops`, `founder`, `student`, `other` are the
// current options from `apps/web/src/components/EntryShell.tsx`
// (`roleOptions`); the type stays open so adding a future role doesn't
// force a contract bump.
export type TrackingOnboardingRole = string;
export interface OnboardingPageViewProps {
page_name: 'onboarding';
area: TrackingOnboardingArea;
step_index: TrackingOnboardingStepIndex;
step_name: TrackingOnboardingStepName;
onboarding_session_id: string;
}
// ---- Onboarding ui_click ------------------------------------------------
//
// One interface so every onboarding click site can write
// `trackOnboardingClick(...)`, regardless of which sub-surface. The doc
// pre-allocates a large element / action enum because the funnel needs
// to discriminate Skip from Back from Continue, runtime choice from
// source selection, etc.
export type TrackingOnboardingClickElement =
// Runtime / connect step
| 'amr_cloud'
| 'local_coding_agent'
| 'byok'
// Action buttons
| 'continue'
| 'back'
| 'skip'
| 'generate'
// About you fields
| 'role'
| 'organization_size'
| 'use_case'
| 'hear_about_us'
// Fires once on Finish-setup, carrying the full survey snapshot
// (role + organization_size + use_case + discovery_source) so the
// funnel always has the user's final picks even when individual
// dropdown clicks were dropped on a fast navigate.
| 'about_you_submit'
// Design system source options
| 'github_repo'
| 'local_code'
| 'fig_upload'
| 'assets_upload'
| 'show_access_methods';
export type TrackingOnboardingClickAction =
| 'select_runtime'
| 'continue'
| 'back'
| 'skip'
| 'generate'
| 'select_option'
| 'add_source'
| 'upload_source'
| 'show_access_methods';
// All optional except the discriminators (area/element/action/step/
// session id). `role`/`organization_size`/`use_case`/`discovery_source`
// ride along on About-you clicks AND on the `about_you_submit` snapshot
// click. `source_type`/`has_brand_description`/`source_count` only on
// Design-system source clicks. `runtime_type`/`is_recommended` only on
// Connect clicks. Doc explicitly forbids brand text, GitHub URL, file
// name, or path values — all enum + bool + count, no free-text.
export interface OnboardingClickProps {
page_name: 'onboarding';
area: TrackingOnboardingArea;
element: TrackingOnboardingClickElement;
action: TrackingOnboardingClickAction;
step_index: TrackingOnboardingStepIndex;
step_name: TrackingOnboardingStepName;
onboarding_session_id: string;
runtime_type?: TrackingOnboardingRuntimeType;
is_recommended?: boolean;
role?: TrackingOnboardingRole;
organization_size?: TrackingOnboardingOrganizationSize;
use_case?: TrackingOnboardingUseCase;
// Multi-pick variant of `use_case` for the survey-snapshot
// `about_you_submit` click. Individual `use_case` picks still go
// through the scalar field one row per option. The list captures
// the final selection at Finish-setup time without firing N rows.
use_cases?: TrackingOnboardingUseCase[];
discovery_source?: TrackingOnboardingDiscoverySource;
source_type?: TrackingOnboardingSourceType;
has_brand_description?: boolean;
source_count?: number;
}
// ---- Onboarding lifecycle result events ---------------------------------
export interface OnboardingRuntimeScanResultProps {
page_name: 'onboarding';
area: 'runtime';
runtime_type: 'local_cli';
result: TrackingOnboardingScanResult;
detected_cli_count: number;
available_cli_count: number;
selected_cli_id?: TrackingCliProviderId;
error_code?: string;
duration_ms: number;
onboarding_session_id: string;
}
export interface OnboardingCompleteResultProps {
page_name: 'onboarding';
area: 'onboarding';
result: TrackingOnboardingCompletionResult;
exit_step_name: TrackingOnboardingStepName;
completion_type: TrackingOnboardingCompletionType;
runtime_type: TrackingOnboardingRuntimeType;
has_about_you: boolean;
has_design_system_request: boolean;
source_count: number;
error_code?: string;
duration_ms: number;
onboarding_session_id: string;
// Survey-snapshot fields. Mirror the values from
// `about_you_submit` ui_click so dashboards can read the user's
// picks even if the individual dropdown clicks were dropped on
// navigate. `'unknown'` is the wire value for a field the user
// never touched on the About-you step; missing field means the user
// skipped the entire step.
role?: TrackingOnboardingRole;
organization_size?: TrackingOnboardingOrganizationSize;
use_cases?: TrackingOnboardingUseCase[];
discovery_source?: TrackingOnboardingDiscoverySource;
}
// --- Design systems page_view (multi-surface) ---
//
// Single shape covering the dedicated DS list / create / preview pages plus
// the DS picker / generation-dialog exposures rendered inside home and
// studio. `page_name` discriminates the host page; `area` + `view_type`
// discriminate the specific surface; the rest carry the DS context needed
// to stitch funnels (DS list → picker → project → run).
export type TrackingDesignSystemsArea =
| 'design_system_list'
| 'design_system_create'
| 'design_system_generation'
| 'design_system_preview'
| 'design_system_picker'
| 'composer';
export type TrackingDesignSystemsViewType =
| 'page'
| 'panel'
| 'dialog'
| 'popover'
| 'module';
export type TrackingDesignSystemsEntryFrom =
| 'onboarding'
| 'design_systems_page'
| 'home_card'
| 'composer_picker'
| 'project_settings'
| 'unknown';
// Origin of the design system itself. NOT the same field as
// `TrackingDesignSystemSource` on run_created/run_finished, which records
// *how the run picked* its DS. v2 doc reuses the field name
// `design_system_source` for both contexts; the value sets are disjoint.
export type TrackingDesignSystemOrigin =
| 'onboarding'
| 'manual_create'
| 'github_repo'
| 'local_code'
| 'fig'
| 'assets'
| 'official_preset'
| 'enterprise'
| 'template'
| 'mixed'
| 'unknown';
export type TrackingDesignSystemStatus =
| 'draft'
| 'generating'
| 'ready'
| 'published'
| 'default'
| 'failed'
| 'archived'
| 'unknown';
export interface DesignSystemsPageViewProps {
page_name: 'design_systems' | 'design_system_project' | 'home' | 'studio';
area: TrackingDesignSystemsArea;
view_type: TrackingDesignSystemsViewType;
entry_from: TrackingDesignSystemsEntryFrom;
design_system_id?: string;
// Re-uses the field name from the v2 doc; values are the
// `TrackingDesignSystemOrigin` set, NOT the run-time
// `TrackingDesignSystemSource` set.
design_system_source?: TrackingDesignSystemOrigin;
design_system_status?: TrackingDesignSystemStatus;
project_id?: string;
available_design_system_count?: number;
}
// ---- Design-system lifecycle enums ---------------------------------------
//
// Shared between source_ingest / create / review / status / apply result
// events. Buckets are deliberately string literals so the dashboard can
// group on them without bucket-vs-raw drift.
export type TrackingDesignSystemLengthBucket =
| '0'
| '1_50'
| '51_200'
| '201_500'
| '500_plus';
export type TrackingDesignSystemFolderCountBucket =
| '0'
| '1_10'
| '11_50'
| '51_200'
| '200_plus'
| 'unknown';
export type TrackingDesignSystemTotalSizeBucket =
| '0_1mb'
| '1_10mb'
| '10_50mb'
| '50mb_plus'
| 'unknown';
// `partial_success` reserved for ingests that captured some files but
// dropped others (e.g. a GitHub fetch that hit a per-file size cap on a
// subset). Daemon currently emits success/failed only; partial kept
// in the contract for the connector follow-up.
export type TrackingDesignSystemSourceIngestResult =
| 'success'
| 'partial_success'
| 'failed'
| 'cancelled'
| 'timeout';
export type TrackingDesignSystemIngestSourceType =
| 'github_repo'
| 'local_code'
| 'fig'
| 'assets'
| 'mixed';
// `manual_text` covers the brand-description textarea fallback when
// no concrete file/repo ingest happened. `unknown` is the terminal
// failure path where the ingest never reached a method branch.
export type TrackingDesignSystemIngestMethod =
| 'github_api'
| 'git_clone'
| 'local_snapshot'
| 'fig_parse'
| 'asset_upload'
| 'manual_text'
| 'unknown';
export type TrackingDesignSystemFallbackType =
| 'none'
| 'native_github_auth'
| 'local_git_clone'
| 'manual_upload'
| 'unknown';
export type TrackingDesignSystemRepoHost =
| 'github'
| 'gitlab'
| 'other'
| 'unknown';
export type TrackingDesignSystemCreateEntryFrom =
| 'onboarding'
| 'design_systems_page'
| 'home_card'
| 'project_settings'
| 'unknown';
export type TrackingDesignSystemSourceIngestEntryFrom =
| 'onboarding'
| 'design_systems_page'
| 'project_settings'
| 'unknown';
export type TrackingDesignSystemCreateResult = 'success' | 'failed' | 'cancelled';
export type TrackingDesignSystemReviewAction =
| 'looks_good'
| 'needs_work'
| 'submit_revision'
| 'regenerate';
export type TrackingDesignSystemReviewResult =
| 'submitted'
| 'generated'
| 'failed'
| 'cancelled';
export type TrackingDesignSystemModuleType =
| 'typography'
| 'colors'
| 'spacing'
| 'components'
| 'brand_assets'
| 'other';
export type TrackingDesignSystemStatusAction =
| 'publish'
| 'unpublish'
| 'set_default'
| 'unset_default'
| 'archive'
| 'delete';
export type TrackingDesignSystemStatusResult = 'success' | 'failed' | 'cancelled';
// Like `TrackingDesignSystemStatus` but adds `deleted` for the
// status_after side of a delete action.
export type TrackingDesignSystemStatusValue =
| 'draft'
| 'ready'
| 'published'
| 'default'
| 'failed'
| 'archived'
| 'deleted'
| 'unknown';
export type TrackingDesignSystemApplyAction =
| 'select_design_system'
| 'auto_select'
| 'clear_selection'
| 'apply_to_run';
export type TrackingDesignSystemApplyResult = 'success' | 'failed' | 'cancelled';
export type TrackingDesignSystemSelectionMode =
| 'auto'
| 'manual'
| 'default'
| 'none';
// Project kind values for the picker's target project. Wider than
// `TrackingProjectKind`'s prototype-side enum because the picker
// shows up in slide / image / video / audio / live-artifact composers
// too. `unknown` covers picker views with no project locked in.
export type TrackingDesignSystemApplyTargetKind =
| 'prototype'
| 'slide_deck'
| 'image'
| 'video'
| 'audio'
| 'live_artifact'
| 'unknown';
// Entry from for the run_created / run_finished DS variant. Distinct
// from the chat-panel entry_from enum because DS runs don't originate
// from new_project / chat_composer at all.
export type TrackingDesignSystemRunEntryFrom =
| 'design_system_create'
| 'onboarding_design_system'
| 'regenerate_from_review'
| 'unknown';
// ---- Design-system lifecycle result props --------------------------------
export interface DesignSystemSourceIngestResultProps {
page_name: 'design_systems' | 'design_system_project';
area: 'design_system_create';
entry_from: TrackingDesignSystemSourceIngestEntryFrom;
source_type: TrackingDesignSystemIngestSourceType;
ingest_method: TrackingDesignSystemIngestMethod;
result: TrackingDesignSystemSourceIngestResult;
has_fallback: boolean;
fallback_type: TrackingDesignSystemFallbackType;
repo_host: TrackingDesignSystemRepoHost;
file_count: number;
folder_file_count_bucket: TrackingDesignSystemFolderCountBucket;
total_size_bucket: TrackingDesignSystemTotalSizeBucket;
error_code?: string;
duration_ms: number;
project_id?: string;
design_system_id?: string;
}
export interface DesignSystemCreateResultProps {
page_name: 'design_systems';
area: 'design_system_create';
entry_from: TrackingDesignSystemCreateEntryFrom;
result: TrackingDesignSystemCreateResult;
project_id?: string;
design_system_id?: string;
design_system_source: TrackingDesignSystemOrigin;
source_count: number;
created_as_project: boolean;
has_brand_description: boolean;
brand_description_length_bucket: TrackingDesignSystemLengthBucket;
notes_length_bucket: TrackingDesignSystemLengthBucket;
error_code?: string;
duration_ms: number;
}
export interface DesignSystemReviewResultProps {
page_name: 'design_system_project';
area: 'design_system_preview';
review_action: TrackingDesignSystemReviewAction;
result: TrackingDesignSystemReviewResult;
design_system_id: string;
project_id: string;
// Stable identifier for the reviewed module. Derived from the
// markdown section header slug (e.g. `typography`, `brand-assets`)
// since DS sections don't have DB ids today. `module_index` makes
// it unique when a DS has multiple sections sharing a header type.
module_id: string;
module_type: TrackingDesignSystemModuleType;
module_index: number;
feedback_length_bucket: TrackingDesignSystemLengthBucket;
has_custom_feedback: boolean;
run_id?: string;
revision_run_id?: string;
error_code?: string;
duration_ms: number;
}
export interface DesignSystemStatusResultProps {
page_name: 'design_systems' | 'design_system_project';
area: 'design_system_status';
action: TrackingDesignSystemStatusAction;
result: TrackingDesignSystemStatusResult;
design_system_id: string;
project_id?: string;
status_before: TrackingDesignSystemStatusValue;
status_after: TrackingDesignSystemStatusValue;
is_default_before: boolean;
is_default_after: boolean;
error_code?: string;
duration_ms: number;
}
export interface DesignSystemApplyResultProps {
page_name: 'home' | 'studio';
area: 'design_system_picker' | 'composer';
action: TrackingDesignSystemApplyAction;
result: TrackingDesignSystemApplyResult;
target_project_kind: TrackingDesignSystemApplyTargetKind;
design_system_id?: string;
design_system_source?: TrackingDesignSystemOrigin;
design_system_status?: TrackingDesignSystemStatusValue;
design_system_applied: boolean;
design_system_selection_mode: TrackingDesignSystemSelectionMode;
is_default: boolean;
is_auto_selected: boolean;
available_design_system_count: number;
run_id?: string;
error_code?: string;
duration_ms: number;
}
// --- Generic page_view (existing surfaces) ---
//
// Covers all page-level page_views that don't carry surface-specific
// fields. `chat_panel` is the only one that uses the optional `source`.
export interface GenericPageViewProps {
page_name: Exclude<
TrackingPageName,
'onboarding' | 'design_system_project' | 'studio'
>;
source?: TrackingChatPanelPageViewSource;
}
// Discriminated union by `page_name`. `home` and `design_systems` belong
// to BOTH `GenericPageViewProps` (page-level visit) and
// `DesignSystemsPageViewProps` (DS module / picker exposure on those
// pages); call sites that pass `area` get narrowed to the DS shape.
export type PageViewProps =
| GenericPageViewProps
| OnboardingPageViewProps
| DesignSystemsPageViewProps;
// ---- ui_click ------------------------------------------------------------
//
// Each surface lives in its own `*ClickProps` interface so call sites stay
// strongly typed. The union below collects them for the central `track()`
// signature; helpers in apps/web/src/analytics/events.ts pick a specific
// interface per surface.
// HOME -- left nav / toolbar / center composer / recent projects / templates
export interface HomeNavClickProps {
page_name: 'home';
area: 'nav';
element:
| 'home'
| 'projects'
| 'automations'
| 'plugins'
| 'design_systems'
| 'integrations'
| 'new_project_plus'
| 'help';
}
export interface HelpPopoverClickProps {
page_name: 'home';
area: 'help_resources_popover';
element:
| 'get_help_on_github'
| 'submit_a_feature_request'
| 'whats_new'
| 'download_desktop_app';
surface: 'popover';
}
export interface HomeToolbarClickProps {
page_name: 'home';
area: 'toolbar';
element: 'star' | 'execution_settings' | 'use_everywhere' | 'settings';
}
export interface ExecutionSettingsPopoverClickProps {
page_name: 'home';
area: 'execution_settings_popover';
element:
| 'mode_local_cli'
| 'mode_byok'
| 'agent_card'
| 'model_dropdown'
| 'open_execution_settings';
}
export interface SettingsPopoverClickProps {
page_name: 'home';
area: 'settings_popover';
element:
| 'follow_x'
| 'join_discord'
| 'language'
| 'appearance'
| 'use_everywhere'
| 'settings';
}
export interface HomeChatComposerClickProps {
page_name: 'home';
area: 'chat_composer';
element:
| 'chat_input'
| 'send_button'
| 'plugin_chip'
| 'action_chip'
// Paperclip icon opening the file picker. Mirrors the chat_panel
// composer's `element: 'attachment'` so the same dashboard counts
// "user opened the file picker" across both surfaces.
| 'attachment';
// For plugin / action chips, the specific id (e.g. `prototype`, `from_figma`).
chip_id?: string;
}
export interface UpdateIndicatorClickProps {
page_name: 'home';
area: 'update_indicator' | 'update_prompt';
element: 'ready_indicator' | 'later' | 'install_update';
action: 'open_prompt' | 'dismiss' | 'install';
app_version_before?: string;
app_version_after?: string;
}
export interface NewProjectModalTabClickProps {
page_name: 'home';
area: 'new_project_modal';
element: 'tab';
tab_name: TrackingNewProjectTab;
}
export interface NewProjectModalElementClickProps {
page_name: 'home';
area: 'new_project_modal';
element:
| 'project_name'
| 'design_system'
| 'target_platforms'
| 'include_landing_page'
| 'include_os_widgets'
| 'wireframe'
| 'high_fidelity'
| 'create'
| 'import_claude_design_zip'
| 'open_folder'
| 'path_input';
tab_name: TrackingNewProjectTab;
}
export interface PluginReplacementModalClickProps {
page_name: 'home';
area: 'plugin_replacement_modal';
element: 'cancel' | 'replace';
}
export interface PrivacyModalClickProps {
page_name: 'home';
area: 'privacy_modal';
element: 'yes' | 'no';
}
export interface RecentProjectsClickProps {
page_name: 'home';
area: 'recent_projects';
element: 'project_card' | 'view_all';
project_id?: string;
project_kind?: TrackingProjectKind;
project_status?: string;
}
export interface HomeTemplatesClickProps {
page_name: 'home';
area: 'templates';
element:
| 'featured'
| 'all'
| 'clear_filters'
| 'browse_registry'
| 'search_input'
| 'filter_chip'
| 'templates_feature'
| 'templates_details'
| 'templates_use'
| 'templates_use_dropdown'
| 'create_templates';
template_id?: string;
template_type?: string;
filter_name?: string;
}
export interface HomeTemplatesDropdownClickProps {
page_name: 'home';
area: 'templates_dropdown';
element: 'use' | 'use_with_query';
template_id?: string;
template_type?: string;
}
// PROJECTS page
export interface ProjectsListControlsClickProps {
page_name: 'projects';
area: 'list_controls';
element:
| 'recent'
| 'your_designs'
| 'search_input'
| 'select'
| 'grid_view'
| 'list_view';
}
export interface ProjectsListClickProps {
page_name: 'projects';
area: 'list';
element: 'project_card' | 'more';
project_id?: string;
project_kind?: TrackingProjectKind;
project_source?: TrackingProjectSource;
}
export interface ProjectsMorePopoverClickProps {
page_name: 'projects';
area: 'projects_more_popover';
element: 'rename' | 'delete';
project_id?: string;
project_kind?: TrackingProjectKind;
}
// AUTOMATIONS
export interface AutomationsClickProps {
page_name: 'automations';
area: 'automations';
element:
| 'new_automation'
| 'new'
| 'view_progress'
| 'run_now'
| 'open_artifact'
| 'type_card'
| 'filter_tab';
type_id?: 'orbit' | 'routines' | 'schedules' | 'live_artifacts';
filter_id?: 'all' | 'scheduled' | 'running' | 'done';
}
// PLUGINS
export interface PluginsTopClickProps {
page_name: 'plugins';
area: 'plugins';
element:
| 'create_plugin'
| 'import_plugin'
| 'agent_context'
| 'installed_tab'
| 'available_tab'
| 'sources_tab'
| 'team_tab';
}
export interface PluginsInstalledTabClickProps {
page_name: 'plugins';
area: 'installed_tab';
element:
| 'clear_filters'
| 'search_input'
| 'filter_chip'
| 'templates_details'
| 'templates_use'
| 'templates_use_dropdown'
| 'templates_publish'
| 'templates_contribute'
| 'create_plugin';
filter_key?: string;
filter_name?: string;
template_id?: string;
template_type?: string;
}
export interface PluginsTemplatesDropdownClickProps {
page_name: 'plugins';
area: 'templates_dropdown';
element: 'use' | 'use_with_query';
template_id?: string;
template_type?: string;
}
export interface PluginsAvailableTabClickProps {
page_name: 'plugins';
area: 'available_tab';
element: 'search_input' | 'details' | 'install' | 'source_dropdown';
plugin_id?: string;
plugin_type?: string;
}
export interface PluginsSourcesTabClickProps {
page_name: 'plugins';
area: 'sources_tab';
element: 'source_url_input' | 'add_source' | 'refresh' | 'remove';
plugin_id?: string;
plugin_type?: string;
}
// DESIGN SYSTEMS
export interface DesignSystemsTopClickProps {
page_name: 'design_systems';
area: 'design_systems';
element: 'search_input' | 'search_dropdown' | 'filter_chip';
filter_name?: string;
}
export interface DesignSystemsTemplateCardClickProps {
page_name: 'design_systems';
area: 'templates_card';
element: 'templates_card';
templates_id?: string;
templates_type?: string;
}
export interface DesignSystemsTemplatesModalClickProps {
page_name: 'design_systems';
area: 'templates_modal';
element:
| 'showcase'
| 'tokens'
| 'design_md'
| 'open_design_set'
| 'fullscreen'
| 'share';
templates_id?: string;
templates_type?: string;
}
export interface DesignSystemsTemplatesModalSharePopoverClickProps {
page_name: 'design_systems';
area: 'templates_modal_share_popover';
// Share popover element list is pending product confirmation; kept open so
// the helper can ship now and the enum tightens later.
element: string;
templates_id?: string;
templates_type?: string;
}
// INTEGRATIONS
export interface IntegrationsTabClickProps {
page_name: 'integrations';
area: 'integrations_tab';
element: 'mcp' | 'connectors' | 'skills' | 'use_everywhere';
}
export interface IntegrationsMcpTabClickProps {
page_name: 'integrations';
area: 'mcp_tab';
element: 'add_server' | 'saved';
}
export interface IntegrationsConnectorsTabClickProps {
page_name: 'integrations';
area: 'connectors_tab';
element:
| 'api_key_input'
| 'save_key'
| 'clear'
| 'get_api_key'
| 'provider_chip'
| 'search_connectors';
}
export interface IntegrationsSkillsTabClickProps {
page_name: 'integrations';
area: 'skills_tab';
element: 'coming_soon';
}
export interface IntegrationsUseEverywhereTabClickProps {
page_name: 'integrations';
area: 'use_everywhere_tab';
element:
| 'overview'
| 'cli_od'
| 'mcp_server'
| 'http_api'
| 'skills_headless'
| 'configure_mcp_server'
| 'copy_guide_for_agent'
| 'copy';
}
// CHAT PANEL (studio)
export interface ChatPanelClickProps {
page_name: 'chat_panel';
area: 'chat_panel';
element:
| 'history'
| 'new_chat'
| 'back'
| 'template_card'
| 'chat_input'
| 'composer_settings'
| 'attachment'
| 'send'
| 'resources_popover_trigger';
}
export interface ChatPanelResourcesPopoverClickProps {
page_name: 'chat_panel';
area: 'resources_popover';
element:
| 'plugins_tab'
| 'skills_tab'
| 'mcp_tab'
| 'users_tab'
| 'files_tab'
| 'official'
| 'my_plugins'
| 'search_input'
| 'template_card'
| 'customize_in_settings';
}
// FILE MANAGER
export interface FileManagerClickProps {
page_name: 'file_manager';
area: 'file_manager';
element:
| 'new_sketch'
| 'paste'
| 'upload'
| 'select_all_on_page'
| 'select_everything'
| 'download_as_zip'
| 'delete'
| 'previous'
| 'next'
| 'per_page_dropdown';
}
// ARTIFACT
export interface ArtifactToolbarClickProps {
page_name: 'artifact';
area: 'artifact_toolbar';
element:
| 'reload'
| 'preview'
| 'source'
| 'tweaks'
| 'draw'
| 'comment'
| 'pods'
| 'inspect'
| 'edit'
| 'zoom_out'
| 'zoom_level_dropdown'
| 'zoom_in';
artifact_id?: string;
artifact_kind?: TrackingArtifactKind;
}
export interface TweaksPopoverClickProps {
page_name: 'artifact';
area: 'tweaks_popover';
element: 'variant_option';
variant_name?: string;
artifact_id?: string;
artifact_kind?: TrackingArtifactKind;
status_before: 'on' | 'off';
status_after: 'on' | 'off';
}
export interface CommentPopoverClickProps {
page_name: 'artifact';
area: 'comment_popover';
element: 'save_comment' | 'send_to_chat' | 'add_note';
artifact_id?: string;
artifact_kind?: TrackingArtifactKind;
}
export interface ArtifactHeaderClickProps {
page_name: 'artifact';
area: 'artifact_header';
element:
| 'back'
| 'edit'
| 'present_dropdown'
| 'share_dropdown'
| 'settings';
artifact_id?: string;
artifact_kind?: TrackingArtifactKind;
}
export interface PresentPopoverClickProps {
page_name: 'artifact';
area: 'present_popover';
element: 'in_this_tab' | 'fullscreen' | 'new_tab';
artifact_id?: string;
artifact_kind?: TrackingArtifactKind;
}
export interface ShareOptionPopoverClickProps {
page_name: 'artifact';
area: 'share_option_popover';
element: TrackingExportFormat;
artifact_id: string;
artifact_kind: TrackingArtifactKind;
project_id: string;
project_kind: TrackingProjectKind | null;
}
// FEEDBACK clicks (CSV rows 56 / 58)
export interface AssistantFeedbackButtonClickProps {
page_name: 'chat_panel';
area: 'chat_panel';
element: 'assistant_feedback_button';
action: 'submit_feedback_rating' | 'clear_feedback_rating';
project_id: string;
project_kind: TrackingProjectKind | null;
conversation_id: string | null;
assistant_message_id: string;
run_id: string;
// For `clear_feedback_rating`, `rating` carries the rating that was
// cleared (not the previous-before-clear value, which lives in
// `rating_before`). Mason flagged the v1 emission supplied the wrong
// value here; v2 corrects that.
rating: 'positive' | 'negative';
rating_before: 'positive' | 'negative' | 'none';
has_produced_files: boolean;
}
export interface AssistantFeedbackReasonSubmitClickProps {
page_name: 'chat_panel';
area: 'chat_panel';
element: 'assistant_feedback_reason_submit_button';
action: 'click_submit_feedback_reason';
project_id: string;
project_kind: TrackingProjectKind | null;
conversation_id: string | null;
assistant_message_id: string;
run_id: string;
rating: 'positive' | 'negative';
reason?: string;
reason_count: number;
has_custom_reason: boolean;
custom_reason?: string;
}
// SETTINGS clicks
export type TrackingSettingsArea =
| 'configure_execution_mode'
| 'configure_execution_mode_local_cli'
| 'configure_execution_mode_byok'
| 'instructions'
| 'memory'
| 'media_providers'
| 'skills'
| 'external_mcp'
| 'connectors'
| 'orbit'
| 'mcp_server'
| 'language'
| 'appearance'
| 'notifications'
| 'pets'
| 'design_systems'
| 'privacy'
| 'about';
export interface SettingsSidebarClickProps {
page_name: TrackingSettingsPage;
area: 'settings_sidebar';
element: TrackingSettingsArea;
}
export interface SettingsExecutionModeTabClickProps {
page_name: TrackingSettingsPage;
area: 'configure_execution_mode';
element: 'execution_mode_tab';
action: 'switch_execution_mode';
mode_before: TrackingExecutionMode;
mode_after: TrackingExecutionMode;
}
export interface SettingsLocalCliClickProps {
page_name: TrackingSettingsPage;
area: 'configure_execution_mode_local_cli';
element: 'test' | 'rescan' | 'cli_provider' | 'install' | 'docs';
cli_provider_id?: TrackingCliProviderId;
install_status?: 'installed' | 'not_installed' | 'unknown';
}
export interface SettingsByokProviderOptionClickProps {
page_name: TrackingSettingsPage;
area: 'configure_execution_mode_byok';
element: 'byok_provider_option';
action: 'select_byok_provider';
provider_id: TrackingByokProviderId;
is_selected: boolean;
}
export interface SettingsByokFieldClickProps {
page_name: TrackingSettingsPage;
area: 'configure_execution_mode_byok';
element:
| 'fetch_models'
| 'test'
| 'quick_fill_provider'
| 'api_key'
| 'model'
| 'memory_model'
| 'base_url';
provider_id: TrackingByokProviderId;
// Only set for `api_key` / `base_url` / `model` focus events.
has_value?: boolean;
}
export interface SettingsMediaProvidersClickProps {
page_name: TrackingSettingsPage;
area: 'media_providers';
element: 'reload' | 'key_input' | 'url_input' | 'clear';
providers_id?: string;
is_configured?: boolean;
}
export interface SettingsConnectorsClickProps {
page_name: TrackingSettingsPage;
area: 'connectors';
element:
| 'api_key_input'
| 'save_key'
| 'clear'
| 'get_api_key'
| 'provider_chip'
| 'search_connectors';
connector_id?: string;
}
export interface SettingsLanguageClickProps {
page_name: TrackingSettingsPage;
area: 'language';
// Locale id, e.g. `english`, `bahasa_indonesia`, `zh_cn`.
element: string;
}
export interface SettingsAppearanceClickProps {
page_name: TrackingSettingsPage;
area: 'appearance';
element: 'system' | 'light' | 'dark' | 'accent_color';
color?: string;
}
export interface SettingsNotificationsClickProps {
page_name: TrackingSettingsPage;
area: 'notifications';
element:
| 'completion_sound'
| 'desktop_notification'
| 'send_test'
| 'success_sound'
| 'failure_sound';
// For sound selection events, the chosen tone id.
sound_id?: 'ding' | 'chime' | 'two_tone_up' | 'pluck' | 'buzz' | 'two_tone_down' | 'thud';
completion_sound_status?: 'on' | 'off';
desktop_notification_status?: 'on' | 'off';
}
export interface SettingsPetsClickProps {
page_name: TrackingSettingsPage;
area: 'pets';
element:
| 'tuck_away'
| 'built_in'
| 'custom'
| 'community'
| 'custom_card'
| 'adopt';
pet_id?: string;
}
export interface SettingsPrivacyClickProps {
page_name: TrackingSettingsPage;
area: 'privacy';
element:
| 'anonymous_metrics'
| 'conversation_and_tool_content'
| 'project_artifacts_manifest'
| 'delete_my_data';
anonymous_metrics_status?: 'on' | 'off';
conversation_and_tool_content_status?: 'on' | 'off';
project_artifacts_manifest_status?: 'on' | 'off';
}
// Discriminated union of every supported ui_click payload.
export type UiClickProps =
| HomeNavClickProps
| HelpPopoverClickProps
| HomeToolbarClickProps
| ExecutionSettingsPopoverClickProps
| SettingsPopoverClickProps
| HomeChatComposerClickProps
| UpdateIndicatorClickProps
| NewProjectModalTabClickProps
| NewProjectModalElementClickProps
| PluginReplacementModalClickProps
| PrivacyModalClickProps
| RecentProjectsClickProps
| HomeTemplatesClickProps
| HomeTemplatesDropdownClickProps
| ProjectsListControlsClickProps
| ProjectsListClickProps
| ProjectsMorePopoverClickProps
| AutomationsClickProps
| PluginsTopClickProps
| PluginsInstalledTabClickProps
| PluginsTemplatesDropdownClickProps
| PluginsAvailableTabClickProps
| PluginsSourcesTabClickProps
| DesignSystemsTopClickProps
| DesignSystemsTemplateCardClickProps
| DesignSystemsTemplatesModalClickProps
| DesignSystemsTemplatesModalSharePopoverClickProps
| IntegrationsTabClickProps
| IntegrationsMcpTabClickProps
| IntegrationsConnectorsTabClickProps
| IntegrationsSkillsTabClickProps
| IntegrationsUseEverywhereTabClickProps
| ChatPanelClickProps
| ChatPanelResourcesPopoverClickProps
| FileManagerClickProps
| ArtifactToolbarClickProps
| TweaksPopoverClickProps
| CommentPopoverClickProps
| ArtifactHeaderClickProps
| PresentPopoverClickProps
| ShareOptionPopoverClickProps
| AssistantFeedbackButtonClickProps
| AssistantFeedbackReasonSubmitClickProps
| SettingsSidebarClickProps
| SettingsExecutionModeTabClickProps
| SettingsLocalCliClickProps
| SettingsByokProviderOptionClickProps
| SettingsByokFieldClickProps
| SettingsMediaProvidersClickProps
| SettingsConnectorsClickProps
| SettingsLanguageClickProps
| SettingsAppearanceClickProps
| SettingsNotificationsClickProps
| SettingsPetsClickProps
| SettingsPrivacyClickProps
| OnboardingClickProps;
// ---- surface_view --------------------------------------------------------
export interface HelpPopoverSurfaceViewProps {
page_name: 'home';
area: 'help_resources_popover';
}
export interface NewProjectModalSurfaceViewProps {
page_name: 'home';
area: 'new_project_modal';
tab_name: TrackingNewProjectTab;
}
export interface PluginReplacementModalSurfaceViewProps {
page_name: 'home';
area: 'plugin_replacement_modal';
}
export interface DesignSystemsTemplatesModalSurfaceViewProps {
page_name: 'design_systems';
area: 'templates_modal';
templates_id?: string;
templates_type?: string;
}
export interface AssistantFeedbackReasonPanelSurfaceViewProps {
page_name: 'chat_panel';
area: 'chat_panel';
element: 'assistant_feedback_reason_panel';
view_type: 'panel';
project_id: string;
project_kind: TrackingProjectKind | null;
conversation_id: string | null;
assistant_message_id: string;
run_id: string;
rating: 'positive' | 'negative';
}
// Packaged updater UI surfaces. The download pipeline is intentionally
// silent; these fire only when a verified update is installable and when the
// user opens the final confirmation prompt.
export interface UpdateIndicatorSurfaceViewProps {
page_name: 'home';
area: 'update_indicator';
app_version_before?: string;
app_version_after?: string;
}
export interface UpdatePromptSurfaceViewProps {
page_name: 'home';
area: 'update_prompt';
app_version_before?: string;
app_version_after?: string;
}
export type SurfaceViewProps =
| HelpPopoverSurfaceViewProps
| NewProjectModalSurfaceViewProps
| PluginReplacementModalSurfaceViewProps
| DesignSystemsTemplatesModalSurfaceViewProps
| AssistantFeedbackReasonPanelSurfaceViewProps
| UpdateIndicatorSurfaceViewProps
| UpdatePromptSurfaceViewProps;
// ---- Result events -------------------------------------------------------
export interface ProjectCreateResultProps {
page_name: 'home';
area: 'new_project';
project_source: TrackingProjectSource;
project_id: string | null;
project_kind: TrackingProjectKind | null;
design_system?: string;
target_platforms?: string;
companion_surfaces?: string;
fidelity: TrackingFidelity;
connectors?: string;
use_speaker_notes?: boolean;
include_animations?: boolean;
reference_template?: string;
model_id?: string;
aspect?: string;
result: TrackingResult;
error_code?: string;
}
export interface PluginReplacementResultProps {
page_name: 'home';
area: 'plugin_replacement';
plugin_before: string;
plugin_after: string;
result: TrackingResult;
error_code?: string;
}
export interface UpdateInstallResultProps {
page_name: 'home';
area: 'update_prompt';
result: TrackingResult;
app_version_before?: string;
app_version_after?: string;
error_code?: string;
}
// run_created/finished merges CSV rows 17/18 (extended fields) and 44/45
// (current daemon-side authoritative emission). Daemon supplies token /
// duration data; entry surfaces propagate the optional context (entry_from,
// fidelity, etc.) via the create-run payload.
export interface RunCreatedProps {
// `chat_panel` is the regular artifact-run surface; `design_system_project`
// is the DS-as-project variant (DS creation + regeneration runs).
page_name: 'chat_panel' | 'design_system_project';
area: 'chat_composer' | 'design_system_generation';
// Where the run was initiated from. The DS variant uses the
// `TrackingDesignSystemRunEntryFrom` set; both unions stay
// distinct so the dashboard can split funnels cleanly.
entry_from?:
| 'new_project'
| 'chat_composer'
| TrackingDesignSystemRunEntryFrom;
project_source?: TrackingProjectSource;
project_id: string;
conversation_id: string | null;
run_id: string;
project_kind: TrackingProjectKind | null;
design_system_id?: string;
design_system_source: TrackingDesignSystemSource;
design_system_version?: string;
// DS-variant context. `ds_source_origin` mirrors the
// `TrackingDesignSystemOrigin` set used on DS page_views (where
// the DS came from), separate from the runtime-selection
// `design_system_source` field above. Optional on the chat_panel
// shape; required-shaped data on the DS shape (callers populate
// them when emitting the DS variant).
ds_source_origin?: TrackingDesignSystemOrigin;
source_count?: number;
has_brand_description?: boolean;
brand_description_length_bucket?: TrackingDesignSystemLengthBucket;
github_repo_count?: number;
local_folder_count?: number;
fig_file_count?: number;
asset_file_count?: number;
// Optional context inherited from the originating surface.
target_platforms?: string;
companion_surfaces?: string;
fidelity?: TrackingFidelity;
connectors?: string;
use_speaker_notes?: boolean;
include_animations?: boolean;
reference_template?: string;
aspect?: string;
has_attachment: boolean;
user_query_tokens: number;
model_id: string | null;
agent_provider_id: string | null;
skill_id: string | null;
mcp_id: string | null;
token_count_source: TrackingTokenCountSource;
}
export interface RunFinishedProps extends Omit<RunCreatedProps, 'area'> {
area: 'chat_panel' | 'design_system_generation';
result: TrackingRunResult;
error_code?: string;
artifact_count: number;
input_tokens?: number;
output_tokens?: number;
total_tokens?: number;
time_to_first_token_ms?: number;
generation_duration_ms?: number;
total_duration_ms: number;
// DS-variant outcome fields. `design_system_created` is true when
// the run produced a stored DESIGN.md; `preview_module_count` and
// `missing_font_count` give the dashboard a coarse quality read
// without inspecting the artifact contents.
design_system_created?: boolean;
preview_module_count?: number;
missing_font_count?: number;
}
export type TrackingUpdateApplyResult = 'success' | 'not_applied' | 'unknown';
export type TrackingUpdateApplyReason =
| 'app_version_matches'
| 'app_version_unchanged'
| 'expired'
| 'identity_mismatch';
export type TrackingUpdateApplyElapsedBucket =
| 'lt_5m'
| '5m_1h'
| '1h_6h'
| '6h_24h'
| '1d_7d'
| 'gt_7d'
| 'unknown';
export interface UpdateApplyObservedProps {
flow_id: string;
channel: 'stable' | 'beta' | 'nightly' | 'preview';
namespace: string;
platform: string;
arch: string;
artifact_type: 'dmg' | 'installer';
from_version: string;
to_version: string;
result: TrackingUpdateApplyResult;
reason: TrackingUpdateApplyReason;
elapsed_bucket: TrackingUpdateApplyElapsedBucket;
}
// Discriminated union over the four surfaces that fire
// `file_upload_result`. The `file_manager` shape is the original (Files
// panel Upload button). `home` / `chat_panel` were added in PR #2459 so
// the home + chat composer paperclip uploads stop being silent. The
// `onboarding` shape covers the Design-system step's source ingest:
// `source_type` is required so the dashboard can split the funnel by
// source kind without inspecting `file_type`.
export type TrackingFileUploadSurface =
| { page_name: 'file_manager'; area: 'file_manager'; project_id: string }
| { page_name: 'chat_panel'; area: 'chat_composer'; project_id: string }
| { page_name: 'home'; area: 'chat_composer'; project_id: string }
| {
page_name: 'onboarding';
area: 'design_system_source';
source_type: 'local_code' | 'fig' | 'assets';
onboarding_session_id: string;
// Onboarding uploads happen BEFORE a project exists, so
// `project_id` is optional and present only when the upload was
// re-issued after a project landed (rare in the onboarding flow).
project_id?: string;
}
| {
// DS create page upload (Design systems → New design system →
// source dropzones). Distinct from the onboarding shape because
// the funnel splits by entry surface; both share `source_type`
// so the dashboard can union on it when needed.
page_name: 'design_systems';
area: 'design_system_source';
source_type: 'local_code' | 'fig' | 'assets';
design_system_id?: string;
project_id?: string;
};
export type FileUploadResultProps = TrackingFileUploadSurface & {
file_count: number;
file_type: TrackingFileType;
file_size_bucket: TrackingFileSizeBucket;
result: TrackingRunResult;
error_code?: string;
duration_ms?: number;
};
export interface ArtifactExportResultProps {
page_name: 'artifact';
area: 'share_option_popover';
artifact_id: string;
artifact_kind: TrackingArtifactKind;
export_format: TrackingExportFormat;
result: TrackingExportResult;
error_code?: string;
export_duration_ms: number;
project_id: string;
project_kind: TrackingProjectKind | null;
}
export interface FeedbackSubmitResultProps {
page_name: 'chat_panel';
area: 'chat_panel';
element: 'assistant_feedback_reason_submit';
action: 'submit_feedback_reason';
project_id: string;
project_kind: TrackingProjectKind | null;
conversation_id: string | null;
assistant_message_id: string;
run_id: string;
rating: 'positive' | 'negative';
reason?: string;
reason_count: number;
has_custom_reason: boolean;
custom_reason?: string;
result: TrackingResult;
}
interface AssistantFeedbackBase {
page: 'studio';
area: 'chat_panel';
project_id: string;
project_kind: TrackingProjectKind;
conversation_id: string;
assistant_message_id: string;
// run_id may be absent for messages whose run record is missing or pruned,
// but the product funnel keys off this; we emit `null` rather than dropping
// the field so PostHog can distinguish "no run id" from "field forgotten".
run_id: string | null;
rating: TrackingFeedbackRating;
}
// Click events override `rating` to allow `'none'` because the user can
// clear a previously-set rating; reason_* events still inherit the
// stricter `positive | negative` base since they only fire after the user
// commits to a thumb.
export interface AssistantFeedbackClickProps
extends Omit<AssistantFeedbackBase, 'rating'> {
element: 'assistant_feedback_button';
action: TrackingFeedbackAction;
/** Post-action state. `'none'` when the user just cleared their rating. */
rating: TrackingFeedbackRatingWithNone;
/** Pre-action state. Renamed from `previous_rating` for symmetry with `rating`. */
rating_before: TrackingFeedbackRatingWithNone;
has_produced_files: boolean;
}
export interface AssistantFeedbackReasonViewProps extends AssistantFeedbackBase {
element: 'assistant_feedback_reason_panel';
view_type: 'panel';
}
// Shape shared by reason_click (button click) and reason_submit (result).
// Both fire from the same submit handler with the same payload, threaded by
// request_id so PostHog can stitch click→result.
interface AssistantFeedbackReasonResultBase extends AssistantFeedbackBase {
reason: TrackingFeedbackReasonCode[];
reason_count: number;
has_custom_reason: boolean;
/** Raw free-text the user typed in the "other" input. Empty string when
* the user didn't select "other" or left the field blank. Product
* confirmed on 2026-05-13 that the raw text ships (no length bucketing). */
custom_reason: string;
}
export interface AssistantFeedbackReasonClickProps
extends AssistantFeedbackReasonResultBase {
element: 'assistant_feedback_reason_submit_button';
action: 'click_submit_feedback_reason';
}
export interface AssistantFeedbackReasonSubmitProps
extends AssistantFeedbackReasonResultBase {
element: 'assistant_feedback_reason_submit';
action: 'submit_feedback_reason';
}
// SETTINGS view + result events (page=settings)
export interface SettingsViewProps {
page_name: TrackingSettingsPage;
area: TrackingSettingsArea;
}
export interface SettingsCliTestResultProps {
page_name: TrackingSettingsPage;
area: 'configure_execution_mode';
cli_provider_id: TrackingCliProviderId;
result: TrackingTestResult;
error_code?: string;
duration_ms: number;
}
export interface SettingsByokTestResultProps {
page_name: TrackingSettingsPage;
// CSV row 67 names this area `execution_model`; keep that spelling so the
// wire format matches the doc.
area: 'execution_model';
provider_id: TrackingByokProviderId;
result: TrackingTestResult | 'not_ready';
error_code?: string;
duration_ms: number;
}
export interface SettingsConnectorAuthResultProps {
page_name: TrackingSettingsPage;
area: 'connectors';
connector_id: string;
action: 'connect' | 'disconnect' | 'refresh';
result: TrackingRunResult;
error_code?: string;
}
// ---- Discriminated union of all event payloads ---------------------------
export type AnalyticsEventPayload =
| { event: 'page_view'; props: PageViewProps }
| { event: 'ui_click'; props: UiClickProps }
| { event: 'surface_view'; props: SurfaceViewProps }
| { event: 'project_create_result'; props: ProjectCreateResultProps }
| { event: 'plugin_replacement_result'; props: PluginReplacementResultProps }
| { event: 'run_created'; props: RunCreatedProps }
| { event: 'run_finished'; props: RunFinishedProps }
| { event: 'update_install_result'; props: UpdateInstallResultProps }
| { event: 'update_apply_observed'; props: UpdateApplyObservedProps }
| { event: 'file_upload_result'; props: FileUploadResultProps }
| { event: 'artifact_export_result'; props: ArtifactExportResultProps }
| { event: 'feedback_submit_result'; props: FeedbackSubmitResultProps }
| { event: 'assistant_feedback_click'; props: AssistantFeedbackClickProps }
| {
event: 'assistant_feedback_reason_view';
props: AssistantFeedbackReasonViewProps;
}
| {
event: 'assistant_feedback_reason_click';
props: AssistantFeedbackReasonClickProps;
}
| {
event: 'assistant_feedback_reason_submit';
props: AssistantFeedbackReasonSubmitProps;
}
| { event: 'settings_view'; props: SettingsViewProps }
| { event: 'settings_cli_test_result'; props: SettingsCliTestResultProps }
| { event: 'settings_byok_test_result'; props: SettingsByokTestResultProps }
| { event: 'settings_connector_auth_result'; props: SettingsConnectorAuthResultProps }
| { event: 'onboarding_runtime_scan_result'; props: OnboardingRuntimeScanResultProps }
| { event: 'onboarding_complete_result'; props: OnboardingCompleteResultProps }
| {
event: 'design_system_source_ingest_result';
props: DesignSystemSourceIngestResultProps;
}
| { event: 'design_system_create_result'; props: DesignSystemCreateResultProps }
| { event: 'design_system_review_result'; props: DesignSystemReviewResultProps }
| { event: 'design_system_status_result'; props: DesignSystemStatusResultProps }
| { event: 'design_system_apply_result'; props: DesignSystemApplyResultProps };
// ---- Enum mapping helpers (code ↔ CSV wire format) -----------------------
// Code `ProjectKind` from packages/contracts/src/api/projects.ts:
// 'prototype' | 'deck' | 'template' | 'other' | 'image' | 'video' | 'audio'
export function projectKindToTracking(
kind: string | null | undefined,
): TrackingProjectKind | null {
switch (kind) {
case 'prototype':
return 'prototype';
case 'deck':
return 'slide_deck';
case 'template':
return 'template';
case 'other':
return 'other';
case 'image':
return 'image';
case 'video':
return 'video';
case 'audio':
return 'audio';
case 'live-artifact':
case 'live_artifact':
return 'live_artifact';
default:
return null;
}
}
// Code `CreateTab` from apps/web/src/components/NewProjectPanel.tsx:
// 'prototype' | 'live-artifact' | 'deck' | 'template' | 'image' | 'video' | 'audio' | 'other'
export function createTabToTracking(tab: string): TrackingNewProjectTab {
switch (tab) {
case 'prototype':
return 'prototype';
case 'deck':
return 'slide_deck';
case 'template':
return 'from_template';
case 'live-artifact':
return 'live_artifact';
case 'image':
case 'video':
case 'audio':
return 'media';
case 'other':
return 'other';
default:
return 'prototype';
}
}
// Code `fidelity` is 'wireframe' | 'high-fidelity'; the CSV uses underscore.
export function fidelityToTracking(
fidelity: string | null | undefined,
): TrackingFidelity {
if (fidelity === 'wireframe') return 'wireframe';
if (fidelity === 'high-fidelity') return 'high_fidelity';
return 'not_applicable';
}
// Code `mode` ('daemon' | 'api') → CSV execution_mode.
export function executionModeToTracking(
mode: string | null | undefined,
): TrackingExecutionMode {
return mode === 'daemon' ? 'local_cli' : 'byok';
}
// Daemon agent id (apps/daemon/src/agents.ts) → CSV cli_provider_id.
export function agentIdToTracking(agentId: string | null | undefined): TrackingCliProviderId {
switch (agentId) {
case 'claude':
return 'claude_code';
case 'codex':
return 'codex_cli';
case 'devin':
return 'devin_for_terminal';
case 'gemini':
return 'gemini_cli';
case 'opencode':
return 'opencode';
case 'hermes':
return 'hermes';
case 'kimi':
return 'kimi_cli';
case 'cursor-agent':
return 'cursor_agent';
case 'qwen':
return 'qwen_code';
case 'qoder':
return 'qoder_cli';
case 'copilot':
return 'github_copilot_cli';
case 'pi':
return 'pi';
case 'kilo':
return 'kilo';
default:
return 'other';
}
}
// Code `apiProtocol` → v2 BYOK provider_id. The v1 wire values
// (azure / ollama / google) get the v2 spelling here.
export function byokProtocolToTracking(
protocol: string | null | undefined,
): TrackingByokProviderId | null {
switch (protocol) {
case 'anthropic':
return 'anthropic';
case 'openai':
return 'openai';
case 'azure':
case 'azure_openai':
return 'azure_openai';
case 'google':
case 'google_gemini':
return 'google_gemini';
case 'ollama':
case 'ollama_cloud':
return 'ollama_cloud';
case 'senseaudio':
return 'senseaudio';
default:
return null;
}
}
// Code `SettingsSection` from apps/web/src/components/SettingsDialog.tsx
// (the v0.8 settings sidebar). Sections that have no CSV counterpart still
// get emitted under the same event so dashboards can group them.
export function settingsSectionToTracking(
section: string,
): TrackingSettingsArea {
switch (section) {
case 'execution':
return 'configure_execution_mode';
case 'instructions':
return 'instructions';
case 'media':
return 'media_providers';
case 'language':
return 'language';
case 'appearance':
return 'appearance';
case 'pet':
return 'pets';
case 'about':
return 'about';
case 'composio':
case 'integrations':
case 'connectors':
return 'connectors';
case 'mcpClient':
case 'mcp_server':
return 'mcp_server';
case 'orbit':
return 'orbit';
case 'skills':
return 'skills';
case 'designSystems':
return 'design_systems';
case 'memory':
return 'memory';
case 'privacy':
return 'privacy';
case 'notifications':
return 'notifications';
case 'externalMcp':
return 'external_mcp';
default:
return 'configure_execution_mode';
}
}
// FileViewer renderer.id / file.kind → CSV artifact_kind.
export function artifactKindToTracking(args: {
rendererId?: string | null;
fileKind?: string | null;
}): TrackingArtifactKind {
const { rendererId, fileKind } = args;
if (rendererId === 'html' || rendererId === 'deck-html' || rendererId === 'react-component') {
return 'html';
}
if (rendererId === 'markdown') return 'markdown';
if (rendererId === 'svg') return 'image';
if (fileKind === 'image' || fileKind === 'sketch') return 'image';
if (fileKind === 'video') return 'video';
if (fileKind === 'audio') return 'audio';
if (
fileKind === 'pdf' ||
fileKind === 'document' ||
fileKind === 'presentation' ||
fileKind === 'spreadsheet'
) {
return 'doc';
}
return 'unknown';
}
// Bytes → CSV file_size_bucket (CSV row 48). 1 MB == 1024 * 1024 bytes.
export function fileSizeBucketToTracking(bytes: number): TrackingFileSizeBucket {
const mb = bytes / (1024 * 1024);
if (mb < 1) return '0_1mb';
if (mb < 10) return '1_10mb';
if (mb < 100) return '10_100mb';
return '100mb_plus';
}
// MIME / extension → CSV file_type.
export function fileTypeToTracking(args: {
mime?: string | null;
isFolder?: boolean;
isZip?: boolean;
}): TrackingFileType {
if (args.isFolder) return 'folder';
if (args.isZip) return 'zip';
const m = args.mime ?? '';
if (m.startsWith('image/')) return 'image';
if (m.startsWith('video/')) return 'video';
if (m.startsWith('audio/')) return 'audio';
if (m === 'application/pdf') return 'pdf';
return 'other';
}
// Pure helper deriving the v2 configure-state triplet from the execution
// config + detected agent list. Used both by the web client (to re-register
// the PostHog globals when the user switches mode / agent / BYOK
// credentials) and by the daemon `/api/runs` handler (so the
// authoritative run_created/finished captures carry consistent values).
//
// Inputs are intentionally narrow — caller passes only the bits that
// matter for analytics — so the helper has no coupling to the web's
// `AppConfig` shape or the daemon's `detectAgents` return type.
export interface DeriveConfigureGlobalsInput {
// 'daemon' = Local CLI execution mode; 'api' = BYOK execution mode.
// Anything else is treated as unknown.
mode?: string | null;
// Currently selected CLI agent id, if any.
agentId?: string | null;
// Available CLI agents detected on the user's machine. Only the
// `available` flag is read; the helper does not care about ids.
agents?: ReadonlyArray<{ id: string; available?: boolean }>;
// Whether a BYOK key/url has been saved (web client only — daemon
// can leave this undefined).
byokConfigured?: boolean;
}
export function deriveConfigureGlobals(
input: DeriveConfigureGlobalsInput,
): {
has_available_configure_cli: boolean;
configure_type: TrackingConfigureType;
configure_availability: TrackingConfigureAvailability;
} {
const agents = input.agents ?? [];
const hasAvailableCli = agents.some((a) => a.available === true);
const selectedAgent = input.agentId
? agents.find((a) => a.id === input.agentId)
: undefined;
const selectedAgentAvailable = selectedAgent?.available === true;
const byokConfigured = input.byokConfigured === true;
let configureType: TrackingConfigureType;
if (input.mode === 'daemon') {
configureType = byokConfigured ? 'both' : 'local_cli';
} else if (input.mode === 'api') {
configureType = hasAvailableCli ? 'both' : 'byok';
} else if (hasAvailableCli && byokConfigured) {
configureType = 'both';
} else if (hasAvailableCli) {
configureType = 'local_cli';
} else if (byokConfigured) {
configureType = 'byok';
} else {
configureType = 'none';
}
let configureAvailability: TrackingConfigureAvailability;
if (input.mode === 'daemon') {
configureAvailability = selectedAgentAvailable
? 'available'
: 'unavailable';
} else if (input.mode === 'api') {
configureAvailability = byokConfigured ? 'available' : 'unavailable';
} else if (hasAvailableCli || byokConfigured) {
configureAvailability = 'available';
} else {
configureAvailability = 'unknown';
}
return {
has_available_configure_cli: hasAvailableCli,
configure_type: configureType,
configure_availability: configureAvailability,
};
}
// Normalize the "other" custom-reason free text for transport. Trims
// whitespace and returns empty string when the field is blank or the user
// didn't select the "other" option. Callers should pass the raw text only
// when `has_custom_reason` is true; the helper itself is permissive.
export function normalizeCustomReason(
text: string | null | undefined,
): string {
return (text ?? '').trim();
}
// ---- Design-system tracking helpers --------------------------------------
// `length` is a character count (after trimming). Buckets match the
// v2 doc literally so brand description / notes / feedback all share
// the same shape.
export function designSystemLengthBucket(
text: string | null | undefined,
): TrackingDesignSystemLengthBucket {
const length = (text ?? '').trim().length;
if (length === 0) return '0';
if (length <= 50) return '1_50';
if (length <= 200) return '51_200';
if (length <= 500) return '201_500';
return '500_plus';
}
export function designSystemFolderCountBucket(
count: number | null | undefined,
): TrackingDesignSystemFolderCountBucket {
if (count === null || count === undefined || !Number.isFinite(count)) {
return 'unknown';
}
if (count <= 0) return '0';
if (count <= 10) return '1_10';
if (count <= 50) return '11_50';
if (count <= 200) return '51_200';
return '200_plus';
}
export function designSystemTotalSizeBucket(
bytes: number | null | undefined,
): TrackingDesignSystemTotalSizeBucket {
if (bytes === null || bytes === undefined || !Number.isFinite(bytes)) {
return 'unknown';
}
const mb = bytes / (1024 * 1024);
if (mb < 1) return '0_1mb';
if (mb < 10) return '1_10mb';
if (mb < 50) return '10_50mb';
return '50mb_plus';
}
// Slugifies a DESIGN.md section header (`## Typography & Type Scale`)
// into a stable module id (`typography-type-scale`). Lowercases, strips
// punctuation, collapses whitespace to `-`. Empty input → 'unknown'.
export function designSystemModuleSlug(
header: string | null | undefined,
): string {
const trimmed = (header ?? '').trim().replace(/^#+\s*/, '');
if (!trimmed) return 'unknown';
return (
trimmed
.toLowerCase()
.replace(/[^a-z0-9\s-]+/g, '')
.replace(/\s+/g, '-')
.replace(/-+/g, '-')
.replace(/^-|-$/g, '') || 'unknown'
);
}
// Maps a DESIGN.md section slug to one of the six review module
// types. Heuristic keyword match; defaults to `'other'`.
export function designSystemModuleType(
slug: string | null | undefined,
): TrackingDesignSystemModuleType {
const s = (slug ?? '').toLowerCase();
if (!s) return 'other';
if (/(typography|type|font)/.test(s)) return 'typography';
if (/(color|palette)/.test(s)) return 'colors';
if (/(spacing|layout|grid|radius|shadow|elevation)/.test(s)) {
return 'spacing';
}
if (/(component|button|input|form|icon|widget)/.test(s)) return 'components';
if (/(brand|asset|logo|image|illustration)/.test(s)) return 'brand_assets';
return 'other';
}
// Maps a repository URL host to the tracking enum. Unparseable URLs
// → `'unknown'`. Non-github/gitlab hosts → `'other'`.
export function designSystemRepoHostFromUrl(
url: string | null | undefined,
): TrackingDesignSystemRepoHost {
const raw = (url ?? '').trim();
if (!raw) return 'unknown';
try {
const host = new URL(raw).hostname.toLowerCase();
if (host === 'github.com' || host.endsWith('.github.com')) return 'github';
if (host === 'gitlab.com' || host.endsWith('.gitlab.com')) return 'gitlab';
return 'other';
} catch {
return 'unknown';
}
}