| |
| |
| |
| |
| |
| |
|
|
| export const DISCLOSURE_CATEGORIES = [ |
| { id: 'usage', label: 'App usage' }, |
| { id: 'generation', label: 'AI generation' }, |
| { id: 'projects', label: 'Projects and deployments' }, |
| { id: 'features', label: 'Feature usage' }, |
| ] as const; |
|
|
| export type DisclosureCategoryId = (typeof DISCLOSURE_CATEGORIES)[number]['id']; |
|
|
| export interface TelemetryEventDef { |
| |
| category: DisclosureCategoryId; |
| |
| disclosure: string; |
| } |
|
|
| export const EVENT_CATALOG = { |
| |
| session_start: { category: 'usage', disclosure: 'When a session starts' }, |
| pageview: { category: 'usage', disclosure: 'Which views are visited (e.g. dashboard, workspace, settings)' }, |
| heartbeat: { category: 'usage', disclosure: 'Session heartbeats (how long the app is open)' }, |
| telemetry_accepted: { category: 'usage', disclosure: 'That the analytics notice was acknowledged' }, |
| telemetry_disabled: { category: 'usage', disclosure: 'That analytics were turned off (final event before stopping)' }, |
| tour_started: { category: 'usage', disclosure: 'Whether the guided tour is started, completed, or exited early (and at which step)' }, |
| tour_completed: { category: 'usage', disclosure: 'Whether the guided tour is started, completed, or exited early (and at which step)' }, |
| tour_abandoned: { category: 'usage', disclosure: 'Whether the guided tour is started, completed, or exited early (and at which step)' }, |
|
|
| |
| provider_selected: { category: 'generation', disclosure: 'Which AI providers are selected (and whether a key is set, never the key)' }, |
| model_selected: { category: 'generation', disclosure: 'Which AI models are selected' }, |
| task_started: { category: 'generation', disclosure: 'When a generation task starts' }, |
| task_complete: { category: 'generation', disclosure: 'Whether tasks succeed, with duration and tool/turn counts (not what was asked)' }, |
| task_fail: { category: 'generation', disclosure: 'Whether tasks fail and why in broad terms (api error vs stopped), how long the task ran, never the error message itself' }, |
| task_nudged: { category: 'generation', disclosure: 'How often the AI needs a reminder to finish properly' }, |
| tool_call: { category: 'generation', disclosure: 'Which tools the AI uses, whether they work, and how long they take (never file paths, arguments, or contents)' }, |
| api_error: { category: 'generation', disclosure: 'API and tool error types (not error messages)' }, |
| compaction_fired: { category: 'generation', disclosure: 'Whether conversation compaction ran, with token counts only (not messages)' }, |
| agent_spawned: { category: 'generation', disclosure: 'When the AI delegates to a sub-agent (explore, plan, task)' }, |
| skill_read: { category: 'generation', disclosure: 'Whether the AI consulted a skill file (built-in skill names only; custom skills are counted anonymously)' }, |
| ask_response: { category: 'generation', disclosure: 'Whether AI questions are answered by tapping an option or typing' }, |
|
|
| |
| project_create: { category: 'projects', disclosure: 'Which creation flow and runtime are used when a project is created (e.g. quick vs. describe, static vs. react)' }, |
| project_open: { category: 'projects', disclosure: 'When a project is opened, deleted, exported, or imported (never its name or contents; export format only)' }, |
| project_delete: { category: 'projects', disclosure: 'When a project is opened, deleted, exported, or imported (never its name or contents; export format only)' }, |
| project_export: { category: 'projects', disclosure: 'When a project is opened, deleted, exported, or imported (never its name or contents; export format only)' }, |
| project_import: { category: 'projects', disclosure: 'When a project is opened, deleted, exported, or imported (never its name or contents; export format only)' }, |
| runtime_switch: { category: 'projects', disclosure: 'When a project runtime is switched (e.g. static to react)' }, |
| deployment_create: { category: 'projects', disclosure: 'When deployments are created, published, or deleted, and whether a custom domain is configured (not the domain)' }, |
| deployment_publish: { category: 'projects', disclosure: 'When deployments are created, published, or deleted, and whether a custom domain is configured (not the domain)' }, |
| deployment_delete: { category: 'projects', disclosure: 'When deployments are created, published, or deleted, and whether a custom domain is configured (not the domain)' }, |
| custom_domain_set: { category: 'projects', disclosure: 'When deployments are created, published, or deleted, and whether a custom domain is configured (not the domain)' }, |
| backend_feature_enabled: { category: 'projects', disclosure: 'Which kinds of backend features are enabled (e.g. database, functions)' }, |
| sync_manual: { category: 'projects', disclosure: 'Manual server sync usage by item type and direction (never item names or contents)' }, |
| sync_fail: { category: 'projects', disclosure: 'Whether server sync operations fail (not the data involved)' }, |
|
|
| |
| mode_switch: { category: 'features', disclosure: 'Switches between Chat, Code, and Interview modes' }, |
| interview_started: { category: 'features', disclosure: 'Interview usage: which built-in template is used (custom templates counted anonymously) and whether interviews complete' }, |
| interview_completed:{ category: 'features', disclosure: 'Interview usage: which built-in template is used (custom templates counted anonymously) and whether interviews complete' }, |
| interview_abandoned:{ category: 'features', disclosure: 'Interview usage: which built-in template is used (custom templates counted anonymously) and whether interviews complete' }, |
| handoff_used: { category: 'features', disclosure: 'Whether the interview handoff button is used' }, |
| skill_created: { category: 'features', disclosure: 'That custom skills or templates are created or deleted (counts only, never names or contents)' }, |
| skill_deleted: { category: 'features', disclosure: 'That custom skills or templates are created or deleted (counts only, never names or contents)' }, |
| interview_template_created: { category: 'features', disclosure: 'That custom skills or templates are created or deleted (counts only, never names or contents)' }, |
| interview_template_deleted: { category: 'features', disclosure: 'That custom skills or templates are created or deleted (counts only, never names or contents)' }, |
| model_template_created: { category: 'features', disclosure: 'That custom skills or templates are created or deleted (counts only, never names or contents)' }, |
| model_template_deleted: { category: 'features', disclosure: 'That custom skills or templates are created or deleted (counts only, never names or contents)' }, |
| connection_added: { category: 'features', disclosure: 'Which provider connections are added or removed (never keys or endpoints)' }, |
| connection_removed: { category: 'features', disclosure: 'Which provider connections are added or removed (never keys or endpoints)' }, |
| image_attached: { category: 'features', disclosure: 'Whether an image was attached to a chat message (not the image itself)' }, |
| voice_input_used: { category: 'features', disclosure: 'Whether voice input is used and how the clip is handled (never audio or transcripts)' }, |
| code_edited: { category: 'features', disclosure: 'Whether project code was edited by hand during a session (a yes/no, nothing about the edits)' }, |
| checkpoint_restore: { category: 'features', disclosure: 'Whether checkpoints are restored or changes discarded' }, |
| changes_discarded: { category: 'features', disclosure: 'Whether checkpoints are restored or changes discarded' }, |
| } as const satisfies Record<string, TelemetryEventDef>; |
|
|
| export type TelemetryEventName = keyof typeof EVENT_CATALOG; |
|
|
| export type TelemetryEventProperties = Record<string, unknown>; |
|
|
| export interface TelemetryEvent { |
| event: TelemetryEventName; |
| timestamp: number; |
| fields: Record<string, unknown>; |
| } |
|
|
| |
| export function getDisclosureLines(): { label: string; lines: string[] }[] { |
| return DISCLOSURE_CATEGORIES.map(cat => { |
| const lines: string[] = []; |
| for (const def of Object.values(EVENT_CATALOG)) { |
| if (def.category === cat.id && !lines.includes(def.disclosure)) lines.push(def.disclosure); |
| } |
| return { label: cat.label, lines }; |
| }).filter(g => g.lines.length > 0); |
| } |
|
|