mirror of
https://github.com/stablyai/orca.git
synced 2026-10-24 08:03:58 +00:00
Split telemetry event schema responsibilities
This commit is contained in:
@@ -57,7 +57,6 @@ inline src/renderer/src/store/slices/linear.ts
|
||||
inline src/renderer/src/store/slices/tabs.ts
|
||||
inline src/renderer/src/store/slices/ui.ts
|
||||
inline src/shared/keybindings.ts
|
||||
inline src/shared/telemetry-events.ts
|
||||
mobile-config app/h/*/files/*.tsx
|
||||
mobile-config app/h/*/session/*.tsx
|
||||
mobile-config app/h/*/source-control/*.tsx
|
||||
|
||||
@@ -0,0 +1,134 @@
|
||||
import { z } from 'zod'
|
||||
import { appStarSourceSchema } from './gh-star-source'
|
||||
import {
|
||||
FEATURE_INTERACTION_CATEGORIES,
|
||||
FEATURE_INTERACTION_IDS,
|
||||
FEATURE_INTERACTION_USAGE_BUCKETS,
|
||||
getFeatureInteractionCategory
|
||||
} from './feature-interactions'
|
||||
import {
|
||||
starNagAgentBucketSchema,
|
||||
starNagOutcomeSchema,
|
||||
starNagPromptModeSchema,
|
||||
starNagPromptSourceSchema
|
||||
} from './star-nag-telemetry'
|
||||
import {
|
||||
agentKindSchema,
|
||||
errorClassSchema,
|
||||
launchSourceSchema,
|
||||
repoMethodSchema,
|
||||
requestKindSchema,
|
||||
workspaceSourceSchema
|
||||
} from './telemetry-property-schemas'
|
||||
|
||||
// ── Per-event schemas ───────────────────────────────────────────────────
|
||||
|
||||
// Cohort signal (repo count at emit time); `.optional()` lets a fail-soft `undefined` validate. See docs/onboarding-funnel-cohort-addendum.md.
|
||||
export const nthRepoAddedSchema = z.number().int().nonnegative().optional()
|
||||
|
||||
export const appOpenedSchema = z.object({ nth_repo_added: nthRepoAddedSchema }).strict()
|
||||
|
||||
export const featureInteractionIdSchema = z.enum(FEATURE_INTERACTION_IDS)
|
||||
export const featureInteractionCategorySchema = z.enum(FEATURE_INTERACTION_CATEGORIES)
|
||||
export const featureInteractionUsageBucketSchema = z.enum(FEATURE_INTERACTION_USAGE_BUCKETS)
|
||||
export const featureInteractionUsageBucketSourceSchema = z.enum([
|
||||
'crossed_now',
|
||||
'observed_existing'
|
||||
])
|
||||
export const featureInteractionUsageBucketReachedSchema = z
|
||||
.object({
|
||||
feature_id: featureInteractionIdSchema,
|
||||
feature_category: featureInteractionCategorySchema,
|
||||
count_bucket: featureInteractionUsageBucketSchema,
|
||||
bucket_source: featureInteractionUsageBucketSourceSchema,
|
||||
nth_repo_added: nthRepoAddedSchema
|
||||
})
|
||||
.strict()
|
||||
.refine((value) => getFeatureInteractionCategory(value.feature_id) === value.feature_category, {
|
||||
message: 'feature_category must match feature_id',
|
||||
path: ['feature_category']
|
||||
})
|
||||
|
||||
export const repoAddedSchema = z
|
||||
// Why: `.optional()` so paths that can't detect git-ness validate cleanly; never default-guess `false` — omit instead.
|
||||
.object({
|
||||
method: repoMethodSchema,
|
||||
is_git_repo: z.boolean().optional(),
|
||||
nth_repo_added: nthRepoAddedSchema
|
||||
})
|
||||
.strict()
|
||||
|
||||
export const appStarredOrcaSchema = z
|
||||
.object({
|
||||
source: appStarSourceSchema,
|
||||
nth_repo_added: nthRepoAddedSchema
|
||||
})
|
||||
.strict()
|
||||
|
||||
export const starNagOutcomeEventSchema = z
|
||||
.object({
|
||||
outcome: starNagOutcomeSchema,
|
||||
source: starNagPromptSourceSchema,
|
||||
mode: starNagPromptModeSchema,
|
||||
threshold: z.number().int().positive(),
|
||||
agents_since_baseline: z.number().int().nonnegative(),
|
||||
agents_since_baseline_bucket: starNagAgentBucketSchema,
|
||||
nth_repo_added: nthRepoAddedSchema,
|
||||
next_threshold: z.number().int().positive().optional(),
|
||||
cooldown_days: z.number().int().positive().optional()
|
||||
})
|
||||
.strict()
|
||||
.refine(
|
||||
(payload) =>
|
||||
payload.next_threshold === undefined ||
|
||||
payload.outcome === 'dismissed' ||
|
||||
payload.outcome === 'later',
|
||||
{
|
||||
message: 'next_threshold is only valid for later or dismissed outcomes',
|
||||
path: ['next_threshold']
|
||||
}
|
||||
)
|
||||
.refine(
|
||||
(payload) =>
|
||||
payload.cooldown_days === undefined ||
|
||||
payload.outcome === 'later' ||
|
||||
payload.outcome === 'dismissed',
|
||||
{
|
||||
message: 'cooldown_days is only valid for later or dismissed outcomes',
|
||||
path: ['cooldown_days']
|
||||
}
|
||||
)
|
||||
|
||||
export const workspaceCreatedSchema = z
|
||||
.object({
|
||||
source: workspaceSourceSchema,
|
||||
from_existing_branch: z.boolean(),
|
||||
nth_repo_added: nthRepoAddedSchema
|
||||
})
|
||||
.strict()
|
||||
|
||||
export const agentStartedSchema = z
|
||||
.object({
|
||||
agent_kind: agentKindSchema,
|
||||
launch_source: launchSourceSchema,
|
||||
request_kind: requestKindSchema,
|
||||
nth_repo_added: nthRepoAddedSchema
|
||||
})
|
||||
.strict()
|
||||
export const agentPromptSentSchema = z
|
||||
.object({
|
||||
agent_kind: agentKindSchema,
|
||||
launch_source: launchSourceSchema,
|
||||
request_kind: requestKindSchema,
|
||||
nth_repo_added: nthRepoAddedSchema
|
||||
})
|
||||
.strict()
|
||||
|
||||
// Enum-only by design: `.strict()` blocks `error_message`/`error_stack`, keeping raw user/path content off the wire.
|
||||
export const agentErrorSchema = z
|
||||
.object({
|
||||
error_class: errorClassSchema,
|
||||
agent_kind: agentKindSchema,
|
||||
nth_repo_added: nthRepoAddedSchema
|
||||
})
|
||||
.strict()
|
||||
@@ -0,0 +1,174 @@
|
||||
import { z } from 'zod'
|
||||
import { AGENT_HOOK_TARGETS } from './agent-hook-types'
|
||||
import {
|
||||
DAEMON_LIFECYCLE_SESSION_BUCKETS,
|
||||
DAEMON_LIFECYCLE_TRANSITIONS,
|
||||
DAEMON_REPLACE_REASONS,
|
||||
DAEMON_RETIRE_REASONS
|
||||
} from './daemon-lifecycle-telemetry'
|
||||
import {
|
||||
DAEMON_AUDIT_GENERATION_ROLE_VALUES,
|
||||
DAEMON_AUDIT_PROCESS_REASON_VALUES,
|
||||
DAEMON_AUDIT_REASON_VALUES,
|
||||
DAEMON_AUDIT_STATE_VALUES,
|
||||
DAEMON_AUDIT_TRIGGER_VALUES,
|
||||
DAEMON_EVIDENCE_SOURCE_VALUES
|
||||
} from './daemon-audit-eligibility'
|
||||
import { errorClassSchema, settingsChangedKeySchema } from './telemetry-property-schemas'
|
||||
|
||||
// Why: daemon start-failure signal (fleet-wide outage like v1.4.129-rc.1); enum-only so raw stderr never reaches the wire.
|
||||
export const daemonStartFailedSchema = z.object({ error_class: errorClassSchema }).strict()
|
||||
|
||||
export const runtimeRpcStartErrorClassSchema = z.enum([
|
||||
'permission_denied',
|
||||
'address_in_use',
|
||||
'storage_unavailable',
|
||||
'invalid_path',
|
||||
'unknown'
|
||||
])
|
||||
export type RuntimeRpcStartErrorClass = z.infer<typeof runtimeRpcStartErrorClassSchema>
|
||||
|
||||
// Why: runtime discovery failures can contain user paths; keep telemetry to closed filesystem/socket categories.
|
||||
export const runtimeRpcStartFailedSchema = z
|
||||
.object({ error_class: runtimeRpcStartErrorClassSchema })
|
||||
.strict()
|
||||
|
||||
// Why: classify session-killing 1013 closures as producer size failures or queue backpressure.
|
||||
export const remoteOutboundBudgetCloseSchema = z
|
||||
.object({ emitter: z.enum(['size', 'queue']) })
|
||||
.strict()
|
||||
|
||||
// Why: a deadlocked main thread never crashes, so it produces no crash report and no user report
|
||||
// beyond "it froze" — incidence has been unmeasurable. `self_recovered` splits stalls that cleared
|
||||
// from ones that never did, which is the number that decides whether auto-recovery is ever safe to
|
||||
// build: every self-recovered stall is a kill that design would have gotten wrong. `unresponsive_ms`
|
||||
// is the observed silence, kept raw so the 45s threshold can be calibrated against real tails.
|
||||
export const mainThreadHangDetectedSchema = z
|
||||
.object({
|
||||
unresponsive_ms: z.number().int().nonnegative(),
|
||||
self_recovered: z.boolean()
|
||||
})
|
||||
.strict()
|
||||
|
||||
// Why: daemon replace/retire lifecycle signal — issue #7936 was undiagnosable without asking a user for daemon.log.
|
||||
// Enum-only + bucketed session count so no paths, raw versions, or exact counts reach the wire.
|
||||
// The union keeps each reason pinned to its transition, so a death can't be reported as a replace.
|
||||
export const daemonLifecycleSchema = z.discriminatedUnion('transition', [
|
||||
z
|
||||
.object({
|
||||
transition: z.literal(DAEMON_LIFECYCLE_TRANSITIONS[0]),
|
||||
reason: z.enum(DAEMON_REPLACE_REASONS),
|
||||
live_session_count_bucket: z.enum(DAEMON_LIFECYCLE_SESSION_BUCKETS)
|
||||
})
|
||||
.strict(),
|
||||
z
|
||||
.object({
|
||||
transition: z.literal(DAEMON_LIFECYCLE_TRANSITIONS[1]),
|
||||
reason: z.enum(DAEMON_RETIRE_REASONS),
|
||||
live_session_count_bucket: z.enum(DAEMON_LIFECYCLE_SESSION_BUCKETS)
|
||||
})
|
||||
.strict()
|
||||
])
|
||||
|
||||
export const daemonAuditEligibilityBaseSchema = z.object({
|
||||
state: z.enum(DAEMON_AUDIT_STATE_VALUES),
|
||||
reason: z.enum(DAEMON_AUDIT_REASON_VALUES),
|
||||
trigger: z.enum(DAEMON_AUDIT_TRIGGER_VALUES),
|
||||
evidence_sources: z.array(z.enum(DAEMON_EVIDENCE_SOURCE_VALUES)).min(1).max(12),
|
||||
protocol_generation: z.number().int().positive().max(1_000),
|
||||
generation_role: z.enum(DAEMON_AUDIT_GENERATION_ROLE_VALUES),
|
||||
provider: z.literal('local-daemon'),
|
||||
endpoint_kind: z.enum(['unix-socket', 'windows-named-pipe']),
|
||||
profile_scope: z.enum(['configured', 'unspecified']),
|
||||
reachability: z.enum(['authenticated', 'disconnected', 'unknown']),
|
||||
inventory_authority: z.enum(['authoritative', 'unavailable']),
|
||||
process_liveness: z.enum(['present', 'gone', 'unknown']),
|
||||
process_reason: z.enum(DAEMON_AUDIT_PROCESS_REASON_VALUES).nullable(),
|
||||
endpoint_state: z.enum(['missing', 'named-pipe', 'non-socket', 'socket', 'unknown'])
|
||||
})
|
||||
|
||||
export const daemonAuditEligibilitySchema = z.discriminatedUnion('exact_incarnation', [
|
||||
daemonAuditEligibilityBaseSchema
|
||||
.extend({
|
||||
exact_incarnation: z.literal('endpoint-identity'),
|
||||
exact_incarnation_correlation: z.string().regex(/^v1:[0-9a-f]{32}$/)
|
||||
})
|
||||
.strict(),
|
||||
daemonAuditEligibilityBaseSchema
|
||||
.extend({
|
||||
exact_incarnation: z.literal('endpoint-identity-linux-ticks'),
|
||||
exact_incarnation_correlation: z.string().regex(/^v1:[0-9a-f]{32}$/)
|
||||
})
|
||||
.strict(),
|
||||
daemonAuditEligibilityBaseSchema.extend({ exact_incarnation: z.literal('unavailable') }).strict()
|
||||
])
|
||||
|
||||
// Rollout signal for granting Codex hook trust via codex app-server RPCs
|
||||
// instead of Orca's self-computed trusted_hash. `fallback`/`verify_failed`
|
||||
// spikes mean the RPC lane is not taking; steady-state ledger skips are not
|
||||
// reported (they would only measure launch volume). `lane` attributes the
|
||||
// grant surface (real ~/.codex vs managed home); `error_class`/`verify_class`
|
||||
// are closed classifications so `error` fallbacks are diagnosable in the
|
||||
// field — e.g. `binary-missing` = codex CLI absent, no rollout impact.
|
||||
export const codexTrustGrantSchema = z
|
||||
.object({
|
||||
outcome: z.enum(['granted', 'fallback', 'verify_failed']),
|
||||
host_kind: z.enum(['native', 'wsl']),
|
||||
lane: z.enum(['real-home', 'managed']),
|
||||
fallback_reason: z
|
||||
.enum([
|
||||
'disabled',
|
||||
'no-managed-entries',
|
||||
'unsupported',
|
||||
'unsupported-cached',
|
||||
'verify-failed',
|
||||
'retry-cached',
|
||||
'error'
|
||||
])
|
||||
.optional(),
|
||||
error_class: z
|
||||
.enum(['binary-missing', 'timeout', 'entry-failed', 'early-exit', 'rpc-failed', 'unexpected'])
|
||||
.optional(),
|
||||
verify_class: z
|
||||
.enum([
|
||||
'list-mismatch',
|
||||
'post-grant-untrusted',
|
||||
'post-grant-mismatch',
|
||||
'unexpected-key',
|
||||
'duplicate-key',
|
||||
'coverage'
|
||||
])
|
||||
.optional()
|
||||
})
|
||||
.strict()
|
||||
|
||||
export const settingsChangedSchema = z
|
||||
.object({
|
||||
setting_key: settingsChangedKeySchema,
|
||||
value_kind: z.enum(['bool', 'enum'])
|
||||
})
|
||||
.strict()
|
||||
|
||||
// Managed-hook installer label from `AGENT_HOOK_TARGETS`, distinct from `AGENT_KIND_VALUES`; `claude` (not `claude-code`) is intentional.
|
||||
export const hookInstallAgentSchema = z.enum(AGENT_HOOK_TARGETS)
|
||||
export type HookInstallAgent = z.infer<typeof hookInstallAgentSchema>
|
||||
|
||||
// Why: config-shape errors (not user content); callers must truncate before `track` — `.max(200)` drops overlength strings.
|
||||
export const agentHookInstallFailedSchema = z
|
||||
.object({
|
||||
agent: hookInstallAgentSchema,
|
||||
error_message: z.string().max(200)
|
||||
})
|
||||
.strict()
|
||||
|
||||
// Why: regression signal for paneKey attribution — a hook event that can't route to a pane. See docs/cli-terminal-hook-pane-key.md.
|
||||
export const agentHookUnattributedSchema = z
|
||||
.object({ reason: z.enum(['empty_pane_key', 'unknown_tab_id']) })
|
||||
.strict()
|
||||
|
||||
// Why (#11217): loopback hook POSTs reset mid-body by local security software kill agent status for
|
||||
// every runtime at once. Count only — the truncated bodies carry user prompts and tool I/O, so
|
||||
// nothing derived from them may reach the wire.
|
||||
export const agentHookTransportBlockedSchema = z
|
||||
.object({ count: z.number().int().nonnegative() })
|
||||
.strict()
|
||||
@@ -0,0 +1,135 @@
|
||||
import { z } from 'zod'
|
||||
|
||||
import { eventSchemas } from './telemetry-event-registry'
|
||||
import type { cohortSchema } from './telemetry-onboarding-foundation-schemas'
|
||||
|
||||
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]
|
||||
|
||||
// Why: non-`ZodObject` schemas have no `.shape`; return null so `key in undefined` can't throw at module load.
|
||||
function eventSchemaShape(schema: z.ZodTypeAny): z.ZodRawShape | null {
|
||||
if (schema instanceof z.ZodObject) {
|
||||
return schema.shape
|
||||
}
|
||||
|
||||
const shapeBearingSchema = schema as { shape?: unknown }
|
||||
// Why: refined object schemas may expose `.shape` even when refinement breaks `instanceof ZodObject`.
|
||||
if (shapeBearingSchema.shape && typeof shapeBearingSchema.shape === 'object') {
|
||||
return shapeBearingSchema.shape as z.ZodRawShape
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
function eventsWithShapeKey(key: string): ReadonlySet<EventName> {
|
||||
return new Set(
|
||||
(Object.entries(eventSchemas) as [EventName, z.ZodTypeAny][])
|
||||
.filter(([, schema]) => {
|
||||
const shape = eventSchemaShape(schema)
|
||||
return shape !== null && key in shape
|
||||
})
|
||||
.map(([name]) => name)
|
||||
)
|
||||
}
|
||||
|
||||
// Cohort injection is gated on this derived set because `.strict()` schemas drop events that don't declare `nth_repo_added`.
|
||||
const COHORT_EXTENDED_SET = eventsWithShapeKey('nth_repo_added')
|
||||
|
||||
// Compile-time roster guarding the runtime injection set against silent schema drift.
|
||||
type _CohortExtendedRoster =
|
||||
| 'app_opened'
|
||||
| 'app_starred_orca'
|
||||
| 'star_nag_outcome'
|
||||
| 'feature_interaction_usage_bucket_reached'
|
||||
| 'repo_added'
|
||||
| 'add_repo_setup_step_action'
|
||||
| 'add_repo_existing_workspaces_detected'
|
||||
| 'add_repo_default_checkout_handoff'
|
||||
| 'add_repo_nested_scan_result'
|
||||
| 'add_repo_nested_import_action'
|
||||
| 'add_repo_nested_import_result'
|
||||
| 'workspace_created'
|
||||
| 'workspace_create_failed'
|
||||
| 'setup_script_prompt_shown'
|
||||
| 'setup_script_prompt_action'
|
||||
| 'agent_started'
|
||||
| 'agent_prompt_sent'
|
||||
| 'agent_error'
|
||||
| 'orca_cli_feature_tip_shown'
|
||||
| 'orca_cli_feature_tip_setup_clicked'
|
||||
| 'orca_cli_feature_tip_setup_result'
|
||||
| 'cmd_j_palette_feature_tip_shown'
|
||||
| 'cmd_j_palette_feature_tip_acknowledged'
|
||||
// Why: strict empty payloads infer a string index signature; ignore index-only keys so they aren't pulled into keyed rosters.
|
||||
type _KnownPayloadKeys<T> = string extends keyof T ? never : keyof T
|
||||
type _DerivedCohortExtendedEvents = {
|
||||
[N in EventName]: 'nth_repo_added' extends _KnownPayloadKeys<EventMap[N]> ? N : never
|
||||
}[EventName]
|
||||
type _CohortExtendedRosterSync = _CohortExtendedRoster extends _DerivedCohortExtendedEvents
|
||||
? _DerivedCohortExtendedEvents extends _CohortExtendedRoster
|
||||
? true
|
||||
: never
|
||||
: never
|
||||
const _cohortExtendedRosterSyncCheck: _CohortExtendedRosterSync = true
|
||||
void _cohortExtendedRosterSyncCheck
|
||||
|
||||
export function isCohortExtendedEvent(name: EventName): boolean {
|
||||
return COHORT_EXTENDED_SET.has(name)
|
||||
}
|
||||
|
||||
// Events whose schema declares `cohort`: the IPC handler injects cohort only for these — a `.strict()` schema without it would reject the event.
|
||||
const ONBOARDING_COHORT_SET = eventsWithShapeKey('cohort')
|
||||
// `NonNullable` strips `undefined` introduced by `cohortSchema`'s `.optional()`.
|
||||
export type OnboardingCohort = NonNullable<z.infer<typeof cohortSchema>>
|
||||
|
||||
// Compile-time roster: dropping `cohort` from any of these fails tsc, rather than silently at runtime (`.optional()` would tolerate that).
|
||||
type _OnboardingCohortRoster =
|
||||
| 'onboarding_started'
|
||||
| 'onboarding_step_viewed'
|
||||
| 'onboarding_step_completed'
|
||||
| 'onboarding_step_skipped'
|
||||
| 'onboarding_tour_outcome'
|
||||
| 'onboarding_step4_path_clicked'
|
||||
| 'onboarding_step4_path_failed'
|
||||
| 'onboarding_task_sources_snapshot'
|
||||
| 'onboarding_windows_terminal_snapshot'
|
||||
| 'onboarding_completed'
|
||||
| 'onboarding_dismissed'
|
||||
| 'onboarding_agent_picked'
|
||||
| 'onboarding_ghostty_discovered'
|
||||
| 'onboarding_ghostty_import_clicked'
|
||||
| 'onboarding_ghostty_import_failed'
|
||||
| 'onboarding_feature_setup_toggled'
|
||||
| 'onboarding_feature_setup_run'
|
||||
| 'onboarding_feature_setup_terminal_opened'
|
||||
| 'onboarding_feature_setup_terminal_interacted'
|
||||
type _DerivedOnboardingCohortEvents = {
|
||||
[N in EventName]: 'cohort' extends _KnownPayloadKeys<EventMap[N]> ? N : never
|
||||
}[EventName]
|
||||
type _OnboardingCohortRosterSync = _OnboardingCohortRoster extends _DerivedOnboardingCohortEvents
|
||||
? _DerivedOnboardingCohortEvents extends _OnboardingCohortRoster
|
||||
? true
|
||||
: never
|
||||
: never
|
||||
const _onboardingCohortRosterSyncCheck: _OnboardingCohortRosterSync = true
|
||||
void _onboardingCohortRosterSyncCheck
|
||||
|
||||
export function isOnboardingEvent(name: EventName): boolean {
|
||||
return ONBOARDING_COHORT_SET.has(name)
|
||||
}
|
||||
|
||||
// No `env` discriminator: every transmitted event is from an official CI build (dev/contributor builds only console-mirror).
|
||||
// The per-field `.max(64)` is the validator's string-length cap — there is no separate post-parse length check.
|
||||
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),
|
||||
// `.min(1)`: an empty install_id/session_id would collapse unrelated events into one synthetic user/session, corrupting analytics.
|
||||
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>
|
||||
@@ -0,0 +1,193 @@
|
||||
import {
|
||||
agentErrorSchema,
|
||||
agentPromptSentSchema,
|
||||
agentStartedSchema,
|
||||
appOpenedSchema,
|
||||
appStarredOrcaSchema,
|
||||
featureInteractionUsageBucketReachedSchema,
|
||||
repoAddedSchema,
|
||||
starNagOutcomeEventSchema,
|
||||
workspaceCreatedSchema
|
||||
} from './telemetry-app-event-schemas'
|
||||
import {
|
||||
agentHookInstallFailedSchema,
|
||||
agentHookTransportBlockedSchema,
|
||||
agentHookUnattributedSchema,
|
||||
codexTrustGrantSchema,
|
||||
daemonAuditEligibilitySchema,
|
||||
daemonLifecycleSchema,
|
||||
daemonStartFailedSchema,
|
||||
mainThreadHangDetectedSchema,
|
||||
remoteOutboundBudgetCloseSchema,
|
||||
runtimeRpcStartFailedSchema,
|
||||
settingsChangedSchema
|
||||
} from './telemetry-daemon-event-schemas'
|
||||
import {
|
||||
contextualTourOutcomeSchema,
|
||||
contextualTourShownSchema,
|
||||
directSshReconnectOperationSchema,
|
||||
editorExternalChangeConflictActionSchema,
|
||||
editorExternalChangeConflictShownSchema,
|
||||
setupGuideClosedSchema,
|
||||
setupGuideOpenedSchema,
|
||||
setupGuideStepCompletedSchema,
|
||||
terminalPaneSplitSchema
|
||||
} from './telemetry-feature-education-event-schemas'
|
||||
import {
|
||||
cmdJPaletteFeatureTipAcknowledgedSchema,
|
||||
cmdJPaletteFeatureTipShownSchema,
|
||||
featureWallClosedSchema,
|
||||
featureWallDocsClickedSchema,
|
||||
featureWallFeatureSelectedSchema,
|
||||
featureWallGroupSelectedSchema,
|
||||
featureWallOpenedSchema,
|
||||
featureWallTileClickedSchema,
|
||||
featureWallTileFocusedSchema,
|
||||
nativeChatMessageSentSchema,
|
||||
nativeChatPickerItemAcceptedSchema,
|
||||
nativeChatPickerOpenedSchema,
|
||||
nativeChatSendClassifiedSchema,
|
||||
nativeChatSkillDiscoverySchema,
|
||||
nativeChatToggledSchema,
|
||||
orcaCliFeatureTipSetupClickedSchema,
|
||||
orcaCliFeatureTipSetupResultSchema,
|
||||
orcaCliFeatureTipShownSchema,
|
||||
telemetryOptedInSchema,
|
||||
telemetryOptedOutSchema
|
||||
} from './telemetry-native-feature-event-schemas'
|
||||
import {
|
||||
activationChecklistItemCompletedSchema,
|
||||
onboardingAgentPickedSchema,
|
||||
onboardingCompletedSchema,
|
||||
onboardingDismissedSchema,
|
||||
onboardingFeatureSetupRunSchema,
|
||||
onboardingFeatureSetupTerminalInteractedSchema,
|
||||
onboardingFeatureSetupTerminalOpenedSchema,
|
||||
onboardingFeatureSetupToggledSchema,
|
||||
onboardingGhosttyDiscoveredSchema,
|
||||
onboardingGhosttyImportClickedSchema,
|
||||
onboardingGhosttyImportFailedSchema,
|
||||
onboardingStartedSchema,
|
||||
onboardingStep4PathClickedSchema,
|
||||
onboardingStep4PathFailedSchema,
|
||||
onboardingStepCompletedSchema,
|
||||
onboardingStepSkippedSchema,
|
||||
onboardingStepViewedSchema,
|
||||
onboardingTaskSourcesSnapshotSchema,
|
||||
onboardingTourOutcomeEventSchema,
|
||||
onboardingWindowsTerminalSnapshotSchema,
|
||||
smartSortClass1PromotionSchema,
|
||||
smartSortClassDistributionSchema,
|
||||
smartToRecentSwitchSchema
|
||||
} from './telemetry-onboarding-event-schemas'
|
||||
import {
|
||||
addRepoDefaultCheckoutHandoffSchema,
|
||||
addRepoExistingWorkspacesDetectedSchema,
|
||||
addRepoNestedImportActionSchema,
|
||||
addRepoNestedImportResultSchema,
|
||||
addRepoNestedScanResultSchema,
|
||||
addRepoSetupStepActionEventSchema,
|
||||
setupScriptPromptActionSchema,
|
||||
setupScriptPromptShownSchema,
|
||||
workspaceCreateFailedSchema
|
||||
} from './telemetry-repository-event-schemas'
|
||||
|
||||
// ── Event registry: the one record the validator consumes ───────────────
|
||||
// Versioning: breaking changes (rename/re-mean/remove a key) need a new event name; in-place edits blend pre/post rows unmixably. Additive-optional fields are safe.
|
||||
export const eventSchemas = {
|
||||
app_opened: appOpenedSchema,
|
||||
app_starred_orca: appStarredOrcaSchema,
|
||||
star_nag_outcome: starNagOutcomeEventSchema,
|
||||
feature_interaction_usage_bucket_reached: featureInteractionUsageBucketReachedSchema,
|
||||
|
||||
repo_added: repoAddedSchema,
|
||||
add_repo_setup_step_action: addRepoSetupStepActionEventSchema,
|
||||
add_repo_existing_workspaces_detected: addRepoExistingWorkspacesDetectedSchema,
|
||||
add_repo_default_checkout_handoff: addRepoDefaultCheckoutHandoffSchema,
|
||||
add_repo_nested_scan_result: addRepoNestedScanResultSchema,
|
||||
add_repo_nested_import_action: addRepoNestedImportActionSchema,
|
||||
add_repo_nested_import_result: addRepoNestedImportResultSchema,
|
||||
workspace_created: workspaceCreatedSchema,
|
||||
workspace_create_failed: workspaceCreateFailedSchema,
|
||||
setup_script_prompt_shown: setupScriptPromptShownSchema,
|
||||
setup_script_prompt_action: setupScriptPromptActionSchema,
|
||||
|
||||
agent_started: agentStartedSchema,
|
||||
agent_prompt_sent: agentPromptSentSchema,
|
||||
agent_error: agentErrorSchema,
|
||||
agent_hook_install_failed: agentHookInstallFailedSchema,
|
||||
agent_hook_unattributed: agentHookUnattributedSchema,
|
||||
agent_hook_transport_blocked: agentHookTransportBlockedSchema,
|
||||
|
||||
daemon_start_failed: daemonStartFailedSchema,
|
||||
main_thread_hang_detected: mainThreadHangDetectedSchema,
|
||||
daemon_lifecycle: daemonLifecycleSchema,
|
||||
daemon_audit_eligibility: daemonAuditEligibilitySchema,
|
||||
runtime_rpc_start_failed: runtimeRpcStartFailedSchema,
|
||||
remote_outbound_budget_close: remoteOutboundBudgetCloseSchema,
|
||||
|
||||
codex_trust_grant: codexTrustGrantSchema,
|
||||
|
||||
settings_changed: settingsChangedSchema,
|
||||
|
||||
native_chat_toggled: nativeChatToggledSchema,
|
||||
native_chat_message_sent: nativeChatMessageSentSchema,
|
||||
native_chat_picker_opened: nativeChatPickerOpenedSchema,
|
||||
native_chat_picker_item_accepted: nativeChatPickerItemAcceptedSchema,
|
||||
native_chat_send_classified: nativeChatSendClassifiedSchema,
|
||||
native_chat_skill_discovery: nativeChatSkillDiscoverySchema,
|
||||
|
||||
telemetry_opted_in: telemetryOptedInSchema,
|
||||
telemetry_opted_out: telemetryOptedOutSchema,
|
||||
|
||||
orca_cli_feature_tip_shown: orcaCliFeatureTipShownSchema,
|
||||
orca_cli_feature_tip_setup_clicked: orcaCliFeatureTipSetupClickedSchema,
|
||||
orca_cli_feature_tip_setup_result: orcaCliFeatureTipSetupResultSchema,
|
||||
cmd_j_palette_feature_tip_shown: cmdJPaletteFeatureTipShownSchema,
|
||||
cmd_j_palette_feature_tip_acknowledged: cmdJPaletteFeatureTipAcknowledgedSchema,
|
||||
|
||||
feature_wall_opened: featureWallOpenedSchema,
|
||||
feature_wall_closed: featureWallClosedSchema,
|
||||
feature_wall_tile_focused: featureWallTileFocusedSchema,
|
||||
feature_wall_tile_clicked: featureWallTileClickedSchema,
|
||||
feature_wall_group_selected: featureWallGroupSelectedSchema,
|
||||
feature_wall_feature_selected: featureWallFeatureSelectedSchema,
|
||||
feature_wall_docs_clicked: featureWallDocsClickedSchema,
|
||||
|
||||
onboarding_started: onboardingStartedSchema,
|
||||
onboarding_step_viewed: onboardingStepViewedSchema,
|
||||
onboarding_step_completed: onboardingStepCompletedSchema,
|
||||
onboarding_step_skipped: onboardingStepSkippedSchema,
|
||||
onboarding_tour_outcome: onboardingTourOutcomeEventSchema,
|
||||
onboarding_step4_path_clicked: onboardingStep4PathClickedSchema,
|
||||
onboarding_step4_path_failed: onboardingStep4PathFailedSchema,
|
||||
onboarding_task_sources_snapshot: onboardingTaskSourcesSnapshotSchema,
|
||||
onboarding_windows_terminal_snapshot: onboardingWindowsTerminalSnapshotSchema,
|
||||
onboarding_completed: onboardingCompletedSchema,
|
||||
onboarding_dismissed: onboardingDismissedSchema,
|
||||
onboarding_agent_picked: onboardingAgentPickedSchema,
|
||||
onboarding_ghostty_discovered: onboardingGhosttyDiscoveredSchema,
|
||||
onboarding_ghostty_import_clicked: onboardingGhosttyImportClickedSchema,
|
||||
onboarding_ghostty_import_failed: onboardingGhosttyImportFailedSchema,
|
||||
onboarding_feature_setup_toggled: onboardingFeatureSetupToggledSchema,
|
||||
onboarding_feature_setup_run: onboardingFeatureSetupRunSchema,
|
||||
onboarding_feature_setup_terminal_opened: onboardingFeatureSetupTerminalOpenedSchema,
|
||||
onboarding_feature_setup_terminal_interacted: onboardingFeatureSetupTerminalInteractedSchema,
|
||||
activation_checklist_item_completed: activationChecklistItemCompletedSchema,
|
||||
|
||||
contextual_tour_shown: contextualTourShownSchema,
|
||||
contextual_tour_outcome: contextualTourOutcomeSchema,
|
||||
setup_guide_opened: setupGuideOpenedSchema,
|
||||
setup_guide_closed: setupGuideClosedSchema,
|
||||
setup_guide_step_completed: setupGuideStepCompletedSchema,
|
||||
terminal_pane_split: terminalPaneSplitSchema,
|
||||
|
||||
editor_external_change_conflict_shown: editorExternalChangeConflictShownSchema,
|
||||
editor_external_change_conflict_action: editorExternalChangeConflictActionSchema,
|
||||
|
||||
direct_ssh_reconnect_operation: directSshReconnectOperationSchema,
|
||||
|
||||
smart_sort_class_distribution: smartSortClassDistributionSchema,
|
||||
smart_sort_class_1_promotion: smartSortClass1PromotionSchema,
|
||||
smart_to_recent_switch: smartToRecentSwitchSchema
|
||||
} as const
|
||||
+64
-1653
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,177 @@
|
||||
import { z } from 'zod'
|
||||
import {
|
||||
CONTEXTUAL_TOUR_OUTCOMES,
|
||||
FEATURE_EDUCATION_CONTEXTUAL_TOUR_IDS,
|
||||
FEATURE_EDUCATION_SOURCES,
|
||||
SETUP_GUIDE_CLOSE_OUTCOMES,
|
||||
SETUP_GUIDE_SOURCES,
|
||||
TERMINAL_PANE_SPLIT_SOURCES
|
||||
} from './feature-education-telemetry'
|
||||
import { FEATURE_WALL_SETUP_STEP_IDS } from './feature-wall-setup-steps'
|
||||
|
||||
export const featureEducationSourceSchema = z.enum(FEATURE_EDUCATION_SOURCES)
|
||||
export const featureEducationContextualTourIdSchema = z.enum(FEATURE_EDUCATION_CONTEXTUAL_TOUR_IDS)
|
||||
export const setupGuideSourceSchema = z.enum(SETUP_GUIDE_SOURCES)
|
||||
export const setupGuideCloseOutcomeSchema = z.enum(SETUP_GUIDE_CLOSE_OUTCOMES)
|
||||
export const setupGuideStepIdSchema = z.enum(FEATURE_WALL_SETUP_STEP_IDS)
|
||||
export const setupGuideStepIdOrNoneSchema = z.enum([
|
||||
...FEATURE_WALL_SETUP_STEP_IDS,
|
||||
'none'
|
||||
] as const)
|
||||
export const terminalPaneSplitSourceSchema = z.enum(TERMINAL_PANE_SPLIT_SOURCES)
|
||||
|
||||
export const contextualTourShownSchema = z
|
||||
.object({
|
||||
tour_id: featureEducationContextualTourIdSchema,
|
||||
source: featureEducationSourceSchema,
|
||||
was_feature_previously_interacted: z.boolean()
|
||||
})
|
||||
.strict()
|
||||
|
||||
export const contextualTourOutcomeSchema = z
|
||||
.object({
|
||||
tour_id: featureEducationContextualTourIdSchema,
|
||||
source: featureEducationSourceSchema,
|
||||
outcome: z.enum(CONTEXTUAL_TOUR_OUTCOMES),
|
||||
steps_seen: z.number().int().min(0).max(8),
|
||||
total_steps: z.number().int().min(1).max(8),
|
||||
furthest_step_index: z.number().int().min(1).max(8).optional(),
|
||||
defined_step_count: z.number().int().min(1).max(8).optional()
|
||||
})
|
||||
.refine((payload) => payload.steps_seen <= payload.total_steps, {
|
||||
message: 'steps_seen must be less than or equal to total_steps',
|
||||
path: ['steps_seen']
|
||||
})
|
||||
.refine(
|
||||
(payload) =>
|
||||
payload.furthest_step_index === undefined ||
|
||||
payload.defined_step_count === undefined ||
|
||||
payload.furthest_step_index <= payload.defined_step_count,
|
||||
{
|
||||
message: 'furthest_step_index must be less than or equal to defined_step_count',
|
||||
path: ['furthest_step_index']
|
||||
}
|
||||
)
|
||||
.refine(
|
||||
(payload) =>
|
||||
(payload.furthest_step_index === undefined) === (payload.defined_step_count === undefined),
|
||||
{
|
||||
message: 'furthest_step_index and defined_step_count must be sent together',
|
||||
path: ['defined_step_count']
|
||||
}
|
||||
)
|
||||
.strict()
|
||||
|
||||
export const setupGuideOpenedSchema = z
|
||||
.object({
|
||||
source: setupGuideSourceSchema,
|
||||
initial_completed_count: z.number().int().min(0).max(8),
|
||||
total_steps: z.literal(8),
|
||||
first_incomplete_step_id: setupGuideStepIdOrNoneSchema
|
||||
})
|
||||
.strict()
|
||||
|
||||
export const setupGuideClosedSchema = z
|
||||
.object({
|
||||
source: setupGuideSourceSchema,
|
||||
outcome: setupGuideCloseOutcomeSchema,
|
||||
initial_completed_count: z.number().int().min(0).max(8),
|
||||
final_completed_count: z.number().int().min(0).max(8),
|
||||
total_steps: z.literal(8),
|
||||
active_step_id: setupGuideStepIdOrNoneSchema
|
||||
})
|
||||
.refine((payload) => payload.final_completed_count >= payload.initial_completed_count, {
|
||||
message: 'final_completed_count must be greater than or equal to initial_completed_count',
|
||||
path: ['final_completed_count']
|
||||
})
|
||||
.strict()
|
||||
|
||||
export const setupGuideStepCompletedSchema = z
|
||||
.object({
|
||||
step_id: setupGuideStepIdSchema,
|
||||
section_id: z.enum(['parallel-work', 'setup']),
|
||||
completed_count: z.number().int().min(1).max(8),
|
||||
total_steps: z.literal(8),
|
||||
setup_guide_visible: z.boolean()
|
||||
})
|
||||
.strict()
|
||||
|
||||
export const terminalPaneSplitSchema = z
|
||||
.object({
|
||||
source: terminalPaneSplitSourceSchema,
|
||||
direction: z.enum(['vertical', 'horizontal'])
|
||||
})
|
||||
.strict()
|
||||
|
||||
// Why: measures the changed-on-disk conflict flow (issue #7265) per transport; deliberately path-free.
|
||||
export const editorExternalChangeConflictShownSchema = z
|
||||
.object({
|
||||
surface: z.enum(['edit', 'unstaged-diff']),
|
||||
transport: z.enum(['local', 'ssh', 'runtime']),
|
||||
origin: z.enum(['live', 'restore'])
|
||||
})
|
||||
.strict()
|
||||
|
||||
export const editorExternalChangeConflictActionSchema = z
|
||||
.object({
|
||||
action: z.enum(['reload', 'keep', 'compare', 'undo_reload', 'save_overwrite']),
|
||||
surface: z.enum(['edit', 'unstaged-diff']),
|
||||
transport: z.enum(['local', 'ssh', 'runtime'])
|
||||
})
|
||||
.strict()
|
||||
|
||||
export const directSshReconnectCountSchema = z.number().int().min(0).max(1_000_000)
|
||||
export const directSshReconnectDurationSchema = z.number().int().min(0).max(86_400_000)
|
||||
export const directSshReconnectOperationSchema = z
|
||||
.object({
|
||||
mode: z.enum(['reconnect', 'prepare_only']),
|
||||
reason: z.enum(['reconnect', 'initial_hydration', 'workspace_snapshot', 'wake_refresh']),
|
||||
outcome: z.enum(['complete', 'degraded', 'canceled', 'stale', 'stopped', 'stabilizing']),
|
||||
terminal_retried_count: directSshReconnectCountSchema,
|
||||
terminal_stale_binding_cleared_count: directSshReconnectCountSchema,
|
||||
terminal_correction_succeeded_count: directSshReconnectCountSchema,
|
||||
catalog_complete_count: directSshReconnectCountSchema,
|
||||
catalog_degraded_count: directSshReconnectCountSchema,
|
||||
catalog_stale_count: directSshReconnectCountSchema,
|
||||
repo_complete_count: directSshReconnectCountSchema,
|
||||
repo_non_authoritative_count: directSshReconnectCountSchema,
|
||||
repo_retrying_count: directSshReconnectCountSchema,
|
||||
repo_timed_out_count: directSshReconnectCountSchema,
|
||||
repo_cancel_budget_exhausted_count: directSshReconnectCountSchema,
|
||||
repo_canceled_count: directSshReconnectCountSchema,
|
||||
repo_stale_count: directSshReconnectCountSchema,
|
||||
repo_rejected_count: directSshReconnectCountSchema,
|
||||
lineage_complete_count: directSshReconnectCountSchema,
|
||||
lineage_degraded_count: directSshReconnectCountSchema,
|
||||
lineage_canceled_count: directSshReconnectCountSchema,
|
||||
lineage_stale_count: directSshReconnectCountSchema,
|
||||
lineage_not_started_count: directSshReconnectCountSchema,
|
||||
git_worktree_count: directSshReconnectCountSchema,
|
||||
folder_workspace_count: directSshReconnectCountSchema,
|
||||
ambiguous_owner_count: directSshReconnectCountSchema,
|
||||
contradictory_owner_count: directSshReconnectCountSchema,
|
||||
total_duration_ms: directSshReconnectDurationSchema,
|
||||
terminal_finalization_duration_ms: directSshReconnectDurationSchema,
|
||||
catalog_duration_ms: directSshReconnectDurationSchema,
|
||||
queue_wait_sample_count: directSshReconnectCountSchema,
|
||||
queue_wait_duration_ms_p50: directSshReconnectDurationSchema,
|
||||
queue_wait_duration_ms_p95: directSshReconnectDurationSchema,
|
||||
queue_wait_duration_ms_p99: directSshReconnectDurationSchema,
|
||||
queue_wait_duration_ms_max: directSshReconnectDurationSchema,
|
||||
provider_execution_sample_count: directSshReconnectCountSchema,
|
||||
provider_execution_duration_ms_p50: directSshReconnectDurationSchema,
|
||||
provider_execution_duration_ms_p95: directSshReconnectDurationSchema,
|
||||
provider_execution_duration_ms_p99: directSshReconnectDurationSchema,
|
||||
provider_execution_duration_ms_max: directSshReconnectDurationSchema,
|
||||
timeout_retry_count: directSshReconnectCountSchema,
|
||||
locally_settled_waiter_count: directSshReconnectCountSchema,
|
||||
cancel_debt_count: directSshReconnectCountSchema,
|
||||
replacement_admission_delayed_count: directSshReconnectCountSchema,
|
||||
overlapping_join_count: directSshReconnectCountSchema,
|
||||
coordinator_owned_direct_ssh_detected_worktree_concurrency_peak:
|
||||
directSshReconnectCountSchema.max(5),
|
||||
estimated_late_work_allowance_count: directSshReconnectCountSchema.max(2),
|
||||
authority_rotation_count: directSshReconnectCountSchema,
|
||||
damped_preparation_count: directSshReconnectCountSchema
|
||||
})
|
||||
.strict()
|
||||
@@ -0,0 +1,133 @@
|
||||
import { z } from 'zod'
|
||||
import { FEATURE_WALL_MAX_DWELL_MS } from './feature-wall-telemetry'
|
||||
import { nthRepoAddedSchema } from './telemetry-app-event-schemas'
|
||||
import {
|
||||
agentKindSchema,
|
||||
featureWallExitActionSchema,
|
||||
featureWallOpenSourceSchema,
|
||||
featureWallTileIdSchema,
|
||||
featureWallTourDepthStepSchema,
|
||||
featureWallWorkflowIdSchema,
|
||||
optInViaSchema
|
||||
} from './telemetry-property-schemas'
|
||||
|
||||
// Native chat (terminal⇄chat toggle) adoption; view-mode enum mirrors `Tab.viewMode` in shared/types.ts.
|
||||
export const nativeChatViewModeSchema = z.enum(['terminal', 'chat'])
|
||||
export const nativeChatToggledSchema = z
|
||||
.object({
|
||||
from_mode: nativeChatViewModeSchema,
|
||||
to_mode: nativeChatViewModeSchema,
|
||||
agent_kind: agentKindSchema
|
||||
})
|
||||
.strict()
|
||||
// `runtime`: local vs SSH/remote agent PTY; `'unknown'` when unresolved at send time.
|
||||
export const nativeChatRuntimeSchema = z.enum(['local', 'remote', 'unknown'])
|
||||
export type NativeChatRuntime = z.infer<typeof nativeChatRuntimeSchema>
|
||||
export const nativeChatMessageSentSchema = z
|
||||
.object({
|
||||
agent_kind: agentKindSchema,
|
||||
runtime: nativeChatRuntimeSchema
|
||||
})
|
||||
.strict()
|
||||
export const nativeChatPickerOpenedSchema = z
|
||||
.object({ agent_kind: agentKindSchema, prefix: z.enum(['slash', 'dollar']) })
|
||||
.strict()
|
||||
export const nativeChatPickerItemAcceptedSchema = z
|
||||
.object({ agent_kind: agentKindSchema, item_kind: z.enum(['command', 'skill']) })
|
||||
.strict()
|
||||
export const nativeChatSendClassifiedSchema = z
|
||||
.object({ agent_kind: agentKindSchema, outcome: z.enum(['chat', 'command', 'unknown-token']) })
|
||||
.strict()
|
||||
export const nativeChatSkillDiscoverySchema = z
|
||||
.object({
|
||||
agent_kind: agentKindSchema,
|
||||
outcome: z.enum(['ready', 'error', 'timeout', 'unavailable']),
|
||||
execution_host_kind: z.enum(['local', 'runtime', 'ssh'])
|
||||
})
|
||||
.strict()
|
||||
|
||||
export const telemetryOptedInSchema = z.object({ via: optInViaSchema }).strict()
|
||||
export const telemetryOptedOutSchema = z.object({ via: optInViaSchema }).strict()
|
||||
|
||||
export const orcaCliFeatureTipSourceSchema = z.enum(['app_open', 'manual'])
|
||||
export const orcaCliFeatureTipShownSchema = z
|
||||
.object({
|
||||
source: orcaCliFeatureTipSourceSchema,
|
||||
nth_repo_added: nthRepoAddedSchema
|
||||
})
|
||||
.strict()
|
||||
export const orcaCliFeatureTipSetupClickedSchema = z
|
||||
.object({
|
||||
source: orcaCliFeatureTipSourceSchema,
|
||||
nth_repo_added: nthRepoAddedSchema
|
||||
})
|
||||
.strict()
|
||||
export const orcaCliFeatureTipSetupResultSchema = z
|
||||
.object({
|
||||
source: orcaCliFeatureTipSourceSchema,
|
||||
result: z.enum(['installed', 'needs_attention', 'dev_preview', 'failed']),
|
||||
nth_repo_added: nthRepoAddedSchema
|
||||
})
|
||||
.strict()
|
||||
|
||||
export const cmdJPaletteFeatureTipShownSchema = z
|
||||
.object({
|
||||
source: orcaCliFeatureTipSourceSchema,
|
||||
nth_repo_added: nthRepoAddedSchema
|
||||
})
|
||||
.strict()
|
||||
export const cmdJPaletteFeatureTipAcknowledgedSchema = z
|
||||
.object({
|
||||
source: orcaCliFeatureTipSourceSchema,
|
||||
nth_repo_added: nthRepoAddedSchema
|
||||
})
|
||||
.strict()
|
||||
|
||||
export const featureWallOpenedSchema = z
|
||||
.object({
|
||||
source: featureWallOpenSourceSchema
|
||||
})
|
||||
.strict()
|
||||
export const featureWallClosedSchema = z
|
||||
.object({
|
||||
dwell_ms: z.number().int().min(0).max(FEATURE_WALL_MAX_DWELL_MS),
|
||||
source: featureWallOpenSourceSchema.optional(),
|
||||
exit_action: featureWallExitActionSchema.optional(),
|
||||
furthest_step: featureWallTourDepthStepSchema.optional(),
|
||||
last_group_id: featureWallWorkflowIdSchema.optional(),
|
||||
visited_workflow_count: z.number().int().min(0).max(5).optional(),
|
||||
visited_substep_count: z.number().int().min(0).max(9).optional(),
|
||||
completed_workflow_count: z.number().int().min(0).max(5).optional(),
|
||||
completed_substep_count: z.number().int().min(0).max(9).optional()
|
||||
})
|
||||
.strict()
|
||||
export const featureWallTileFocusedSchema = z
|
||||
.object({
|
||||
tile_id: featureWallTileIdSchema
|
||||
})
|
||||
.strict()
|
||||
export const featureWallTileClickedSchema = z
|
||||
.object({
|
||||
tile_id: featureWallTileIdSchema
|
||||
})
|
||||
.strict()
|
||||
export const featureWallGroupSelectedSchema = z
|
||||
.object({
|
||||
group_id: featureWallWorkflowIdSchema,
|
||||
source: featureWallOpenSourceSchema
|
||||
})
|
||||
.strict()
|
||||
export const featureWallFeatureSelectedSchema = z
|
||||
.object({
|
||||
group_id: featureWallWorkflowIdSchema,
|
||||
tile_id: featureWallTileIdSchema,
|
||||
source: featureWallOpenSourceSchema
|
||||
})
|
||||
.strict()
|
||||
export const featureWallDocsClickedSchema = z
|
||||
.object({
|
||||
group_id: featureWallWorkflowIdSchema,
|
||||
tile_id: featureWallTileIdSchema,
|
||||
source: featureWallOpenSourceSchema
|
||||
})
|
||||
.strict()
|
||||
@@ -0,0 +1,296 @@
|
||||
import { z } from 'zod'
|
||||
import { FEATURE_WALL_MAX_DWELL_MS } from './feature-wall-telemetry'
|
||||
import type { DiscoveryStatusEmitted } from './onboarding-state-types'
|
||||
import type { PathSource, ShellHydrationFailureReason } from './shell-path-hydration-types'
|
||||
import { agentKindSchema, featureWallTourDepthStepSchema } from './telemetry-property-schemas'
|
||||
import {
|
||||
cohortSchema,
|
||||
hasMatchingOnboardingFeatureSetupSelectedCount,
|
||||
onboardingChecklistItemSchema,
|
||||
onboardingFailureReasonSchema,
|
||||
onboardingFeatureSetupFeatureSchema,
|
||||
onboardingFeatureSetupSelectedCountRefinement,
|
||||
onboardingFeatureSetupSelectionSchema,
|
||||
onboardingPathSchema,
|
||||
onboardingStepSchema,
|
||||
onboardingTaskSourcesExitActionSchema,
|
||||
onboardingTaskSourcesGithubStatusSchema,
|
||||
onboardingTaskSourcesLinearStatusSchema,
|
||||
onboardingTourOutcomeSchema,
|
||||
onboardingValueKindSchema,
|
||||
onboardingWindowsTerminalExitActionSchema,
|
||||
onboardingWindowsTerminalRightClickSchema,
|
||||
onboardingWindowsTerminalShellSchema
|
||||
} from './telemetry-onboarding-foundation-schemas'
|
||||
|
||||
// Uniform button/keyboard shape lets keyboard skip/dismiss paths arrive without a schema migration.
|
||||
export const advancedViaSchema = z.enum(['button', 'keyboard']).optional()
|
||||
|
||||
export const onboardingStartedSchema = z
|
||||
.object({ resumed_from_step: onboardingStepSchema.optional(), cohort: cohortSchema })
|
||||
.strict()
|
||||
export const onboardingStepViewedSchema = z
|
||||
.object({
|
||||
step: onboardingStepSchema,
|
||||
value_kind: onboardingValueKindSchema,
|
||||
cohort: cohortSchema
|
||||
})
|
||||
.strict()
|
||||
export const onboardingStepCompletedSchema = z
|
||||
.object({
|
||||
step: onboardingStepSchema,
|
||||
value_kind: onboardingValueKindSchema,
|
||||
duration_ms: z.number().int().nonnegative().optional(),
|
||||
advanced_via: advancedViaSchema,
|
||||
cohort: cohortSchema
|
||||
})
|
||||
.strict()
|
||||
export const onboardingStepSkippedSchema = z
|
||||
.object({
|
||||
step: onboardingStepSchema,
|
||||
value_kind: onboardingValueKindSchema,
|
||||
duration_ms: z.number().int().nonnegative().optional(),
|
||||
advanced_via: advancedViaSchema,
|
||||
cohort: cohortSchema
|
||||
})
|
||||
.strict()
|
||||
export type OnboardingTourOutcomeTelemetry = {
|
||||
outcome: z.infer<typeof onboardingTourOutcomeSchema>
|
||||
tour_dwell_ms?: number
|
||||
furthest_step?: z.infer<typeof featureWallTourDepthStepSchema>
|
||||
visited_workflow_count?: number
|
||||
visited_substep_count?: number
|
||||
completed_workflow_count?: number
|
||||
completed_substep_count?: number
|
||||
}
|
||||
|
||||
export function validateOnboardingTourOutcome(
|
||||
props: OnboardingTourOutcomeTelemetry,
|
||||
ctx: z.RefinementCtx
|
||||
): void {
|
||||
if (props.outcome !== 'skipped_intro') {
|
||||
return
|
||||
}
|
||||
for (const key of [
|
||||
'tour_dwell_ms',
|
||||
'furthest_step',
|
||||
'visited_workflow_count',
|
||||
'visited_substep_count',
|
||||
'completed_workflow_count',
|
||||
'completed_substep_count'
|
||||
] as const) {
|
||||
if (props[key] !== undefined) {
|
||||
ctx.addIssue({
|
||||
code: 'custom',
|
||||
path: [key],
|
||||
message: `${key} is only valid after the inline tour starts`
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export const onboardingTourOutcomeEventSchema = z
|
||||
.object({
|
||||
outcome: onboardingTourOutcomeSchema,
|
||||
intro_duration_ms: z.number().int().min(0).max(FEATURE_WALL_MAX_DWELL_MS).optional(),
|
||||
tour_dwell_ms: z.number().int().min(0).max(FEATURE_WALL_MAX_DWELL_MS).optional(),
|
||||
furthest_step: featureWallTourDepthStepSchema.optional(),
|
||||
visited_workflow_count: z.number().int().min(0).max(5).optional(),
|
||||
visited_substep_count: z.number().int().min(0).max(9).optional(),
|
||||
completed_workflow_count: z.number().int().min(0).max(5).optional(),
|
||||
completed_substep_count: z.number().int().min(0).max(9).optional(),
|
||||
advanced_via: advancedViaSchema,
|
||||
cohort: cohortSchema
|
||||
})
|
||||
.strict()
|
||||
.superRefine(validateOnboardingTourOutcome)
|
||||
export const onboardingStep4PathClickedSchema = z
|
||||
.object({ path: onboardingPathSchema, cohort: cohortSchema })
|
||||
.strict()
|
||||
export const onboardingStep4PathFailedSchema = z
|
||||
.object({
|
||||
path: onboardingPathSchema,
|
||||
reason: onboardingFailureReasonSchema,
|
||||
cohort: cohortSchema
|
||||
})
|
||||
.strict()
|
||||
export const onboardingTaskSourcesSnapshotSchema = z
|
||||
.object({
|
||||
github_status: onboardingTaskSourcesGithubStatusSchema,
|
||||
linear_status: onboardingTaskSourcesLinearStatusSchema,
|
||||
exit_action: onboardingTaskSourcesExitActionSchema,
|
||||
duration_ms: z.number().int().nonnegative().optional(),
|
||||
advanced_via: advancedViaSchema,
|
||||
cohort: cohortSchema
|
||||
})
|
||||
.strict()
|
||||
export const onboardingWindowsTerminalSnapshotSchema = z
|
||||
.object({
|
||||
default_shell: onboardingWindowsTerminalShellSchema,
|
||||
right_click_behavior: onboardingWindowsTerminalRightClickSchema,
|
||||
exit_action: onboardingWindowsTerminalExitActionSchema,
|
||||
duration_ms: z.number().int().nonnegative().optional(),
|
||||
advanced_via: advancedViaSchema,
|
||||
cohort: cohortSchema
|
||||
})
|
||||
.strict()
|
||||
// Why: no `is_git_repo` here; the signal moved to `repo_added.is_git_repo`.
|
||||
export const onboardingCompletedSchema = z
|
||||
.object({
|
||||
path: onboardingPathSchema,
|
||||
total_duration_ms: z.number().int().nonnegative(),
|
||||
cohort: cohortSchema
|
||||
})
|
||||
.strict()
|
||||
export const onboardingDismissedSchema = z
|
||||
.object({
|
||||
last_step: onboardingStepSchema,
|
||||
duration_ms: z.number().int().nonnegative().optional(),
|
||||
advanced_via: advancedViaSchema,
|
||||
cohort: cohortSchema
|
||||
})
|
||||
.strict()
|
||||
export const activationChecklistItemCompletedSchema = z
|
||||
.object({
|
||||
item: onboardingChecklistItemSchema,
|
||||
time_since_completed_ms: z.number().int().nonnegative()
|
||||
})
|
||||
.strict()
|
||||
|
||||
// Why: disambiguates `on_path:false` rows on dashboard 1562016 (shell-hydration failure vs genuinely-not-on-PATH). See docs/agent-on-path-detection.md.
|
||||
export const pathSourceSchema = z.enum(['shell_hydrate', 'sync_seed_only'])
|
||||
export const pathFailureReasonSchema = z.enum([
|
||||
'none',
|
||||
'no_shell',
|
||||
'timeout',
|
||||
'spawn_error',
|
||||
'empty_path'
|
||||
])
|
||||
|
||||
// Compile-time guard: schema enum must match `ShellHydrationFailureReason`; drift breaks the build, not runtime parsing.
|
||||
export type _PathFailureReasonSync =
|
||||
z.infer<typeof pathFailureReasonSchema> extends ShellHydrationFailureReason
|
||||
? ShellHydrationFailureReason extends z.infer<typeof pathFailureReasonSchema>
|
||||
? true
|
||||
: never
|
||||
: never
|
||||
export const _pathFailureReasonSyncCheck: _PathFailureReasonSync = true
|
||||
void _pathFailureReasonSyncCheck
|
||||
|
||||
export type _PathSourceSync =
|
||||
z.infer<typeof pathSourceSchema> extends PathSource
|
||||
? PathSource extends z.infer<typeof pathSourceSchema>
|
||||
? true
|
||||
: never
|
||||
: never
|
||||
export const _pathSourceSyncCheck: _PathSourceSync = true
|
||||
void _pathSourceSyncCheck
|
||||
|
||||
// Fired at click time (captures mind-changes); `agent_kind` uses `tuiAgentToAgentKind` to keep the wire enum closed.
|
||||
export const onboardingAgentPickedSchema = z
|
||||
.object({
|
||||
agent_kind: agentKindSchema,
|
||||
on_path: z.boolean(),
|
||||
detected_count: z.number().int().nonnegative(),
|
||||
// `'pending'` when detection is still running at click time (picked-before-detection vs picked-the-only-agent).
|
||||
detection_state: z.enum(['complete', 'pending']),
|
||||
// `true` when the agent lived under the "Show N more" disclosure — signals demand for less-popular agents.
|
||||
from_collapsed_section: z.boolean(),
|
||||
// Why: `.optional()` is load-bearing so pre-deploy events validate under `.strict()`. See docs/agent-on-path-detection.md.
|
||||
path_source: pathSourceSchema.optional(),
|
||||
path_failure_reason: pathFailureReasonSchema.optional(),
|
||||
cohort: cohortSchema
|
||||
})
|
||||
.strict()
|
||||
|
||||
// Mirrors ThemeStep.tsx DiscoveryState; `failed` is intentionally absent (it's an import outcome, see onboarding_ghostty_import_failed).
|
||||
export const ghosttyDiscoveryStateSchema = z.enum(['found', 'absent', 'imported'])
|
||||
|
||||
// Compile-time guard: schema enum must stay in sync with the renderer's DiscoveryState; drift breaks the build, not runtime.
|
||||
export type _GhosttyDiscoveryStateSync =
|
||||
z.infer<typeof ghosttyDiscoveryStateSchema> extends DiscoveryStatusEmitted
|
||||
? DiscoveryStatusEmitted extends z.infer<typeof ghosttyDiscoveryStateSchema>
|
||||
? true
|
||||
: never
|
||||
: never
|
||||
export const _ghosttyDiscoveryStateSyncCheck: _GhosttyDiscoveryStateSync = true
|
||||
void _ghosttyDiscoveryStateSyncCheck
|
||||
|
||||
export const onboardingGhosttyDiscoveredSchema = z
|
||||
.object({
|
||||
state: ghosttyDiscoveryStateSchema,
|
||||
// Bucketed not raw: exact group counts fingerprint heavy customizers.
|
||||
field_group_count_bucket: z.enum(['0', '1-3', '4-7', '8+']),
|
||||
cohort: cohortSchema
|
||||
})
|
||||
.strict()
|
||||
export const onboardingGhosttyImportClickedSchema = z.object({ cohort: cohortSchema }).strict()
|
||||
|
||||
// Smart-sort telemetry: measures whether the redesign concentrates users in Class 1-3, and flags Smart→Recent abandonment as a regression.
|
||||
export const smartSortClassDistributionSchema = z
|
||||
.object({
|
||||
class_1: z.number().int().nonnegative(),
|
||||
class_2: z.number().int().nonnegative(),
|
||||
class_3: z.number().int().nonnegative(),
|
||||
class_4: z.number().int().nonnegative(),
|
||||
total_worktrees: z.number().int().nonnegative()
|
||||
})
|
||||
.strict()
|
||||
export const smartSortClass1PromotionSchema = z
|
||||
.object({
|
||||
cause: z.enum(['blocked', 'waiting', 'title-heuristic'])
|
||||
})
|
||||
.strict()
|
||||
// Why `_v` not `z.object({})`: empty zod object infers as TS `{}` ("anything"), breaking the `keyof EventMap[N]` roster probes.
|
||||
export const smartToRecentSwitchSchema = z.object({ _v: z.literal(1).optional() }).strict()
|
||||
export const onboardingGhosttyImportFailedSchema = z
|
||||
.object({
|
||||
// `'no_config'` is reserved for future use; call sites currently emit `'empty_diff'` or `'unknown'`.
|
||||
reason: z.enum(['no_config', 'empty_diff', 'unknown']),
|
||||
cohort: cohortSchema
|
||||
})
|
||||
.strict()
|
||||
export const onboardingFeatureSetupToggledSchema = z
|
||||
.object({
|
||||
feature: onboardingFeatureSetupFeatureSchema,
|
||||
selected: z.boolean(),
|
||||
cohort: cohortSchema
|
||||
})
|
||||
.strict()
|
||||
export const onboardingFeatureSetupRunSchema = z
|
||||
.object({
|
||||
...onboardingFeatureSetupSelectionSchema,
|
||||
cli_touched: z.boolean(),
|
||||
skill_commands_copied: z.boolean(),
|
||||
skill_install_command_prepared: z.boolean(),
|
||||
computer_use_permissions_opened: z.boolean(),
|
||||
warning_count: z.number().int().nonnegative(),
|
||||
cohort: cohortSchema
|
||||
})
|
||||
// Why: validate derived selected_count at the untrusted IPC boundary rather than trust renderer callers.
|
||||
.refine(
|
||||
hasMatchingOnboardingFeatureSetupSelectedCount,
|
||||
onboardingFeatureSetupSelectedCountRefinement
|
||||
)
|
||||
.strict()
|
||||
export const onboardingFeatureSetupTerminalOpenedSchema = z
|
||||
.object({
|
||||
...onboardingFeatureSetupSelectionSchema,
|
||||
cohort: cohortSchema
|
||||
})
|
||||
.refine(
|
||||
hasMatchingOnboardingFeatureSetupSelectedCount,
|
||||
onboardingFeatureSetupSelectedCountRefinement
|
||||
)
|
||||
.strict()
|
||||
export const onboardingFeatureSetupTerminalInteractedSchema = z
|
||||
.object({
|
||||
...onboardingFeatureSetupSelectionSchema,
|
||||
method: z.enum(['keyboard', 'pointer']),
|
||||
cohort: cohortSchema
|
||||
})
|
||||
.refine(
|
||||
hasMatchingOnboardingFeatureSetupSelectedCount,
|
||||
onboardingFeatureSetupSelectedCountRefinement
|
||||
)
|
||||
.strict()
|
||||
@@ -0,0 +1,125 @@
|
||||
import { z } from 'zod'
|
||||
import type { OnboardingChecklistState } from './onboarding-state-types'
|
||||
|
||||
// ── Onboarding ──────────────────────────────────────────────────────────
|
||||
// Closed enums only — no raw paths/repo names/URLs/error strings (measures activation, not repo debugging).
|
||||
// Why: event names still carry legacy seven-step payloads; keep validation backward-compatible for old rows.
|
||||
export const ONBOARDING_TELEMETRY_LEGACY_MAX_STEP = 7
|
||||
export const onboardingStepSchema = z
|
||||
.number()
|
||||
.int()
|
||||
.min(1)
|
||||
.max(ONBOARDING_TELEMETRY_LEGACY_MAX_STEP)
|
||||
export const onboardingPathSchema = z.enum(['open_folder', 'clone_url', 'add_project_modal'])
|
||||
export const onboardingFailureReasonSchema = z.enum([
|
||||
'invalid_path',
|
||||
'clone_failed',
|
||||
'cancelled',
|
||||
'unknown'
|
||||
])
|
||||
export const onboardingValueKindSchema = z.enum([
|
||||
'agent',
|
||||
'theme',
|
||||
'notifications',
|
||||
'agent_setup',
|
||||
'integrations',
|
||||
'windows_terminal',
|
||||
'tour',
|
||||
'repo'
|
||||
])
|
||||
export const onboardingTourOutcomeSchema = z.enum([
|
||||
'skipped_intro',
|
||||
'started_partial',
|
||||
'completed_inline'
|
||||
])
|
||||
export const onboardingTaskSourcesGithubStatusSchema = z.enum([
|
||||
'connected',
|
||||
'not_authenticated',
|
||||
'not_installed',
|
||||
'checking',
|
||||
'unknown'
|
||||
])
|
||||
export const onboardingTaskSourcesLinearStatusSchema = z.enum([
|
||||
'connected',
|
||||
'not_connected',
|
||||
'checking',
|
||||
'unknown'
|
||||
])
|
||||
export const onboardingTaskSourcesExitActionSchema = z.enum(['continue', 'skip_to_project_setup'])
|
||||
export const onboardingWindowsTerminalShellSchema = z.enum([
|
||||
'powershell',
|
||||
'command_prompt',
|
||||
'git_bash',
|
||||
'wsl',
|
||||
'other'
|
||||
])
|
||||
export const onboardingWindowsTerminalRightClickSchema = z.enum(['paste', 'menu'])
|
||||
export const onboardingWindowsTerminalExitActionSchema = z.enum([
|
||||
'continue',
|
||||
'skip_to_project_setup'
|
||||
])
|
||||
// `dismissed` is intentionally excluded — it's a UI panel-visibility flag, not an activation event.
|
||||
export const onboardingChecklistItemSchema = z.enum([
|
||||
'addedRepo',
|
||||
'addedFolder',
|
||||
'choseAgent',
|
||||
'ranFirstAgent',
|
||||
'ranSecondAgentOnSameTask',
|
||||
'triedCmdJ',
|
||||
'shapedSidebar',
|
||||
'reviewedDiff',
|
||||
'openedPr',
|
||||
'openedFile',
|
||||
'ranAgentOnFile'
|
||||
])
|
||||
export const onboardingFeatureSetupFeatureSchema = z.enum([
|
||||
'browser_use',
|
||||
'computer_use',
|
||||
'orchestration',
|
||||
'linear_tickets'
|
||||
])
|
||||
export const onboardingFeatureSetupSelectionSchema = {
|
||||
browser_use: z.boolean(),
|
||||
computer_use: z.boolean(),
|
||||
linear_tickets: z.boolean(),
|
||||
orchestration: z.boolean(),
|
||||
selected_count: z.number().int().min(0).max(3)
|
||||
} as const
|
||||
export type OnboardingFeatureSetupSelectionTelemetry = {
|
||||
browser_use: boolean
|
||||
computer_use: boolean
|
||||
linear_tickets: boolean
|
||||
orchestration: boolean
|
||||
selected_count: number
|
||||
}
|
||||
export const onboardingFeatureSetupSelectedCountRefinement = {
|
||||
path: ['selected_count'],
|
||||
message: 'selected_count must match selected feature flags'
|
||||
}
|
||||
|
||||
export function hasMatchingOnboardingFeatureSetupSelectedCount(
|
||||
props: OnboardingFeatureSetupSelectionTelemetry
|
||||
): boolean {
|
||||
// Why: Linear ticket setup is a recommended add-on and excluded from progress metrics.
|
||||
const selectedCount =
|
||||
(props.browser_use ? 1 : 0) + (props.computer_use ? 1 : 0) + (props.orchestration ? 1 : 0)
|
||||
return props.selected_count === selectedCount
|
||||
}
|
||||
|
||||
// Compile-time guard: enum must match OnboardingChecklistState activation keys (minus UI-only `dismissed`); drift breaks the build.
|
||||
export type _OnboardingChecklistItemSync =
|
||||
z.infer<typeof onboardingChecklistItemSchema> extends Exclude<
|
||||
keyof OnboardingChecklistState,
|
||||
'dismissed'
|
||||
>
|
||||
? Exclude<keyof OnboardingChecklistState, 'dismissed'> extends z.infer<
|
||||
typeof onboardingChecklistItemSchema
|
||||
>
|
||||
? true
|
||||
: never
|
||||
: never
|
||||
export const _onboardingChecklistItemSyncCheck: _OnboardingChecklistItemSync = true
|
||||
void _onboardingChecklistItemSyncCheck
|
||||
|
||||
// Cohort discriminator for onboarding events; `.optional()` is load-bearing so `.strict()` accepts the `undefined` fallback.
|
||||
export const cohortSchema = z.enum(['fresh_install', 'upgrade_backfill']).optional()
|
||||
@@ -0,0 +1,203 @@
|
||||
import { z } from 'zod'
|
||||
import { FEATURE_WALL_EXIT_ACTIONS, FEATURE_WALL_TOUR_DEPTH_STEPS } from './feature-wall-tour-depth'
|
||||
import { SETUP_SCRIPT_IMPORT_PROVIDERS } from './setup-script-import-providers'
|
||||
import { WORKSPACE_SOURCE_VALUES, type WorkspaceSource } from './workspace-source'
|
||||
import type { GlobalSettings } from './global-settings-types'
|
||||
|
||||
// ── Shared property enums ───────────────────────────────────────────────
|
||||
|
||||
// Mirrors `TuiAgent` launch surface; `claude`↔`claude-code` (product, not CLI string). `other` is the escape hatch; see `tuiAgentToAgentKind`.
|
||||
export const AGENT_KIND_VALUES = [
|
||||
'claude-code',
|
||||
'claude-agent-teams',
|
||||
'openclaude',
|
||||
'codex',
|
||||
'autohand',
|
||||
'opencode',
|
||||
'mimo-code',
|
||||
'pi',
|
||||
'omp',
|
||||
'prime-agent',
|
||||
'gemini',
|
||||
'antigravity',
|
||||
'aider',
|
||||
'goose',
|
||||
'amp',
|
||||
'kilo',
|
||||
'kiro',
|
||||
'crush',
|
||||
'aug',
|
||||
'cline',
|
||||
'codebuff',
|
||||
'command-code',
|
||||
'continue',
|
||||
'cursor',
|
||||
'droid',
|
||||
'kimi',
|
||||
'mistral-vibe',
|
||||
'qwen-code',
|
||||
'rovo',
|
||||
'hermes',
|
||||
'openclaw',
|
||||
'copilot',
|
||||
'grok',
|
||||
'devin',
|
||||
'ante',
|
||||
'trae',
|
||||
'other'
|
||||
] as const
|
||||
export const agentKindSchema = z.enum(AGENT_KIND_VALUES)
|
||||
export type AgentKind = z.infer<typeof agentKindSchema>
|
||||
|
||||
// Small set: only failures Orca's PTY-typed-command launch can observe (`binary_not_found` = shell ENOENT, `paste_readiness_timeout`, `unknown`).
|
||||
// Provider-side errors (auth/rate-limit/network) happen inside the agent CLI subprocess and are invisible to Orca. See telemetry-plan.md §Defer per-incident error fields.
|
||||
export const errorClassSchema = z.enum(['binary_not_found', 'paste_readiness_timeout', '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>
|
||||
|
||||
// Historical setup-step choices (current flows skip that screen); kept for pre-rollout rows and compatibility.
|
||||
export const addRepoSetupStepActionSchema = z.enum([
|
||||
'open_primary',
|
||||
'create_worktree',
|
||||
'configure',
|
||||
'skip',
|
||||
'open_existing',
|
||||
'back'
|
||||
])
|
||||
|
||||
export const addRepoExistingWorkspaceSourceSchema = z.enum([
|
||||
'local_folder_picker',
|
||||
'runtime_server_path',
|
||||
'ssh_remote_path',
|
||||
'clone_url',
|
||||
'create_project'
|
||||
])
|
||||
export type AddRepoExistingWorkspaceSource = z.infer<typeof addRepoExistingWorkspaceSourceSchema>
|
||||
export const addRepoDefaultCheckoutHandoffSourceSchema = z.enum([
|
||||
'local_folder_picker',
|
||||
'runtime_server_path',
|
||||
'ssh_remote_path',
|
||||
'clone_url',
|
||||
'create_project',
|
||||
'onboarding_open_folder',
|
||||
'onboarding_clone_url',
|
||||
'project_added_compat'
|
||||
])
|
||||
export type AddRepoDefaultCheckoutHandoffSource = z.infer<
|
||||
typeof addRepoDefaultCheckoutHandoffSourceSchema
|
||||
>
|
||||
export const addRepoDefaultCheckoutHandoffResultSchema = z.enum([
|
||||
'opened_default_checkout',
|
||||
'revealed_project'
|
||||
])
|
||||
export const addRepoDefaultCheckoutHandoffReasonSchema = z.enum([
|
||||
'loaded_default_checkout',
|
||||
'detected_default_checkout',
|
||||
'no_authoritative_detection',
|
||||
'no_default_checkout',
|
||||
'show_detected_default_failed',
|
||||
'show_detected_linked_failed',
|
||||
'authoritative_refresh_failed',
|
||||
'linked_external_refresh_failed',
|
||||
'refreshed_default_missing'
|
||||
])
|
||||
|
||||
export const setupScriptImportProviderSchema = z.enum(SETUP_SCRIPT_IMPORT_PROVIDERS)
|
||||
|
||||
// Separate enum from `errorClassSchema` — different domain (git/filesystem worktree-create failures); merging would couple the two forever.
|
||||
export const workspaceCreateErrorClassSchema = z.enum([
|
||||
'git_failed',
|
||||
'path_collision',
|
||||
'permission_denied',
|
||||
'base_ref_missing',
|
||||
'unknown'
|
||||
])
|
||||
export type WorkspaceCreateErrorClass = z.infer<typeof workspaceCreateErrorClassSchema>
|
||||
|
||||
export const workspaceSourceSchema = z.enum(WORKSPACE_SOURCE_VALUES)
|
||||
export type { WorkspaceSource }
|
||||
|
||||
export const launchSourceSchema = z.enum([
|
||||
'command_palette',
|
||||
'sidebar',
|
||||
'quick_command',
|
||||
'tab_bar_quick_launch',
|
||||
'task_page',
|
||||
'new_workspace_composer',
|
||||
'workspace_jump_palette',
|
||||
'shortcut',
|
||||
'onboarding',
|
||||
'diff_notes_send',
|
||||
'notes_send',
|
||||
'conflict_resolution',
|
||||
'source_control_recovery',
|
||||
'terminal_context_menu',
|
||||
'unknown'
|
||||
])
|
||||
export type LaunchSource = z.infer<typeof launchSourceSchema>
|
||||
|
||||
export const requestKindSchema = z.enum(['new', 'resume', 'followup'])
|
||||
export type RequestKind = z.infer<typeof requestKindSchema>
|
||||
|
||||
export const featureWallTileIdSchema = z.enum([
|
||||
'tile-01',
|
||||
'tile-02',
|
||||
'tile-03',
|
||||
'tile-04',
|
||||
'tile-05',
|
||||
'tile-06',
|
||||
'tile-07',
|
||||
'tile-08',
|
||||
'tile-09',
|
||||
'tile-10',
|
||||
'tile-11',
|
||||
'tile-12'
|
||||
])
|
||||
|
||||
export const featureWallOpenSourceSchema = z.enum(['help_menu', 'popup', 'onboarding', 'unknown'])
|
||||
export type FeatureWallOpenSourceTelemetry = z.infer<typeof featureWallOpenSourceSchema>
|
||||
|
||||
export const featureWallWorkflowIdSchema = z.enum([
|
||||
'tasks',
|
||||
'workspaces',
|
||||
'agents-orchestration',
|
||||
'workbench',
|
||||
'review'
|
||||
])
|
||||
|
||||
export const featureWallTourDepthStepSchema = z.enum(FEATURE_WALL_TOUR_DEPTH_STEPS)
|
||||
|
||||
export const featureWallExitActionSchema = z.enum(FEATURE_WALL_EXIT_ACTIONS)
|
||||
|
||||
// `env_var` absent — env-var/CI paths override consent at runtime only, never firing an opt-in/out event.
|
||||
// `first_launch_notice` absent — the new-user cohort has no first-launch surface; those opt-outs come via `'settings'`.
|
||||
export const optInViaSchema = z.enum(['first_launch_banner', 'settings'])
|
||||
export type OptInVia = z.infer<typeof optInViaSchema>
|
||||
|
||||
// Whitelist of settings emittable on `settings_changed`. `orca_channel` (build-time, not user-togglable) is absent.
|
||||
// The telemetry opt-in toggle is also absent — it fires dedicated `telemetry_opted_in/out` events; listing it would double-fire.
|
||||
export type BooleanGlobalSettingsKey = {
|
||||
// Why: new toggles may be optional for legacy-settings compat but are still boolean once defaulted.
|
||||
[Key in keyof GlobalSettings]-?: NonNullable<GlobalSettings[Key]> extends boolean ? Key : never
|
||||
}[keyof GlobalSettings]
|
||||
export const SETTINGS_CHANGED_WHITELIST = [
|
||||
'editorAutoSave',
|
||||
'openLinksInApp',
|
||||
'openLinksInAppModifierInverts',
|
||||
'terminalLinkActionPopoverEnabled',
|
||||
'experimentalMobile',
|
||||
'experimentalPet',
|
||||
'experimentalNativeChat',
|
||||
'experimentalStructuredNativeChat',
|
||||
'experimentalActivity',
|
||||
'experimentalAgentDashboardPopout',
|
||||
'experimentalTerminalAttention',
|
||||
'experimentalAgentHibernation',
|
||||
'experimentalEphemeralVms',
|
||||
'geminiCliOAuthEnabled',
|
||||
'openAgentTabsInChatByDefault'
|
||||
] as const satisfies readonly BooleanGlobalSettingsKey[]
|
||||
export const settingsChangedKeySchema = z.enum(SETTINGS_CHANGED_WHITELIST)
|
||||
export type SettingsChangedKey = z.infer<typeof settingsChangedKeySchema>
|
||||
@@ -0,0 +1,265 @@
|
||||
import { z } from 'zod'
|
||||
import {
|
||||
NESTED_REPO_COUNT_BUCKETS,
|
||||
NESTED_REPO_IMPORT_ACTIONS,
|
||||
NESTED_REPO_IMPORT_OUTCOMES,
|
||||
NESTED_REPO_SCAN_RESULTS,
|
||||
NESTED_REPO_TELEMETRY_MAX_REPO_COUNT,
|
||||
NESTED_REPO_TELEMETRY_RUNTIME_KINDS,
|
||||
NESTED_REPO_TELEMETRY_SURFACES,
|
||||
bucketNestedRepoTelemetryCount
|
||||
} from './nested-repo-telemetry'
|
||||
import { nthRepoAddedSchema } from './telemetry-app-event-schemas'
|
||||
import {
|
||||
addRepoDefaultCheckoutHandoffReasonSchema,
|
||||
addRepoDefaultCheckoutHandoffResultSchema,
|
||||
addRepoDefaultCheckoutHandoffSourceSchema,
|
||||
addRepoExistingWorkspaceSourceSchema,
|
||||
addRepoSetupStepActionSchema,
|
||||
setupScriptImportProviderSchema,
|
||||
workspaceCreateErrorClassSchema,
|
||||
workspaceSourceSchema
|
||||
} from './telemetry-property-schemas'
|
||||
|
||||
export const existingWorkspaceCountSchema = z.number().int().min(1).max(50)
|
||||
export const addRepoExistingWorkspaceContextSchema = {
|
||||
source: addRepoExistingWorkspaceSourceSchema,
|
||||
existing_workspace_count: existingWorkspaceCountSchema,
|
||||
existing_linked_workspace_count: z.number().int().min(0).max(50)
|
||||
} as const
|
||||
|
||||
export const addRepoSetupStepActionEventSchema = z
|
||||
.object({
|
||||
action: addRepoSetupStepActionSchema,
|
||||
source: addRepoExistingWorkspaceSourceSchema.optional(),
|
||||
existing_workspace_count: existingWorkspaceCountSchema.optional(),
|
||||
existing_linked_workspace_count: z.number().int().min(0).max(50).optional(),
|
||||
nth_repo_added: nthRepoAddedSchema
|
||||
})
|
||||
.strict()
|
||||
export const addRepoExistingWorkspacesDetectedSchema = z
|
||||
.object({
|
||||
...addRepoExistingWorkspaceContextSchema,
|
||||
main_workspace_count: z.number().int().min(0).max(50),
|
||||
branch_named_workspace_count: z.number().int().min(0).max(50),
|
||||
detached_workspace_count: z.number().int().min(0).max(50),
|
||||
custom_named_workspace_count: z.number().int().min(0).max(50),
|
||||
sparse_workspace_count: z.number().int().min(0).max(50),
|
||||
nth_repo_added: nthRepoAddedSchema
|
||||
})
|
||||
.strict()
|
||||
export const addRepoDefaultCheckoutHandoffSchema = z
|
||||
.object({
|
||||
source: addRepoDefaultCheckoutHandoffSourceSchema,
|
||||
result: addRepoDefaultCheckoutHandoffResultSchema,
|
||||
reason: addRepoDefaultCheckoutHandoffReasonSchema,
|
||||
nth_repo_added: nthRepoAddedSchema
|
||||
})
|
||||
.strict()
|
||||
|
||||
// Why: enum-only like `agent_error` — `.strict()` blocks raw error strings from ever crossing the wire.
|
||||
export const workspaceCreateFailedSchema = z
|
||||
.object({
|
||||
source: workspaceSourceSchema,
|
||||
error_class: workspaceCreateErrorClassSchema,
|
||||
nth_repo_added: nthRepoAddedSchema
|
||||
})
|
||||
.strict()
|
||||
|
||||
export const setupScriptPromptModeSchema = z.enum(['import_available', 'configure_needed'])
|
||||
export const setupScriptCountBucketSchema = z.enum(['0', '1', '2-3', '4+'])
|
||||
export const setupScriptPromptContextSchema = {
|
||||
mode: setupScriptPromptModeSchema,
|
||||
// Why: superRefine (not transform) keeps the top-level ZodObject shape that cohort injection probes.
|
||||
provider: setupScriptImportProviderSchema.optional(),
|
||||
file_count_bucket: setupScriptCountBucketSchema,
|
||||
unsupported_field_count_bucket: setupScriptCountBucketSchema,
|
||||
has_shared_hooks: z.boolean(),
|
||||
nth_repo_added: nthRepoAddedSchema
|
||||
} as const
|
||||
|
||||
export type SetupScriptPromptContextTelemetry = {
|
||||
mode: z.infer<typeof setupScriptPromptModeSchema>
|
||||
provider?: z.infer<typeof setupScriptImportProviderSchema>
|
||||
}
|
||||
|
||||
export function validateSetupScriptPromptProvider(
|
||||
props: SetupScriptPromptContextTelemetry,
|
||||
ctx: z.RefinementCtx
|
||||
): void {
|
||||
if (props.mode === 'import_available' && props.provider === undefined) {
|
||||
ctx.addIssue({
|
||||
code: 'custom',
|
||||
path: ['provider'],
|
||||
message: 'provider is required when a setup candidate is available'
|
||||
})
|
||||
}
|
||||
if (props.mode === 'configure_needed' && props.provider !== undefined) {
|
||||
ctx.addIssue({
|
||||
code: 'custom',
|
||||
path: ['provider'],
|
||||
message: 'provider is only valid when a setup candidate is available'
|
||||
})
|
||||
}
|
||||
}
|
||||
// Why: retention-cohort telemetry, not repo debugging — closed enums and count buckets only.
|
||||
export const setupScriptPromptShownSchema = z
|
||||
.object(setupScriptPromptContextSchema)
|
||||
.strict()
|
||||
.superRefine(validateSetupScriptPromptProvider)
|
||||
export const setupScriptDetectedSaveActions = [
|
||||
'save_detected_setup_clicked',
|
||||
'save_detected_setup_completed',
|
||||
'save_detected_setup_failed'
|
||||
] as const
|
||||
|
||||
export function isSetupScriptDetectedSaveAction(action: unknown): boolean {
|
||||
return setupScriptDetectedSaveActions.includes(action as never)
|
||||
}
|
||||
|
||||
export function validateSetupScriptPromptAction(
|
||||
props: SetupScriptPromptContextTelemetry & {
|
||||
action?: string
|
||||
edited_before_save?: boolean
|
||||
},
|
||||
ctx: z.RefinementCtx
|
||||
): void {
|
||||
validateSetupScriptPromptProvider(props, ctx)
|
||||
const isDetectedSave = isSetupScriptDetectedSaveAction(props.action)
|
||||
if (isDetectedSave && props.provider !== 'package-manager') {
|
||||
ctx.addIssue({
|
||||
code: 'custom',
|
||||
path: ['provider'],
|
||||
message: 'detected setup save actions require the package-manager provider'
|
||||
})
|
||||
}
|
||||
if (isDetectedSave && props.edited_before_save === undefined) {
|
||||
ctx.addIssue({
|
||||
code: 'custom',
|
||||
path: ['edited_before_save'],
|
||||
message: 'edited_before_save is required for detected setup save actions'
|
||||
})
|
||||
}
|
||||
if (!isDetectedSave && props.edited_before_save !== undefined) {
|
||||
ctx.addIssue({
|
||||
code: 'custom',
|
||||
path: ['edited_before_save'],
|
||||
message: 'edited_before_save is only valid for detected setup save actions'
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
export const setupScriptPromptActionSchema = z
|
||||
.object({
|
||||
...setupScriptPromptContextSchema,
|
||||
action: z.enum([
|
||||
'import_completed',
|
||||
'import_failed',
|
||||
'configure_clicked',
|
||||
'dismissed',
|
||||
...setupScriptDetectedSaveActions
|
||||
]),
|
||||
edited_before_save: z.boolean().optional()
|
||||
})
|
||||
.strict()
|
||||
.superRefine(validateSetupScriptPromptAction)
|
||||
|
||||
export const nestedRepoTelemetrySurfaceSchema = z.enum(NESTED_REPO_TELEMETRY_SURFACES)
|
||||
export const nestedRepoTelemetryRuntimeKindSchema = z.enum(NESTED_REPO_TELEMETRY_RUNTIME_KINDS)
|
||||
export const nestedRepoCountSchema = z
|
||||
.number()
|
||||
.int()
|
||||
.min(0)
|
||||
.max(NESTED_REPO_TELEMETRY_MAX_REPO_COUNT)
|
||||
export const nestedRepoCountBucketSchema = z.enum(NESTED_REPO_COUNT_BUCKETS)
|
||||
export const nestedRepoScanResultSchema = z.enum(NESTED_REPO_SCAN_RESULTS)
|
||||
export const nestedRepoImportActionSchema = z.enum(NESTED_REPO_IMPORT_ACTIONS)
|
||||
export const nestedRepoImportOutcomeSchema = z.enum(NESTED_REPO_IMPORT_OUTCOMES)
|
||||
export const nestedRepoScanPathKindSchema = z.enum(['git_repo', 'non_git_folder'])
|
||||
export const nestedRepoImportModeSchema = z.enum(['group', 'separate'])
|
||||
export const nestedRepoAttemptIdSchema = z.string().uuid()
|
||||
|
||||
export function validateNestedRepoCountBucket(
|
||||
props: Record<string, unknown>,
|
||||
countKey: string,
|
||||
bucketKey: string,
|
||||
ctx: z.RefinementCtx
|
||||
): void {
|
||||
const count = props[countKey]
|
||||
const bucket = props[bucketKey]
|
||||
if (typeof count !== 'number' || typeof bucket !== 'string') {
|
||||
return
|
||||
}
|
||||
if (bucketNestedRepoTelemetryCount(count) !== bucket) {
|
||||
ctx.addIssue({
|
||||
code: 'custom',
|
||||
path: [bucketKey],
|
||||
message: `${bucketKey} must match ${countKey}`
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
export function validateNestedRepoCountBuckets(
|
||||
props: Record<string, unknown>,
|
||||
ctx: z.RefinementCtx
|
||||
): void {
|
||||
validateNestedRepoCountBucket(props, 'found_count', 'found_count_bucket', ctx)
|
||||
validateNestedRepoCountBucket(props, 'selected_count', 'selected_count_bucket', ctx)
|
||||
validateNestedRepoCountBucket(props, 'imported_count', 'imported_count_bucket', ctx)
|
||||
validateNestedRepoCountBucket(props, 'already_known_count', 'already_known_count_bucket', ctx)
|
||||
validateNestedRepoCountBucket(props, 'failed_count', 'failed_count_bucket', ctx)
|
||||
}
|
||||
|
||||
export const nestedRepoTelemetryBaseSchema = {
|
||||
// Why: high-cardinality but random and non-persistent — correlates scan→action→result without path-derived IDs.
|
||||
attempt_id: nestedRepoAttemptIdSchema,
|
||||
surface: nestedRepoTelemetrySurfaceSchema,
|
||||
runtime_kind: nestedRepoTelemetryRuntimeKindSchema,
|
||||
nth_repo_added: nthRepoAddedSchema
|
||||
} as const
|
||||
|
||||
export const addRepoNestedScanResultSchema = z
|
||||
.object({
|
||||
...nestedRepoTelemetryBaseSchema,
|
||||
result: nestedRepoScanResultSchema,
|
||||
selected_path_kind: nestedRepoScanPathKindSchema.optional(),
|
||||
found_count: nestedRepoCountSchema,
|
||||
found_count_bucket: nestedRepoCountBucketSchema,
|
||||
truncated: z.boolean(),
|
||||
timed_out: z.boolean()
|
||||
})
|
||||
.strict()
|
||||
.superRefine(validateNestedRepoCountBuckets)
|
||||
|
||||
export const addRepoNestedImportActionSchema = z
|
||||
.object({
|
||||
...nestedRepoTelemetryBaseSchema,
|
||||
action: nestedRepoImportActionSchema,
|
||||
found_count: nestedRepoCountSchema,
|
||||
found_count_bucket: nestedRepoCountBucketSchema,
|
||||
selected_count: nestedRepoCountSchema,
|
||||
selected_count_bucket: nestedRepoCountBucketSchema,
|
||||
all_selected: z.boolean()
|
||||
})
|
||||
.strict()
|
||||
.superRefine(validateNestedRepoCountBuckets)
|
||||
|
||||
export const addRepoNestedImportResultSchema = z
|
||||
.object({
|
||||
...nestedRepoTelemetryBaseSchema,
|
||||
mode: nestedRepoImportModeSchema,
|
||||
outcome: nestedRepoImportOutcomeSchema,
|
||||
found_count: nestedRepoCountSchema,
|
||||
found_count_bucket: nestedRepoCountBucketSchema,
|
||||
selected_count: nestedRepoCountSchema,
|
||||
selected_count_bucket: nestedRepoCountBucketSchema,
|
||||
imported_count: nestedRepoCountSchema,
|
||||
imported_count_bucket: nestedRepoCountBucketSchema,
|
||||
already_known_count: nestedRepoCountSchema,
|
||||
already_known_count_bucket: nestedRepoCountBucketSchema,
|
||||
failed_count: nestedRepoCountSchema,
|
||||
failed_count_bucket: nestedRepoCountBucketSchema,
|
||||
all_selected: z.boolean()
|
||||
})
|
||||
.strict()
|
||||
.superRefine(validateNestedRepoCountBuckets)
|
||||
Reference in New Issue
Block a user