mirror of
https://github.com/stablyai/orca.git
synced 2026-10-08 16:02:37 +00:00
252 lines
9.8 KiB
TypeScript
252 lines
9.8 KiB
TypeScript
// Single source of truth for telemetry event names, schemas, and enums.
|
|
//
|
|
// Zod-first: every event schema is declared once and the compile-time
|
|
// `EventMap` is `z.infer`-derived from the same record the runtime validator
|
|
// consumes. There is no parallel `EVENT_SPEC` / hand-rolled union to drift
|
|
// out of sync with. Adding an event means adding a schema to `eventSchemas`;
|
|
// `EventMap` picks it up automatically and call sites that reference an
|
|
// unknown event name fail `tsc`.
|
|
//
|
|
// `.strict()` on every object schema is the runtime counterpart to "no extra
|
|
// keys." Free-form string fields carry an explicit `.max(N)` cap at the
|
|
// schema — the cap and the schema are the same thing; the validator does not
|
|
// re-check string length.
|
|
|
|
import { z } from 'zod'
|
|
|
|
import type { GlobalSettings } from './types'
|
|
|
|
// ── Shared property enums ───────────────────────────────────────────────
|
|
|
|
// Mirrors the shipped `TuiAgent` launch surface, with one deliberate shift:
|
|
// `claude` in settings/launch state ↔ `claude-code` here (product, not CLI
|
|
// string) so dashboards read cleanly.
|
|
//
|
|
// `other` remains as a telemetry escape hatch, but project-owned TuiAgents
|
|
// should map to concrete values; see `tuiAgentToAgentKind`.
|
|
export const AGENT_KIND_VALUES = [
|
|
'claude-code',
|
|
'codex',
|
|
'autohand',
|
|
'opencode',
|
|
'pi',
|
|
'gemini',
|
|
'aider',
|
|
'goose',
|
|
'amp',
|
|
'kilo',
|
|
'kiro',
|
|
'crush',
|
|
'aug',
|
|
'cline',
|
|
'codebuff',
|
|
'continue',
|
|
'cursor',
|
|
'droid',
|
|
'kimi',
|
|
'mistral-vibe',
|
|
'qwen-code',
|
|
'rovo',
|
|
'hermes',
|
|
'copilot',
|
|
'other'
|
|
] as const
|
|
export const agentKindSchema = z.enum(AGENT_KIND_VALUES)
|
|
export type AgentKind = z.infer<typeof agentKindSchema>
|
|
|
|
// Trimmed to the two values Orca's PTY-typed-command launch architecture can
|
|
// actually emit:
|
|
// - `binary_not_found` — `provider.spawn` ENOENT (the *shell* binary is
|
|
// missing). The agent CLI being missing is invisible: Orca spawns a
|
|
// healthy shell and types the command, and bash/zsh's "command not found"
|
|
// surfaces only as terminal output.
|
|
// - `unknown` — every other thrown error (paste-readiness timeout, env-build
|
|
// failures, unclassifiable shell-spawn errors).
|
|
// Provider-side errors (`auth_expired`, `rate_limited`, `network_timeout`,
|
|
// `provider_*`) happen inside the agent CLI subprocess and are not observable
|
|
// to Orca — see telemetry-plan.md §Decision: Defer per-incident error fields.
|
|
// Adding a new value is additive-safe; do it when the call site lands, not in
|
|
// anticipation.
|
|
export const errorClassSchema = z.enum(['binary_not_found', 'unknown'])
|
|
export type ErrorClass = z.infer<typeof errorClassSchema>
|
|
|
|
export const repoMethodSchema = z.enum(['folder_picker', 'clone_url', 'drag_drop'])
|
|
export type RepoMethod = z.infer<typeof repoMethodSchema>
|
|
|
|
export const workspaceSourceSchema = z.enum([
|
|
'command_palette',
|
|
'sidebar',
|
|
'shortcut',
|
|
'drag_drop',
|
|
'unknown'
|
|
])
|
|
export type WorkspaceSource = z.infer<typeof workspaceSourceSchema>
|
|
|
|
export const launchSourceSchema = z.enum([
|
|
'command_palette',
|
|
'sidebar',
|
|
'tab_bar_quick_launch',
|
|
'task_page',
|
|
'new_workspace_composer',
|
|
'workspace_jump_palette',
|
|
'shortcut',
|
|
'unknown'
|
|
])
|
|
export type LaunchSource = z.infer<typeof launchSourceSchema>
|
|
|
|
export const requestKindSchema = z.enum(['new', 'resume', 'followup'])
|
|
export type RequestKind = z.infer<typeof requestKindSchema>
|
|
|
|
// `env_var` is deliberately absent — env-var and CI paths override consent at
|
|
// runtime only (see consent.ts); they never mutate `optedIn` and therefore
|
|
// never fire a `telemetry_opted_in/out` event. If a future path explicitly
|
|
// persists an env-var-driven opt-out, add `env_var` back here together with
|
|
// the call site.
|
|
//
|
|
// `first_launch_notice` (new-user disclosure toast) is deliberately absent —
|
|
// the new-user cohort has no first-launch surface (see telemetry-plan.md
|
|
// §First-launch experience). Opt-outs from new users come through
|
|
// `via: 'settings'`.
|
|
export const optInViaSchema = z.enum(['first_launch_banner', 'settings'])
|
|
export type OptInVia = z.infer<typeof optInViaSchema>
|
|
|
|
// Whitelist of settings whose `setting_key` may be emitted on
|
|
// `settings_changed`. If a setting isn't in this list, we do not emit.
|
|
//
|
|
// Keys are camelCase to match the actual field names in `GlobalSettings`.
|
|
// `orca_channel` is intentionally absent — it is a build-time common
|
|
// property baked in from `ORCA_BUILD_IDENTITY`, not a user-togglable setting.
|
|
//
|
|
// Intentionally does NOT include the telemetry opt-in toggle — that is
|
|
// covered by the dedicated `telemetry_opted_in` / `telemetry_opted_out`
|
|
// events, which carry `via` context that a plain `settings_changed` could
|
|
// not. Listing it here would double-fire.
|
|
//
|
|
// Kept as an `as const` tuple so the Zod enum below and any call-site usage
|
|
// share one array — typo-drift is impossible.
|
|
type BooleanGlobalSettingsKey = {
|
|
[Key in keyof GlobalSettings]-?: GlobalSettings[Key] extends boolean ? Key : never
|
|
}[keyof GlobalSettings]
|
|
export const SETTINGS_CHANGED_WHITELIST = [
|
|
'editorAutoSave',
|
|
'openLinksInApp',
|
|
'experimentalAgentDashboard',
|
|
'experimentalMobile',
|
|
'experimentalSidekick',
|
|
'experimentalWorktreeSymlinks',
|
|
'geminiCliOAuthEnabled'
|
|
] as const satisfies readonly BooleanGlobalSettingsKey[]
|
|
export const settingsChangedKeySchema = z.enum(SETTINGS_CHANGED_WHITELIST)
|
|
export type SettingsChangedKey = z.infer<typeof settingsChangedKeySchema>
|
|
|
|
// ── Per-event schemas ───────────────────────────────────────────────────
|
|
//
|
|
// `.strict()` on every object is what enforces "no extra keys" at runtime —
|
|
// the validator does not need a separate extra-key check because zod rejects
|
|
// unknown keys at parse time. This is the runtime counterpart to the
|
|
// compile-time "unions of string literals, no raw `string`" rule.
|
|
|
|
const emptySchema = z.object({}).strict()
|
|
|
|
const repoAddedSchema = z.object({ method: repoMethodSchema }).strict()
|
|
|
|
const workspaceCreatedSchema = z
|
|
.object({
|
|
source: workspaceSourceSchema,
|
|
from_existing_branch: z.boolean()
|
|
})
|
|
.strict()
|
|
|
|
const agentStartedSchema = z
|
|
.object({
|
|
agent_kind: agentKindSchema,
|
|
launch_source: launchSourceSchema,
|
|
request_kind: requestKindSchema
|
|
})
|
|
.strict()
|
|
|
|
// Enum-only by design for both fields. `error_message` and `error_stack` are
|
|
// deliberately absent — `.strict()` rejects either key if a call site ever
|
|
// tries to attach one, which fails the validator and drops the event. Raw
|
|
// error strings carry arbitrary user/workspace/path content; keeping them off
|
|
// the wire is the only way to guarantee we never transmit them by accident.
|
|
const agentErrorSchema = z
|
|
.object({
|
|
error_class: errorClassSchema,
|
|
agent_kind: agentKindSchema
|
|
})
|
|
.strict()
|
|
|
|
const settingsChangedSchema = z
|
|
.object({
|
|
setting_key: settingsChangedKeySchema,
|
|
value_kind: z.enum(['bool', 'enum'])
|
|
})
|
|
.strict()
|
|
|
|
const telemetryOptedInSchema = z.object({ via: optInViaSchema }).strict()
|
|
const telemetryOptedOutSchema = z.object({ via: optInViaSchema }).strict()
|
|
|
|
// ── Event registry: the one record the validator consumes ───────────────
|
|
//
|
|
// The validator does `eventSchemas[name].safeParse(props)`. `EventMap` is
|
|
// `z.infer`-derived from this record, so there is exactly one source of
|
|
// truth for both compile-time types and runtime validation.
|
|
//
|
|
// Schema-evolution / versioning doctrine:
|
|
// Breaking changes (renaming a field, changing an enum's meaning, removing a
|
|
// required key) require a new event name (e.g. `agent_started_v2`), not an
|
|
// in-place edit. Additive-optional fields (`z.field().optional()`) are safe
|
|
// to add in place. This keeps PostHog funnels clean — an in-place breaking
|
|
// change silently blends pre- and post-change rows under one event name,
|
|
// which cannot be unmixed after the fact.
|
|
export const eventSchemas = {
|
|
app_opened: emptySchema,
|
|
|
|
repo_added: repoAddedSchema,
|
|
workspace_created: workspaceCreatedSchema,
|
|
|
|
agent_started: agentStartedSchema,
|
|
agent_error: agentErrorSchema,
|
|
|
|
settings_changed: settingsChangedSchema,
|
|
|
|
telemetry_opted_in: telemetryOptedInSchema,
|
|
telemetry_opted_out: telemetryOptedOutSchema
|
|
} as const
|
|
|
|
export type EventMap = { [N in keyof typeof eventSchemas]: z.infer<(typeof eventSchemas)[N]> }
|
|
export type EventName = keyof EventMap
|
|
export type EventProps<N extends EventName> = EventMap[N]
|
|
|
|
// Common props attached by the client — declared here so the validator knows
|
|
// which keys to allow on every outgoing event.
|
|
//
|
|
// No `env: 'prod' | 'dev'` property. Every transmitted event is by
|
|
// construction from an official CI build, so a wire discriminator would be
|
|
// redundant. Contributor / `pnpm dev` builds do not transmit at all; they
|
|
// console-mirror.
|
|
//
|
|
// Every string field carries the 64-char cap directly — this is what the
|
|
// validator's "string-length cap" rule is made of; there is no separate
|
|
// post-parse length check to keep in sync with the schema.
|
|
export const commonPropsSchema = z
|
|
.object({
|
|
app_version: z.string().max(64),
|
|
platform: z.string().max(64),
|
|
arch: z.string().max(64),
|
|
os_release: z.string().max(64),
|
|
// `install_id` is used as PostHog's `distinctId` and `session_id` is the
|
|
// per-process correlation key — an empty string on either would collapse
|
|
// unrelated events into a single synthetic "user" / "session" and
|
|
// silently corrupt analytics. `.min(1)` rejects that actual observed
|
|
// failure mode without pinning the shape to UUIDs (both ids come from
|
|
// `randomUUID()` today, but forward-compatibility with a future id
|
|
// scheme is cheap to preserve).
|
|
install_id: z.string().min(1).max(64),
|
|
session_id: z.string().min(1).max(64),
|
|
orca_channel: z.enum(['stable', 'rc'])
|
|
})
|
|
.strict()
|
|
export type CommonProps = z.infer<typeof commonPropsSchema>
|