Skip to main content

codex_analytics/
facts.rs

1use crate::events::AppServerRpcTransport;
2use crate::events::CodexRuntimeMetadata;
3use crate::events::GuardianReviewEventParams;
4use codex_app_server_protocol::ClientRequest;
5use codex_app_server_protocol::ClientResponsePayload;
6use codex_app_server_protocol::InitializeParams;
7use codex_app_server_protocol::JSONRPCErrorError;
8use codex_app_server_protocol::RequestId;
9use codex_app_server_protocol::ServerNotification;
10use codex_app_server_protocol::ServerRequest;
11use codex_app_server_protocol::ServerResponse;
12use codex_plugin::PluginTelemetryMetadata;
13use codex_protocol::config_types::ApprovalsReviewer;
14use codex_protocol::config_types::ModeKind;
15use codex_protocol::config_types::Personality;
16use codex_protocol::config_types::ReasoningSummary;
17use codex_protocol::config_types::ServiceTier;
18use codex_protocol::error::CodexErr;
19use codex_protocol::models::PermissionProfile;
20use codex_protocol::openai_models::ReasoningEffort;
21use codex_protocol::protocol::AskForApproval;
22use codex_protocol::protocol::HookEventName;
23use codex_protocol::protocol::HookRunStatus;
24use codex_protocol::protocol::HookSource;
25use codex_protocol::protocol::SessionSource;
26use codex_protocol::protocol::SkillScope;
27use codex_protocol::protocol::SubAgentSource;
28use codex_protocol::protocol::TokenUsage;
29use codex_protocol::request_permissions::RequestPermissionsResponse;
30use serde::Serialize;
31use std::path::PathBuf;
32
33#[derive(Clone, Debug, PartialEq, Eq, Serialize)]
34pub struct AcceptedLineFingerprint {
35    pub path_hash: String,
36    pub line_hash: String,
37}
38
39#[derive(Clone)]
40pub struct TrackEventsContext {
41    pub model_slug: String,
42    pub thread_id: String,
43    pub turn_id: String,
44    pub product_client_id: String,
45}
46
47pub fn build_track_events_context(
48    model_slug: String,
49    thread_id: String,
50    turn_id: String,
51    product_client_id: String,
52) -> TrackEventsContext {
53    TrackEventsContext {
54        model_slug,
55        thread_id,
56        turn_id,
57        product_client_id,
58    }
59}
60
61#[derive(Clone, Copy, Debug, Serialize)]
62#[serde(rename_all = "snake_case")]
63pub enum TurnSubmissionType {
64    Default,
65    Queued,
66}
67
68#[derive(Clone)]
69pub struct TurnResolvedConfigFact {
70    pub turn_id: String,
71    pub thread_id: String,
72    pub num_input_images: usize,
73    pub submission_type: Option<TurnSubmissionType>,
74    pub ephemeral: bool,
75    pub session_source: SessionSource,
76    pub model: String,
77    pub model_provider: String,
78    pub permission_profile: PermissionProfile,
79    pub permission_profile_cwd: PathBuf,
80    pub reasoning_effort: Option<ReasoningEffort>,
81    pub reasoning_summary: Option<ReasoningSummary>,
82    pub service_tier: Option<ServiceTier>,
83    pub approval_policy: AskForApproval,
84    pub approvals_reviewer: ApprovalsReviewer,
85    pub sandbox_network_access: bool,
86    pub collaboration_mode: ModeKind,
87    pub personality: Option<Personality>,
88    pub workspace_kind: Option<String>,
89    pub is_first_turn: bool,
90}
91
92#[derive(Clone, Copy, Debug, Serialize)]
93#[serde(rename_all = "snake_case")]
94pub enum ThreadInitializationMode {
95    New,
96    Forked,
97    Resumed,
98}
99
100#[derive(Clone)]
101pub struct TurnTokenUsageFact {
102    pub turn_id: String,
103    pub thread_id: String,
104    pub token_usage: TokenUsage,
105}
106
107#[derive(Clone, Debug, PartialEq, Eq, Serialize)]
108pub struct TurnProfile {
109    pub before_first_sampling_ms: u64,
110    pub sampling_ms: u64,
111    pub between_sampling_overhead_ms: u64,
112    pub tool_blocking_ms: u64,
113    pub after_last_sampling_ms: u64,
114    pub sampling_request_count: u32,
115    pub sampling_retry_count: u32,
116}
117
118#[derive(Clone)]
119pub struct TurnProfileFact {
120    pub turn_id: String,
121    pub profile: TurnProfile,
122}
123
124#[derive(Clone)]
125pub struct TurnCodexErrorFact {
126    pub(crate) turn_id: String,
127    pub(crate) thread_id: String,
128    pub(crate) error: TurnCodexError,
129}
130
131impl TurnCodexErrorFact {
132    pub fn from_codex_err(thread_id: String, turn_id: String, error: &CodexErr) -> Self {
133        Self {
134            turn_id,
135            thread_id,
136            error: TurnCodexError::from_codex_err(error),
137        }
138    }
139}
140
141#[derive(Clone, Copy, Debug, Serialize)]
142#[serde(rename_all = "snake_case")]
143pub enum CodexErrKind {
144    TurnAborted,
145    SessionBudgetExceeded,
146    Stream,
147    ContextWindowExceeded,
148    ThreadNotFound,
149    AgentLimitReached,
150    SessionConfiguredNotFirstEvent,
151    Timeout,
152    RequestTimeout,
153    Spawn,
154    Interrupted,
155    UnexpectedStatus,
156    InvalidRequest,
157    InvalidImageRequest,
158    UsageLimitReached,
159    ServerOverloaded,
160    CyberPolicy,
161    ResponseStreamFailed,
162    ConnectionFailed,
163    QuotaExceeded,
164    UsageNotIncluded,
165    InternalServerError,
166    RetryLimit,
167    InternalAgentDied,
168    Sandbox,
169    LandlockSandboxExecutableNotProvided,
170    UnsupportedOperation,
171    RefreshTokenFailed,
172    Fatal,
173    Io,
174    Json,
175    #[cfg(target_os = "linux")]
176    LandlockRuleset,
177    #[cfg(target_os = "linux")]
178    LandlockPathFd,
179    TokioJoin,
180    EnvVar,
181}
182
183#[derive(Clone)]
184pub(crate) struct TurnCodexError {
185    pub(crate) kind: CodexErrKind,
186    pub(crate) http_status_code: Option<u16>,
187}
188
189impl TurnCodexError {
190    fn from_codex_err(error: &CodexErr) -> Self {
191        Self {
192            kind: error.into(),
193            http_status_code: error.http_status_code_value(),
194        }
195    }
196}
197
198impl From<&CodexErr> for CodexErrKind {
199    fn from(error: &CodexErr) -> Self {
200        match error {
201            CodexErr::TurnAborted => CodexErrKind::TurnAborted,
202            CodexErr::SessionBudgetExceeded => CodexErrKind::SessionBudgetExceeded,
203            CodexErr::Stream(..) => CodexErrKind::Stream,
204            CodexErr::ContextWindowExceeded => CodexErrKind::ContextWindowExceeded,
205            CodexErr::ThreadNotFound(_) => CodexErrKind::ThreadNotFound,
206            CodexErr::AgentLimitReached { .. } => CodexErrKind::AgentLimitReached,
207            CodexErr::SessionConfiguredNotFirstEvent => {
208                CodexErrKind::SessionConfiguredNotFirstEvent
209            }
210            CodexErr::Timeout => CodexErrKind::Timeout,
211            CodexErr::RequestTimeout => CodexErrKind::RequestTimeout,
212            CodexErr::Spawn => CodexErrKind::Spawn,
213            CodexErr::Interrupted => CodexErrKind::Interrupted,
214            CodexErr::UnexpectedStatus(_) => CodexErrKind::UnexpectedStatus,
215            CodexErr::InvalidRequest(_) => CodexErrKind::InvalidRequest,
216            CodexErr::InvalidImageRequest() => CodexErrKind::InvalidImageRequest,
217            CodexErr::UsageLimitReached(_) => CodexErrKind::UsageLimitReached,
218            CodexErr::ServerOverloaded => CodexErrKind::ServerOverloaded,
219            CodexErr::CyberPolicy { .. } => CodexErrKind::CyberPolicy,
220            CodexErr::ResponseStreamFailed(_) => CodexErrKind::ResponseStreamFailed,
221            CodexErr::ConnectionFailed(_) => CodexErrKind::ConnectionFailed,
222            CodexErr::QuotaExceeded => CodexErrKind::QuotaExceeded,
223            CodexErr::UsageNotIncluded => CodexErrKind::UsageNotIncluded,
224            CodexErr::InternalServerError => CodexErrKind::InternalServerError,
225            CodexErr::RetryLimit(_) => CodexErrKind::RetryLimit,
226            CodexErr::InternalAgentDied => CodexErrKind::InternalAgentDied,
227            CodexErr::Sandbox(_) => CodexErrKind::Sandbox,
228            CodexErr::LandlockSandboxExecutableNotProvided => {
229                CodexErrKind::LandlockSandboxExecutableNotProvided
230            }
231            CodexErr::UnsupportedOperation(_) => CodexErrKind::UnsupportedOperation,
232            CodexErr::RefreshTokenFailed(_) => CodexErrKind::RefreshTokenFailed,
233            CodexErr::Fatal(_) => CodexErrKind::Fatal,
234            CodexErr::Io(_) => CodexErrKind::Io,
235            CodexErr::Json(_) => CodexErrKind::Json,
236            #[cfg(target_os = "linux")]
237            CodexErr::LandlockRuleset(_) => CodexErrKind::LandlockRuleset,
238            #[cfg(target_os = "linux")]
239            CodexErr::LandlockPathFd(_) => CodexErrKind::LandlockPathFd,
240            CodexErr::TokioJoin(_) => CodexErrKind::TokioJoin,
241            CodexErr::EnvVar(_) => CodexErrKind::EnvVar,
242        }
243    }
244}
245
246#[derive(Clone, Copy, Debug, Serialize)]
247#[serde(rename_all = "snake_case")]
248pub enum TurnStatus {
249    Completed,
250    Failed,
251    Interrupted,
252}
253
254#[derive(Clone, Copy, Debug, Serialize)]
255#[serde(rename_all = "snake_case")]
256pub enum TurnSteerResult {
257    Accepted,
258    Rejected,
259}
260
261#[derive(Clone, Copy, Debug, Serialize)]
262#[serde(rename_all = "snake_case")]
263pub enum TurnSteerRejectionReason {
264    NoActiveTurn,
265    ExpectedTurnMismatch,
266    NonSteerableReview,
267    NonSteerableCompact,
268    EmptyInput,
269    InputTooLarge,
270}
271
272#[derive(Clone)]
273pub struct CodexTurnSteerEvent {
274    pub expected_turn_id: Option<String>,
275    pub accepted_turn_id: Option<String>,
276    pub num_input_images: usize,
277    pub result: TurnSteerResult,
278    pub rejection_reason: Option<TurnSteerRejectionReason>,
279    pub created_at: u64,
280}
281
282#[derive(Clone, Copy, Debug)]
283pub enum AnalyticsJsonRpcError {
284    TurnSteer(TurnSteerRequestError),
285    Input(InputError),
286}
287
288#[derive(Clone, Copy, Debug)]
289pub enum TurnSteerRequestError {
290    NoActiveTurn,
291    ExpectedTurnMismatch,
292    NonSteerableReview,
293    NonSteerableCompact,
294}
295
296#[derive(Clone, Copy, Debug)]
297pub enum InputError {
298    Empty,
299    TooLarge,
300}
301
302impl From<TurnSteerRequestError> for TurnSteerRejectionReason {
303    fn from(error: TurnSteerRequestError) -> Self {
304        match error {
305            TurnSteerRequestError::NoActiveTurn => Self::NoActiveTurn,
306            TurnSteerRequestError::ExpectedTurnMismatch => Self::ExpectedTurnMismatch,
307            TurnSteerRequestError::NonSteerableReview => Self::NonSteerableReview,
308            TurnSteerRequestError::NonSteerableCompact => Self::NonSteerableCompact,
309        }
310    }
311}
312
313impl From<InputError> for TurnSteerRejectionReason {
314    fn from(error: InputError) -> Self {
315        match error {
316            InputError::Empty => Self::EmptyInput,
317            InputError::TooLarge => Self::InputTooLarge,
318        }
319    }
320}
321
322#[derive(Clone, Debug)]
323pub struct SkillInvocation {
324    pub skill_name: String,
325    pub skill_scope: SkillScope,
326    pub skill_path: PathBuf,
327    pub plugin_id: Option<String>,
328    pub invocation_type: InvocationType,
329}
330
331#[derive(Clone, Copy, Debug, Serialize)]
332#[serde(rename_all = "lowercase")]
333pub enum InvocationType {
334    Explicit,
335    Implicit,
336}
337
338pub struct AppInvocation {
339    pub connector_id: Option<String>,
340    pub app_name: Option<String>,
341    pub invocation_type: Option<InvocationType>,
342}
343
344#[derive(Clone)]
345pub struct SubAgentThreadStartedInput {
346    pub session_id: String,
347    pub thread_id: String,
348    pub parent_thread_id: Option<String>,
349    pub forked_from_thread_id: Option<String>,
350    pub product_client_id: String,
351    pub client_name: String,
352    pub client_version: String,
353    pub model: String,
354    pub ephemeral: bool,
355    pub subagent_source: SubAgentSource,
356    pub created_at: u64,
357}
358
359#[derive(Clone, Copy, Debug, Serialize)]
360#[serde(rename_all = "snake_case")]
361pub enum CompactionTrigger {
362    Manual,
363    Auto,
364}
365
366#[derive(Clone, Copy, Debug, Serialize)]
367#[serde(rename_all = "snake_case")]
368pub enum CompactionReason {
369    UserRequested,
370    ContextLimit,
371    ModelDownshift,
372    CompHashChanged,
373}
374
375#[derive(Clone, Copy, Debug, Serialize)]
376#[serde(rename_all = "snake_case")]
377pub enum CompactionImplementation {
378    Responses,
379    ResponsesCompactionV2,
380    ResponsesCompact,
381}
382
383#[derive(Clone, Copy, Debug, Serialize)]
384#[serde(rename_all = "snake_case")]
385pub enum CompactionPhase {
386    StandaloneTurn,
387    PreTurn,
388    MidTurn,
389}
390
391#[derive(Clone, Copy, Debug, Serialize)]
392#[serde(rename_all = "snake_case")]
393pub enum CompactionStrategy {
394    Memento,
395    PrefixCompaction,
396}
397
398#[derive(Clone, Copy, Debug, Serialize)]
399#[serde(rename_all = "snake_case")]
400pub enum CompactionStatus {
401    Completed,
402    Failed,
403    Interrupted,
404}
405
406#[derive(Clone)]
407pub struct CodexCompactionEvent {
408    pub thread_id: String,
409    pub turn_id: String,
410    pub trigger: CompactionTrigger,
411    pub reason: CompactionReason,
412    pub implementation: CompactionImplementation,
413    pub phase: CompactionPhase,
414    pub strategy: CompactionStrategy,
415    pub status: CompactionStatus,
416    pub codex_error_kind: Option<CodexErrKind>,
417    pub codex_error_http_status_code: Option<u16>,
418    pub active_context_tokens_before: i64,
419    pub active_context_tokens_after: i64,
420    pub retained_image_count: Option<usize>,
421    pub compaction_summary_tokens: Option<i64>,
422    pub cached_input_tokens: Option<i64>,
423    pub cache_write_input_tokens: Option<i64>,
424    pub started_at: u64,
425    pub completed_at: u64,
426    pub duration_ms: Option<u64>,
427}
428
429#[derive(Clone, Copy, Debug, Serialize)]
430#[serde(rename_all = "snake_case")]
431pub enum GoalEventKind {
432    Created,
433    UsageAccounted,
434    StatusChanged,
435    Cleared,
436}
437
438#[derive(Clone)]
439pub struct CodexGoalEvent {
440    pub thread_id: String,
441    pub turn_id: Option<String>,
442    pub goal_id: String,
443    pub event_kind: GoalEventKind,
444    pub goal_status: codex_state::ThreadGoalStatus,
445    pub has_token_budget: bool,
446    pub cumulative_tokens_accounted: Option<i64>,
447    pub cumulative_time_accounted_seconds: Option<i64>,
448}
449
450#[allow(dead_code)]
451pub(crate) enum AnalyticsFact {
452    Initialize {
453        connection_id: u64,
454        params: InitializeParams,
455        product_client_id: String,
456        runtime: CodexRuntimeMetadata,
457        rpc_transport: AppServerRpcTransport,
458    },
459    ClientRequest {
460        connection_id: u64,
461        request_id: RequestId,
462        request: Box<ClientRequest>,
463    },
464    ClientResponse {
465        connection_id: u64,
466        request_id: RequestId,
467        response: Box<ClientResponsePayload>,
468        thread_originator: Option<String>,
469    },
470    ErrorResponse {
471        connection_id: u64,
472        request_id: RequestId,
473        error: JSONRPCErrorError,
474        error_type: Option<AnalyticsJsonRpcError>,
475    },
476    ServerRequest {
477        connection_id: u64,
478        request: Box<ServerRequest>,
479    },
480    ServerResponse {
481        completed_at_ms: u64,
482        response: Box<ServerResponse>,
483    },
484    EffectivePermissionsApprovalResponse {
485        completed_at_ms: u64,
486        request_id: RequestId,
487        response: Box<RequestPermissionsResponse>,
488    },
489    ServerRequestAborted {
490        completed_at_ms: u64,
491        request_id: RequestId,
492    },
493    Notification(Box<ServerNotification>),
494    // Facts that do not naturally exist on the app-server protocol surface, or
495    // would require non-trivial protocol reshaping on this branch.
496    Custom(CustomAnalyticsFact),
497}
498
499pub(crate) enum CustomAnalyticsFact {
500    SubAgentThreadStarted(SubAgentThreadStartedInput),
501    Compaction(Box<CodexCompactionEvent>),
502    Goal(Box<CodexGoalEvent>),
503    GuardianReview(Box<GuardianReviewEventParams>),
504    TurnResolvedConfig(Box<TurnResolvedConfigFact>),
505    TurnTokenUsage(Box<TurnTokenUsageFact>),
506    TurnProfile(Box<TurnProfileFact>),
507    TurnCodexError(Box<TurnCodexErrorFact>),
508    SkillInvoked(SkillInvokedInput),
509    AppMentioned(AppMentionedInput),
510    AppUsed(AppUsedInput),
511    HookRun(HookRunInput),
512    PluginUsed(PluginUsedInput),
513    PluginInstallRequested(PluginInstallRequestedInput),
514    PluginStateChanged(PluginStateChangedInput),
515    PluginInstallFailed(PluginInstallFailedInput),
516    ExternalAgentConfigImportCompleted(ExternalAgentConfigImportCompletedInput),
517    ExternalAgentConfigImportFailure(ExternalAgentConfigImportFailureInput),
518}
519
520pub(crate) struct SkillInvokedInput {
521    pub tracking: TrackEventsContext,
522    pub invocations: Vec<SkillInvocation>,
523}
524
525pub(crate) struct AppMentionedInput {
526    pub tracking: TrackEventsContext,
527    pub mentions: Vec<AppInvocation>,
528}
529
530pub(crate) struct AppUsedInput {
531    pub tracking: TrackEventsContext,
532    pub app: AppInvocation,
533}
534
535pub(crate) struct HookRunInput {
536    pub tracking: TrackEventsContext,
537    pub hook: HookRunFact,
538}
539
540pub struct HookRunFact {
541    pub event_name: HookEventName,
542    pub hook_source: HookSource,
543    pub status: HookRunStatus,
544}
545
546pub(crate) struct PluginUsedInput {
547    pub tracking: TrackEventsContext,
548    pub plugin: PluginTelemetryMetadata,
549}
550
551#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize)]
552#[serde(rename_all = "snake_case")]
553pub enum PluginInstallRequestSource {
554    EndpointRecommendation,
555    LegacyDiscovery,
556}
557
558#[derive(Clone, Debug, PartialEq, Eq)]
559pub struct PluginInstallRequested {
560    pub suggestion_id: String,
561    pub plugins: Vec<PluginInstallRequestedPlugin>,
562    pub source: PluginInstallRequestSource,
563}
564
565#[derive(Clone, Debug, PartialEq, Eq)]
566pub struct PluginInstallRequestedPlugin {
567    pub plugin_id: String,
568    pub remote_plugin_id: Option<String>,
569    pub plugin_name: String,
570    pub connector_ids: Vec<String>,
571}
572
573pub(crate) struct PluginInstallRequestedInput {
574    pub tracking: TrackEventsContext,
575    pub request: PluginInstallRequested,
576}
577
578pub(crate) struct PluginStateChangedInput {
579    pub plugin: PluginTelemetryMetadata,
580    pub state: PluginState,
581}
582
583#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize)]
584#[serde(rename_all = "snake_case")]
585pub enum PluginInstallSource {
586    Manual,
587    ExternalAgentMigration,
588}
589
590pub(crate) struct PluginInstallFailedInput {
591    pub plugin: PluginTelemetryMetadata,
592    pub source: PluginInstallSource,
593    pub error_type: String,
594    pub sub_error_type: Option<String>,
595}
596
597pub struct ExternalAgentConfigImportCompletedInput {
598    pub import_id: String,
599    pub source: String,
600    pub provider_id: String,
601    pub item_type: String,
602    pub success_count: usize,
603    pub failed_count: usize,
604}
605
606pub struct ExternalAgentConfigImportFailureInput {
607    pub import_id: String,
608    pub source: String,
609    pub provider_id: String,
610    pub item_type: String,
611    pub failure_stage: String,
612    pub error_type: String,
613    pub sub_error_type: Option<String>,
614}
615
616#[derive(Clone, Copy)]
617pub(crate) enum PluginState {
618    Installed,
619    Uninstalled,
620    Enabled,
621    Disabled,
622}