Skip to main content

codex_analytics/
events.rs

1use std::time::Instant;
2
3use crate::facts::AcceptedLineFingerprint;
4use crate::facts::AppInvocation;
5use crate::facts::CodexCompactionEvent;
6use crate::facts::CodexErrKind;
7use crate::facts::CodexGoalEvent;
8use crate::facts::CompactionImplementation;
9use crate::facts::CompactionPhase;
10use crate::facts::CompactionReason;
11use crate::facts::CompactionStatus;
12use crate::facts::CompactionStrategy;
13use crate::facts::CompactionTrigger;
14use crate::facts::GoalEventKind;
15use crate::facts::HookRunFact;
16use crate::facts::InvocationType;
17use crate::facts::PluginInstallRequested;
18use crate::facts::PluginState;
19use crate::facts::SubAgentThreadStartedInput;
20use crate::facts::ThreadInitializationMode;
21use crate::facts::TrackEventsContext;
22use crate::facts::TurnStatus;
23use crate::facts::TurnSteerRejectionReason;
24use crate::facts::TurnSteerResult;
25use crate::facts::TurnSubmissionType;
26use crate::now_unix_millis;
27use codex_app_server_protocol::CodexErrorInfo;
28use codex_app_server_protocol::CommandExecutionSource;
29use codex_login::default_client::originator;
30use codex_plugin::PluginId;
31use codex_plugin::PluginTelemetryMetadata;
32use codex_protocol::approvals::NetworkApprovalProtocol;
33use codex_protocol::models::AdditionalPermissionProfile;
34use codex_protocol::models::SandboxPermissions;
35use codex_protocol::protocol::GuardianAssessmentOutcome;
36use codex_protocol::protocol::GuardianCommandSource;
37use codex_protocol::protocol::GuardianRiskLevel;
38use codex_protocol::protocol::GuardianUserAuthorization;
39use codex_protocol::protocol::HookEventName;
40use codex_protocol::protocol::HookRunStatus;
41use codex_protocol::protocol::HookSource;
42use codex_protocol::protocol::SubAgentSource;
43use codex_protocol::protocol::ThreadSource;
44use codex_protocol::protocol::TokenUsage;
45use serde::Serialize;
46
47#[derive(Clone, Copy, Debug, Serialize)]
48#[serde(rename_all = "snake_case")]
49pub enum AppServerRpcTransport {
50    Stdio,
51    Websocket,
52    InProcess,
53}
54
55#[derive(Serialize)]
56pub(crate) struct TrackEventsRequest {
57    pub(crate) events: Vec<TrackEventRequest>,
58}
59
60#[derive(Serialize)]
61#[serde(untagged)]
62pub(crate) enum TrackEventRequest {
63    SkillInvocation(SkillInvocationEventRequest),
64    ThreadInitialized(ThreadInitializedEvent),
65    GuardianReview(Box<GuardianReviewEventRequest>),
66    AppMentioned(CodexAppMentionedEventRequest),
67    AppUsed(CodexAppUsedEventRequest),
68    HookRun(CodexHookRunEventRequest),
69    Compaction(Box<CodexCompactionEventRequest>),
70    Goal(Box<CodexGoalEventRequest>),
71    TurnEvent(Box<CodexTurnEventRequest>),
72    TurnSteer(CodexTurnSteerEventRequest),
73    CommandExecution(CodexCommandExecutionEventRequest),
74    FileChange(CodexFileChangeEventRequest),
75    McpToolCall(CodexMcpToolCallEventRequest),
76    DynamicToolCall(CodexDynamicToolCallEventRequest),
77    CollabAgentToolCall(CodexCollabAgentToolCallEventRequest),
78    WebSearch(CodexWebSearchEventRequest),
79    ImageGeneration(CodexImageGenerationEventRequest),
80    AcceptedLineFingerprints(Box<CodexAcceptedLineFingerprintsEventRequest>),
81    #[allow(dead_code)]
82    ReviewEvent(CodexReviewEventRequest),
83    PluginUsed(CodexPluginUsedEventRequest),
84    PluginInstallRequested(CodexPluginInstallRequestedEventRequest),
85    PluginInstalled(CodexPluginEventRequest),
86    PluginUninstalled(CodexPluginEventRequest),
87    PluginEnabled(CodexPluginEventRequest),
88    PluginDisabled(CodexPluginEventRequest),
89    PluginInstallFailed(CodexPluginInstallFailedEventRequest),
90    ExternalAgentConfigImportCompleted(CodexOnboardingExternalAgentImportCompleteEventRequest),
91    ExternalAgentConfigImportFailure(CodexOnboardingExternalAgentImportFailureEventRequest),
92}
93
94impl TrackEventRequest {
95    pub(crate) fn should_send_in_isolated_request(&self) -> bool {
96        matches!(self, Self::AcceptedLineFingerprints(_))
97    }
98
99    pub(crate) fn can_send_with_api_key_auth(&self) -> bool {
100        match self {
101            Self::PluginUsed(event) => event.event_params.plugin.plugin_id.is_some(),
102            Self::SkillInvocation(event) => event.event_params.plugin_id.is_some(),
103            Self::McpToolCall(event) => event.event_params.plugin_id.is_some(),
104            _ => false,
105        }
106    }
107}
108
109#[derive(Serialize)]
110pub(crate) struct CodexAcceptedLineFingerprintsEventParams {
111    pub(crate) event_type: &'static str,
112    pub(crate) turn_id: String,
113    pub(crate) thread_id: String,
114    pub(crate) product_surface: Option<String>,
115    pub(crate) model_slug: Option<String>,
116    pub(crate) completed_at: u64,
117    pub(crate) repo_hash: Option<String>,
118    pub(crate) accepted_added_lines: u64,
119    pub(crate) accepted_deleted_lines: u64,
120    pub(crate) line_fingerprints: Vec<AcceptedLineFingerprint>,
121}
122
123#[derive(Serialize)]
124pub(crate) struct CodexAcceptedLineFingerprintsEventRequest {
125    pub(crate) event_type: &'static str,
126    pub(crate) event_params: CodexAcceptedLineFingerprintsEventParams,
127}
128
129#[derive(Serialize)]
130pub(crate) struct SkillInvocationEventRequest {
131    pub(crate) event_type: &'static str,
132    pub(crate) skill_id: String,
133    pub(crate) skill_name: String,
134    pub(crate) event_params: SkillInvocationEventParams,
135}
136
137#[derive(Serialize)]
138pub(crate) struct SkillInvocationEventParams {
139    pub(crate) product_client_id: Option<String>,
140    pub(crate) skill_scope: Option<String>,
141    pub(crate) plugin_id: Option<String>,
142    pub(crate) repo_url: Option<String>,
143    pub(crate) thread_id: Option<String>,
144    pub(crate) turn_id: Option<String>,
145    pub(crate) invoke_type: Option<InvocationType>,
146    pub(crate) model_slug: Option<String>,
147}
148
149#[derive(Clone, Serialize)]
150pub(crate) struct CodexAppServerClientMetadata {
151    pub(crate) product_client_id: String,
152    pub(crate) client_name: Option<String>,
153    pub(crate) client_version: Option<String>,
154    pub(crate) rpc_transport: AppServerRpcTransport,
155    pub(crate) experimental_api_enabled: Option<bool>,
156}
157
158#[derive(Clone, Serialize)]
159pub(crate) struct CodexRuntimeMetadata {
160    pub(crate) codex_rs_version: String,
161    pub(crate) runtime_os: String,
162    pub(crate) runtime_os_version: String,
163    pub(crate) runtime_arch: String,
164}
165
166#[derive(Serialize)]
167pub(crate) struct ThreadInitializedEventParams {
168    pub(crate) thread_id: String,
169    pub(crate) session_id: String,
170    pub(crate) app_server_client: CodexAppServerClientMetadata,
171    pub(crate) runtime: CodexRuntimeMetadata,
172    pub(crate) model: String,
173    pub(crate) ephemeral: bool,
174    pub(crate) thread_source: Option<ThreadSource>,
175    pub(crate) initialization_mode: ThreadInitializationMode,
176    pub(crate) subagent_source: Option<String>,
177    pub(crate) parent_thread_id: Option<String>,
178    pub(crate) forked_from_thread_id: Option<String>,
179    pub(crate) created_at: u64,
180}
181
182#[derive(Serialize)]
183pub(crate) struct ThreadInitializedEvent {
184    pub(crate) event_type: &'static str,
185    pub(crate) event_params: ThreadInitializedEventParams,
186}
187
188#[derive(Serialize)]
189pub(crate) struct GuardianReviewEventRequest {
190    pub(crate) event_type: &'static str,
191    pub(crate) event_params: GuardianReviewEventPayload,
192}
193
194#[derive(Clone, Copy, Debug, Serialize)]
195#[serde(rename_all = "snake_case")]
196pub enum GuardianReviewDecision {
197    Approved,
198    Denied,
199    Aborted,
200}
201
202#[derive(Clone, Copy, Debug, Serialize)]
203#[serde(rename_all = "snake_case")]
204pub enum GuardianReviewTerminalStatus {
205    Approved,
206    Denied,
207    Aborted,
208    TimedOut,
209    FailedClosed,
210}
211
212#[derive(Clone, Copy, Debug, Serialize)]
213#[serde(rename_all = "snake_case")]
214pub enum GuardianReviewFailureReason {
215    Timeout,
216    Cancelled,
217    PromptBuildError,
218    SessionError,
219    ParseError,
220}
221
222#[derive(Clone, Copy, Debug, Serialize)]
223#[serde(rename_all = "snake_case")]
224pub enum GuardianReviewSessionKind {
225    TrunkNew,
226    TrunkReused,
227    EphemeralForked,
228}
229
230#[derive(Clone, Copy, Debug, Serialize)]
231#[serde(rename_all = "snake_case")]
232pub enum GuardianApprovalRequestSource {
233    /// Approval requested directly by the main Codex turn.
234    MainTurn,
235    /// Approval requested by a delegated subagent and routed through the parent
236    /// session for guardian review.
237    DelegatedSubagent,
238}
239
240#[derive(Clone, Debug, Serialize)]
241#[serde(tag = "type", rename_all = "snake_case")]
242pub enum GuardianReviewedAction {
243    Shell {
244        sandbox_permissions: SandboxPermissions,
245        additional_permissions: Option<AdditionalPermissionProfile>,
246    },
247    UnifiedExec {
248        sandbox_permissions: SandboxPermissions,
249        additional_permissions: Option<AdditionalPermissionProfile>,
250        tty: bool,
251    },
252    Execve {
253        source: GuardianCommandSource,
254        program: String,
255        additional_permissions: Option<AdditionalPermissionProfile>,
256    },
257    ApplyPatch {},
258    NetworkAccess {
259        protocol: NetworkApprovalProtocol,
260        port: u16,
261    },
262    McpToolCall {
263        server: String,
264        tool_name: String,
265        connector_id: Option<String>,
266        connector_name: Option<String>,
267        tool_title: Option<String>,
268    },
269    RequestPermissions {},
270}
271
272#[derive(Clone, Serialize)]
273pub struct GuardianReviewEventParams {
274    pub thread_id: String,
275    pub turn_id: String,
276    pub review_id: String,
277    pub target_item_id: Option<String>,
278    pub approval_request_source: GuardianApprovalRequestSource,
279    pub reviewed_action: GuardianReviewedAction,
280    pub reviewed_action_truncated: bool,
281    pub decision: GuardianReviewDecision,
282    pub terminal_status: GuardianReviewTerminalStatus,
283    pub failure_reason: Option<GuardianReviewFailureReason>,
284    pub attempt_count: i64,
285    pub risk_level: Option<GuardianRiskLevel>,
286    pub user_authorization: Option<GuardianUserAuthorization>,
287    pub outcome: Option<GuardianAssessmentOutcome>,
288    pub guardian_thread_id: Option<String>,
289    pub guardian_session_kind: Option<GuardianReviewSessionKind>,
290    pub guardian_model: Option<String>,
291    pub guardian_reasoning_effort: Option<String>,
292    pub guardian_default_review_model_id: Option<String>,
293    pub guardian_catalog_contains_auto_review: Option<bool>,
294    pub guardian_review_model_overridden: Option<bool>,
295    pub guardian_review_model_override: Option<String>,
296    pub guardian_model_provider_id: Option<String>,
297    pub had_prior_review_context: Option<bool>,
298    pub review_timeout_ms: u64,
299    pub tool_call_count: Option<u64>,
300    pub time_to_first_token_ms: Option<u64>,
301    pub completion_latency_ms: Option<u64>,
302    pub started_at: u64,
303    pub completed_at: Option<u64>,
304    pub input_tokens: Option<i64>,
305    pub cached_input_tokens: Option<i64>,
306    pub cache_write_input_tokens: Option<i64>,
307    pub output_tokens: Option<i64>,
308    pub reasoning_output_tokens: Option<i64>,
309    pub total_tokens: Option<i64>,
310}
311
312pub struct GuardianReviewTrackContext {
313    thread_id: String,
314    turn_id: String,
315    review_id: String,
316    target_item_id: Option<String>,
317    approval_request_source: GuardianApprovalRequestSource,
318    reviewed_action: GuardianReviewedAction,
319    review_timeout_ms: u64,
320    pub started_at_ms: u64,
321    started_instant: Instant,
322}
323
324impl GuardianReviewTrackContext {
325    pub fn new(
326        thread_id: String,
327        turn_id: String,
328        review_id: String,
329        target_item_id: Option<String>,
330        approval_request_source: GuardianApprovalRequestSource,
331        reviewed_action: GuardianReviewedAction,
332        review_timeout_ms: u64,
333    ) -> Self {
334        Self {
335            thread_id,
336            turn_id,
337            review_id,
338            target_item_id,
339            approval_request_source,
340            reviewed_action,
341            review_timeout_ms,
342            started_at_ms: now_unix_millis(),
343            started_instant: Instant::now(),
344        }
345    }
346
347    pub(crate) fn event_params(
348        &self,
349        result: GuardianReviewAnalyticsResult,
350        completed_at_ms: u64,
351    ) -> GuardianReviewEventParams {
352        GuardianReviewEventParams {
353            thread_id: self.thread_id.clone(),
354            turn_id: self.turn_id.clone(),
355            review_id: self.review_id.clone(),
356            target_item_id: self.target_item_id.clone(),
357            approval_request_source: self.approval_request_source,
358            reviewed_action: self.reviewed_action.clone(),
359            reviewed_action_truncated: result.reviewed_action_truncated,
360            decision: result.decision,
361            terminal_status: result.terminal_status,
362            failure_reason: result.failure_reason,
363            attempt_count: result.attempt_count,
364            risk_level: result.risk_level,
365            user_authorization: result.user_authorization,
366            outcome: result.outcome,
367            guardian_thread_id: result.guardian_thread_id,
368            guardian_session_kind: result.guardian_session_kind,
369            guardian_model: result.guardian_model,
370            guardian_reasoning_effort: result.guardian_reasoning_effort,
371            guardian_default_review_model_id: result.guardian_default_review_model_id,
372            guardian_catalog_contains_auto_review: result.guardian_catalog_contains_auto_review,
373            guardian_review_model_overridden: result.guardian_review_model_overridden,
374            guardian_review_model_override: result.guardian_review_model_override,
375            guardian_model_provider_id: result.guardian_model_provider_id,
376            had_prior_review_context: result.had_prior_review_context,
377            review_timeout_ms: self.review_timeout_ms,
378            // TODO(rhan-oai): plumb nested Guardian review session tool-call counts.
379            tool_call_count: None,
380            time_to_first_token_ms: result.time_to_first_token_ms,
381            completion_latency_ms: Some(self.started_instant.elapsed().as_millis() as u64),
382            started_at: self.started_at_ms / 1_000,
383            completed_at: Some(completed_at_ms / 1_000),
384            input_tokens: result.token_usage.as_ref().map(|usage| usage.input_tokens),
385            cached_input_tokens: result
386                .token_usage
387                .as_ref()
388                .map(|usage| usage.cached_input_tokens),
389            cache_write_input_tokens: result
390                .token_usage
391                .as_ref()
392                .map(|usage| usage.cache_write_input_tokens),
393            output_tokens: result.token_usage.as_ref().map(|usage| usage.output_tokens),
394            reasoning_output_tokens: result
395                .token_usage
396                .as_ref()
397                .map(|usage| usage.reasoning_output_tokens),
398            total_tokens: result.token_usage.as_ref().map(|usage| usage.total_tokens),
399        }
400    }
401}
402
403#[derive(Debug)]
404pub struct GuardianReviewAnalyticsResult {
405    pub decision: GuardianReviewDecision,
406    pub terminal_status: GuardianReviewTerminalStatus,
407    pub failure_reason: Option<GuardianReviewFailureReason>,
408    pub attempt_count: i64,
409    pub risk_level: Option<GuardianRiskLevel>,
410    pub user_authorization: Option<GuardianUserAuthorization>,
411    pub outcome: Option<GuardianAssessmentOutcome>,
412    pub guardian_thread_id: Option<String>,
413    pub guardian_session_kind: Option<GuardianReviewSessionKind>,
414    pub guardian_model: Option<String>,
415    pub guardian_reasoning_effort: Option<String>,
416    pub guardian_default_review_model_id: Option<String>,
417    pub guardian_catalog_contains_auto_review: Option<bool>,
418    pub guardian_review_model_overridden: Option<bool>,
419    pub guardian_review_model_override: Option<String>,
420    pub guardian_model_provider_id: Option<String>,
421    pub had_prior_review_context: Option<bool>,
422    pub reviewed_action_truncated: bool,
423    pub token_usage: Option<TokenUsage>,
424    pub time_to_first_token_ms: Option<u64>,
425}
426
427impl GuardianReviewAnalyticsResult {
428    pub fn without_session() -> Self {
429        Self {
430            decision: GuardianReviewDecision::Denied,
431            terminal_status: GuardianReviewTerminalStatus::FailedClosed,
432            failure_reason: None,
433            attempt_count: 1,
434            risk_level: None,
435            user_authorization: None,
436            outcome: None,
437            guardian_thread_id: None,
438            guardian_session_kind: None,
439            guardian_model: None,
440            guardian_reasoning_effort: None,
441            guardian_default_review_model_id: None,
442            guardian_catalog_contains_auto_review: None,
443            guardian_review_model_overridden: None,
444            guardian_review_model_override: None,
445            guardian_model_provider_id: None,
446            had_prior_review_context: None,
447            reviewed_action_truncated: false,
448            token_usage: None,
449            time_to_first_token_ms: None,
450        }
451    }
452
453    pub fn from_session(params: GuardianReviewSessionAnalyticsParams) -> Self {
454        Self {
455            guardian_thread_id: Some(params.guardian_thread_id),
456            guardian_session_kind: Some(params.guardian_session_kind),
457            guardian_model: Some(params.guardian_model),
458            guardian_reasoning_effort: params.guardian_reasoning_effort,
459            guardian_default_review_model_id: Some(params.guardian_default_review_model_id),
460            guardian_catalog_contains_auto_review: Some(
461                params.guardian_catalog_contains_auto_review,
462            ),
463            guardian_review_model_overridden: Some(params.guardian_review_model_overridden),
464            guardian_review_model_override: params.guardian_review_model_override,
465            guardian_model_provider_id: Some(params.guardian_model_provider_id),
466            had_prior_review_context: Some(params.had_prior_review_context),
467            ..Self::without_session()
468        }
469    }
470}
471
472pub struct GuardianReviewSessionAnalyticsParams {
473    pub guardian_thread_id: String,
474    pub guardian_session_kind: GuardianReviewSessionKind,
475    pub guardian_model: String,
476    pub guardian_reasoning_effort: Option<String>,
477    pub guardian_default_review_model_id: String,
478    pub guardian_catalog_contains_auto_review: bool,
479    pub guardian_review_model_overridden: bool,
480    pub guardian_review_model_override: Option<String>,
481    pub guardian_model_provider_id: String,
482    pub had_prior_review_context: bool,
483}
484
485#[derive(Serialize)]
486pub(crate) struct GuardianReviewEventPayload {
487    pub(crate) session_id: String,
488    pub(crate) app_server_client: CodexAppServerClientMetadata,
489    pub(crate) runtime: CodexRuntimeMetadata,
490    #[serde(flatten)]
491    pub(crate) guardian_review: GuardianReviewEventParams,
492}
493
494#[allow(dead_code)]
495#[derive(Clone, Copy, Debug, Serialize)]
496#[serde(rename_all = "snake_case")]
497pub(crate) enum FinalApprovalOutcome {
498    Unknown,
499    NotNeeded,
500    ConfigAllowed,
501    PolicyForbidden,
502    GuardianApproved,
503    GuardianDenied,
504    GuardianAborted,
505    UserApproved,
506    UserApprovedForSession,
507    UserDenied,
508    UserAborted,
509}
510
511#[allow(dead_code)]
512#[derive(Clone, Copy, Debug, Serialize)]
513#[serde(rename_all = "snake_case")]
514pub(crate) enum ToolItemTerminalStatus {
515    Completed,
516    Failed,
517    Rejected,
518    Interrupted,
519}
520
521#[allow(dead_code)]
522#[derive(Clone, Copy, Debug, Serialize)]
523#[serde(rename_all = "snake_case")]
524pub(crate) enum ToolItemFailureKind {
525    ToolError,
526    ApprovalDenied,
527    ApprovalAborted,
528    SandboxDenied,
529    PolicyForbidden,
530}
531
532#[derive(Serialize)]
533pub(crate) struct CodexToolItemEventBase {
534    pub(crate) thread_id: String,
535    pub(crate) session_id: String,
536    pub(crate) turn_id: String,
537    /// App-server ThreadItem.id. For tool-originated items this generally
538    /// corresponds to the originating core call_id.
539    pub(crate) item_id: String,
540    pub(crate) app_server_client: CodexAppServerClientMetadata,
541    pub(crate) runtime: CodexRuntimeMetadata,
542    pub(crate) thread_source: Option<ThreadSource>,
543    pub(crate) subagent_source: Option<String>,
544    pub(crate) parent_thread_id: Option<String>,
545    pub(crate) tool_name: String,
546    pub(crate) started_at_ms: u64,
547    pub(crate) completed_at_ms: u64,
548    // Observed item lifecycle duration. This may undercount end-to-end execution
549    // for tools where app-server only sees part of the upstream flow.
550    pub(crate) duration_ms: Option<u64>,
551    pub(crate) execution_duration_ms: Option<u64>,
552    pub(crate) review_count: u64,
553    pub(crate) guardian_review_count: u64,
554    pub(crate) user_review_count: u64,
555    pub(crate) final_approval_outcome: FinalApprovalOutcome,
556    pub(crate) terminal_status: ToolItemTerminalStatus,
557    pub(crate) failure_kind: Option<ToolItemFailureKind>,
558    pub(crate) requested_additional_permissions: bool,
559    pub(crate) requested_network_access: bool,
560}
561
562#[allow(dead_code)]
563#[derive(Clone, Copy, Debug, Serialize)]
564#[serde(rename_all = "snake_case")]
565pub(crate) enum ReviewSubjectKind {
566    CommandExecution,
567    FileChange,
568    McpToolCall,
569    Permissions,
570    NetworkAccess,
571}
572
573#[allow(dead_code)]
574#[derive(Clone, Copy, Debug, Serialize)]
575#[serde(rename_all = "snake_case")]
576pub(crate) enum Reviewer {
577    Guardian,
578    User,
579}
580
581#[allow(dead_code)]
582#[derive(Clone, Copy, Debug, Serialize)]
583#[serde(rename_all = "snake_case")]
584pub(crate) enum ReviewTrigger {
585    Initial,
586    SandboxDenial,
587    NetworkPolicyDenial,
588    ExecveIntercept,
589}
590
591#[allow(dead_code)]
592#[derive(Clone, Copy, Debug, Serialize)]
593#[serde(rename_all = "snake_case")]
594pub(crate) enum ReviewStatus {
595    Approved,
596    Denied,
597    Aborted,
598    TimedOut,
599}
600
601#[allow(dead_code)]
602#[derive(Clone, Copy, Debug, Serialize)]
603#[serde(rename_all = "snake_case")]
604pub(crate) enum ReviewResolution {
605    None,
606    SessionApproval,
607    ExecPolicyAmendment,
608    NetworkPolicyAmendment,
609}
610
611#[derive(Serialize)]
612pub(crate) struct CodexReviewEventParams {
613    pub(crate) thread_id: String,
614    pub(crate) turn_id: String,
615    pub(crate) item_id: Option<String>,
616    pub(crate) review_id: String,
617    pub(crate) app_server_client: CodexAppServerClientMetadata,
618    pub(crate) runtime: CodexRuntimeMetadata,
619    pub(crate) thread_source: Option<ThreadSource>,
620    pub(crate) subagent_source: Option<String>,
621    pub(crate) parent_thread_id: Option<String>,
622    pub(crate) subject_kind: ReviewSubjectKind,
623    pub(crate) subject_name: String,
624    pub(crate) reviewer: Reviewer,
625    pub(crate) trigger: ReviewTrigger,
626    pub(crate) status: ReviewStatus,
627    pub(crate) resolution: ReviewResolution,
628    pub(crate) started_at_ms: u64,
629    pub(crate) completed_at_ms: u64,
630    pub(crate) duration_ms: Option<u64>,
631}
632
633#[derive(Serialize)]
634pub(crate) struct CodexReviewEventRequest {
635    pub(crate) event_type: &'static str,
636    pub(crate) event_params: CodexReviewEventParams,
637}
638#[allow(dead_code)]
639#[derive(Clone, Copy, Debug, Serialize)]
640#[serde(rename_all = "snake_case")]
641pub(crate) enum WebSearchActionKind {
642    Search,
643    OpenPage,
644    FindInPage,
645    Other,
646}
647
648#[derive(Serialize)]
649pub(crate) struct CodexCommandExecutionEventParams {
650    #[serde(flatten)]
651    pub(crate) base: CodexToolItemEventBase,
652    pub(crate) command_execution_source: CommandExecutionSource,
653    pub(crate) exit_code: Option<i32>,
654    pub(crate) command_total_action_count: u64,
655    pub(crate) command_read_action_count: u64,
656    pub(crate) command_list_files_action_count: u64,
657    pub(crate) command_search_action_count: u64,
658    pub(crate) command_unknown_action_count: u64,
659}
660
661#[derive(Serialize)]
662pub(crate) struct CodexCommandExecutionEventRequest {
663    pub(crate) event_type: &'static str,
664    pub(crate) event_params: CodexCommandExecutionEventParams,
665}
666
667#[derive(Serialize)]
668pub(crate) struct CodexFileChangeEventParams {
669    #[serde(flatten)]
670    pub(crate) base: CodexToolItemEventBase,
671    pub(crate) file_change_count: u64,
672    pub(crate) file_add_count: u64,
673    pub(crate) file_update_count: u64,
674    pub(crate) file_delete_count: u64,
675    pub(crate) file_move_count: u64,
676}
677
678#[derive(Serialize)]
679pub(crate) struct CodexFileChangeEventRequest {
680    pub(crate) event_type: &'static str,
681    pub(crate) event_params: CodexFileChangeEventParams,
682}
683
684#[derive(Serialize)]
685pub(crate) struct CodexMcpToolCallEventParams {
686    #[serde(flatten)]
687    pub(crate) base: CodexToolItemEventBase,
688    pub(crate) mcp_server_name: String,
689    pub(crate) mcp_tool_name: String,
690    pub(crate) mcp_error_present: bool,
691    pub(crate) plugin_id: Option<String>,
692    pub(crate) connector_id: Option<String>,
693}
694
695#[derive(Serialize)]
696pub(crate) struct CodexMcpToolCallEventRequest {
697    pub(crate) event_type: &'static str,
698    pub(crate) event_params: CodexMcpToolCallEventParams,
699}
700
701#[derive(Serialize)]
702pub(crate) struct CodexDynamicToolCallEventParams {
703    #[serde(flatten)]
704    pub(crate) base: CodexToolItemEventBase,
705    pub(crate) dynamic_tool_name: String,
706    pub(crate) success: Option<bool>,
707    pub(crate) output_content_item_count: Option<u64>,
708    pub(crate) output_text_item_count: Option<u64>,
709    pub(crate) output_image_item_count: Option<u64>,
710    pub(crate) output_audio_item_count: Option<u64>,
711}
712
713#[derive(Serialize)]
714pub(crate) struct CodexDynamicToolCallEventRequest {
715    pub(crate) event_type: &'static str,
716    pub(crate) event_params: CodexDynamicToolCallEventParams,
717}
718
719#[derive(Serialize)]
720pub(crate) struct CodexCollabAgentToolCallEventParams {
721    #[serde(flatten)]
722    pub(crate) base: CodexToolItemEventBase,
723    pub(crate) sender_thread_id: String,
724    pub(crate) receiver_thread_count: u64,
725    pub(crate) receiver_thread_ids: Option<Vec<String>>,
726    pub(crate) requested_model: Option<String>,
727    pub(crate) requested_reasoning_effort: Option<String>,
728    pub(crate) agent_state_count: Option<u64>,
729    pub(crate) completed_agent_count: Option<u64>,
730    pub(crate) failed_agent_count: Option<u64>,
731}
732
733#[derive(Serialize)]
734pub(crate) struct CodexCollabAgentToolCallEventRequest {
735    pub(crate) event_type: &'static str,
736    pub(crate) event_params: CodexCollabAgentToolCallEventParams,
737}
738
739#[derive(Serialize)]
740pub(crate) struct CodexWebSearchEventParams {
741    #[serde(flatten)]
742    pub(crate) base: CodexToolItemEventBase,
743    pub(crate) web_search_action: Option<WebSearchActionKind>,
744    pub(crate) query_present: bool,
745    pub(crate) query_count: Option<u64>,
746}
747
748#[derive(Serialize)]
749pub(crate) struct CodexWebSearchEventRequest {
750    pub(crate) event_type: &'static str,
751    pub(crate) event_params: CodexWebSearchEventParams,
752}
753
754#[derive(Serialize)]
755pub(crate) struct CodexImageGenerationEventParams {
756    #[serde(flatten)]
757    pub(crate) base: CodexToolItemEventBase,
758    pub(crate) revised_prompt_present: bool,
759    pub(crate) saved_path_present: bool,
760}
761
762#[derive(Serialize)]
763pub(crate) struct CodexImageGenerationEventRequest {
764    pub(crate) event_type: &'static str,
765    pub(crate) event_params: CodexImageGenerationEventParams,
766}
767
768#[derive(Serialize)]
769pub(crate) struct CodexAppMetadata {
770    pub(crate) connector_id: Option<String>,
771    pub(crate) thread_id: Option<String>,
772    pub(crate) turn_id: Option<String>,
773    pub(crate) app_name: Option<String>,
774    pub(crate) product_client_id: Option<String>,
775    pub(crate) invoke_type: Option<InvocationType>,
776    pub(crate) model_slug: Option<String>,
777}
778
779#[derive(Serialize)]
780pub(crate) struct CodexAppMentionedEventRequest {
781    pub(crate) event_type: &'static str,
782    pub(crate) event_params: CodexAppMetadata,
783}
784
785#[derive(Serialize)]
786pub(crate) struct CodexAppUsedEventRequest {
787    pub(crate) event_type: &'static str,
788    pub(crate) event_params: CodexAppMetadata,
789}
790
791#[derive(Serialize)]
792pub(crate) struct CodexHookRunMetadata {
793    pub(crate) thread_id: Option<String>,
794    pub(crate) turn_id: Option<String>,
795    pub(crate) product_client_id: Option<String>,
796    pub(crate) model_slug: Option<String>,
797    pub(crate) hook_name: Option<String>,
798    pub(crate) hook_source: Option<&'static str>,
799    pub(crate) status: Option<HookRunStatus>,
800}
801
802#[derive(Serialize)]
803pub(crate) struct CodexHookRunEventRequest {
804    pub(crate) event_type: &'static str,
805    pub(crate) event_params: CodexHookRunMetadata,
806}
807
808#[derive(Serialize)]
809pub(crate) struct CodexCompactionEventParams {
810    pub(crate) thread_id: String,
811    pub(crate) session_id: String,
812    pub(crate) turn_id: String,
813    pub(crate) app_server_client: CodexAppServerClientMetadata,
814    pub(crate) runtime: CodexRuntimeMetadata,
815    pub(crate) thread_source: Option<ThreadSource>,
816    pub(crate) subagent_source: Option<String>,
817    pub(crate) parent_thread_id: Option<String>,
818    pub(crate) trigger: CompactionTrigger,
819    pub(crate) reason: CompactionReason,
820    pub(crate) implementation: CompactionImplementation,
821    pub(crate) phase: CompactionPhase,
822    pub(crate) strategy: CompactionStrategy,
823    pub(crate) status: CompactionStatus,
824    pub(crate) codex_error_kind: Option<CodexErrKind>,
825    pub(crate) codex_error_http_status_code: Option<u16>,
826    pub(crate) active_context_tokens_before: i64,
827    pub(crate) active_context_tokens_after: i64,
828    pub(crate) retained_image_count: Option<usize>,
829    pub(crate) compaction_summary_tokens: Option<i64>,
830    pub(crate) cached_input_tokens: Option<i64>,
831    pub(crate) cache_write_input_tokens: Option<i64>,
832    pub(crate) started_at: u64,
833    pub(crate) completed_at: u64,
834    pub(crate) duration_ms: Option<u64>,
835}
836
837#[derive(Serialize)]
838pub(crate) struct CodexCompactionEventRequest {
839    pub(crate) event_type: &'static str,
840    pub(crate) event_params: CodexCompactionEventParams,
841}
842
843#[derive(Serialize)]
844pub(crate) struct CodexGoalEventParams {
845    pub(crate) thread_id: String,
846    pub(crate) session_id: String,
847    pub(crate) turn_id: Option<String>,
848    pub(crate) app_server_client: CodexAppServerClientMetadata,
849    pub(crate) runtime: CodexRuntimeMetadata,
850    pub(crate) thread_source: Option<ThreadSource>,
851    pub(crate) subagent_source: Option<String>,
852    pub(crate) parent_thread_id: Option<String>,
853    pub(crate) goal_id: String,
854    pub(crate) event_kind: GoalEventKind,
855    pub(crate) goal_status: codex_state::ThreadGoalStatus,
856    pub(crate) has_token_budget: bool,
857    pub(crate) cumulative_tokens_accounted: Option<i64>,
858    pub(crate) cumulative_time_accounted_seconds: Option<i64>,
859}
860
861#[derive(Serialize)]
862pub(crate) struct CodexGoalEventRequest {
863    pub(crate) event_type: &'static str,
864    pub(crate) event_params: CodexGoalEventParams,
865}
866
867#[derive(Serialize)]
868pub(crate) struct CodexTurnEventParams {
869    pub(crate) thread_id: String,
870    pub(crate) session_id: String,
871    pub(crate) turn_id: String,
872    // TODO(rhan-oai): Populate once queued/default submission type is plumbed from
873    // the turn/start callsites instead of always being reported as None.
874    pub(crate) submission_type: Option<TurnSubmissionType>,
875    pub(crate) app_server_client: CodexAppServerClientMetadata,
876    pub(crate) runtime: CodexRuntimeMetadata,
877    pub(crate) ephemeral: bool,
878    pub(crate) thread_source: Option<ThreadSource>,
879    pub(crate) initialization_mode: ThreadInitializationMode,
880    pub(crate) subagent_source: Option<String>,
881    pub(crate) parent_thread_id: Option<String>,
882    pub(crate) model: Option<String>,
883    pub(crate) model_provider: String,
884    pub(crate) sandbox_policy: Option<&'static str>,
885    pub(crate) reasoning_effort: Option<String>,
886    pub(crate) reasoning_summary: Option<String>,
887    pub(crate) service_tier: String,
888    pub(crate) approval_policy: String,
889    pub(crate) approvals_reviewer: String,
890    pub(crate) sandbox_network_access: bool,
891    pub(crate) collaboration_mode: Option<&'static str>,
892    pub(crate) personality: Option<String>,
893    pub(crate) workspace_kind: Option<String>,
894    pub(crate) num_input_images: usize,
895    pub(crate) is_first_turn: bool,
896    pub(crate) status: Option<TurnStatus>,
897    pub(crate) turn_error: Option<CodexErrorInfo>,
898    pub(crate) codex_error_kind: Option<CodexErrKind>,
899    pub(crate) codex_error_http_status_code: Option<u16>,
900    pub(crate) steer_count: Option<usize>,
901    pub(crate) total_tool_call_count: Option<usize>,
902    pub(crate) shell_command_count: Option<usize>,
903    pub(crate) file_change_count: Option<usize>,
904    pub(crate) mcp_tool_call_count: Option<usize>,
905    pub(crate) dynamic_tool_call_count: Option<usize>,
906    pub(crate) subagent_tool_call_count: Option<usize>,
907    pub(crate) web_search_count: Option<usize>,
908    pub(crate) image_generation_count: Option<usize>,
909    pub(crate) input_tokens: Option<i64>,
910    pub(crate) cached_input_tokens: Option<i64>,
911    pub(crate) cache_write_input_tokens: Option<i64>,
912    pub(crate) output_tokens: Option<i64>,
913    pub(crate) reasoning_output_tokens: Option<i64>,
914    pub(crate) total_tokens: Option<i64>,
915    pub(crate) before_first_sampling_ms: u64,
916    pub(crate) sampling_ms: u64,
917    pub(crate) between_sampling_overhead_ms: u64,
918    pub(crate) tool_blocking_ms: u64,
919    pub(crate) after_last_sampling_ms: u64,
920    pub(crate) sampling_request_count: u32,
921    pub(crate) sampling_retry_count: u32,
922    pub(crate) duration_ms: Option<u64>,
923    pub(crate) started_at: Option<u64>,
924    pub(crate) completed_at: Option<u64>,
925}
926
927#[derive(Serialize)]
928pub(crate) struct CodexTurnEventRequest {
929    pub(crate) event_type: &'static str,
930    pub(crate) event_params: CodexTurnEventParams,
931}
932
933#[derive(Serialize)]
934pub(crate) struct CodexTurnSteerEventParams {
935    pub(crate) thread_id: String,
936    pub(crate) session_id: String,
937    pub(crate) expected_turn_id: Option<String>,
938    pub(crate) accepted_turn_id: Option<String>,
939    pub(crate) app_server_client: CodexAppServerClientMetadata,
940    pub(crate) runtime: CodexRuntimeMetadata,
941    pub(crate) thread_source: Option<ThreadSource>,
942    pub(crate) subagent_source: Option<String>,
943    pub(crate) parent_thread_id: Option<String>,
944    pub(crate) num_input_images: usize,
945    pub(crate) result: TurnSteerResult,
946    pub(crate) rejection_reason: Option<TurnSteerRejectionReason>,
947    pub(crate) created_at: u64,
948}
949
950#[derive(Serialize)]
951pub(crate) struct CodexTurnSteerEventRequest {
952    pub(crate) event_type: &'static str,
953    pub(crate) event_params: CodexTurnSteerEventParams,
954}
955
956#[derive(Serialize)]
957pub(crate) struct CodexPluginMetadata {
958    pub(crate) plugin_id: Option<String>,
959    pub(crate) remote_plugin_id: Option<String>,
960    pub(crate) plugin_name: Option<String>,
961    pub(crate) marketplace_name: Option<String>,
962    pub(crate) has_skills: Option<bool>,
963    pub(crate) mcp_server_count: Option<usize>,
964    pub(crate) connector_ids: Option<Vec<String>>,
965    pub(crate) product_client_id: Option<String>,
966}
967
968#[derive(Serialize)]
969pub(crate) struct CodexPluginUsedMetadata {
970    #[serde(flatten)]
971    pub(crate) plugin: CodexPluginMetadata,
972    pub(crate) mcp_server_names: Option<Vec<String>>,
973    pub(crate) thread_id: Option<String>,
974    pub(crate) turn_id: Option<String>,
975    pub(crate) model_slug: Option<String>,
976}
977
978#[derive(Serialize)]
979pub(crate) struct CodexPluginInstallRequestedPluginMetadata {
980    pub(crate) plugin_id: String,
981    pub(crate) remote_plugin_id: Option<String>,
982    pub(crate) plugin_name: String,
983    pub(crate) connector_ids: Vec<String>,
984}
985
986#[derive(Serialize)]
987pub(crate) struct CodexPluginInstallRequestedMetadata {
988    pub(crate) suggestion_id: String,
989    pub(crate) plugins: Vec<CodexPluginInstallRequestedPluginMetadata>,
990    pub(crate) source: crate::facts::PluginInstallRequestSource,
991    pub(crate) thread_id: String,
992    pub(crate) turn_id: String,
993    pub(crate) model_slug: String,
994    pub(crate) product_client_id: Option<String>,
995}
996
997#[derive(Serialize)]
998pub(crate) struct CodexPluginInstallRequestedEventRequest {
999    pub(crate) event_type: &'static str,
1000    pub(crate) event_params: CodexPluginInstallRequestedMetadata,
1001}
1002
1003#[derive(Serialize)]
1004pub(crate) struct CodexPluginEventRequest {
1005    pub(crate) event_type: &'static str,
1006    pub(crate) event_params: CodexPluginMetadata,
1007}
1008
1009#[derive(Serialize)]
1010pub(crate) struct CodexPluginInstallFailedMetadata {
1011    #[serde(flatten)]
1012    pub(crate) plugin: CodexPluginMetadata,
1013    pub(crate) source: crate::facts::PluginInstallSource,
1014    pub(crate) error_type: String,
1015    pub(crate) sub_error_type: Option<String>,
1016}
1017
1018#[derive(Serialize)]
1019pub(crate) struct CodexPluginInstallFailedEventRequest {
1020    pub(crate) event_type: &'static str,
1021    pub(crate) event_params: CodexPluginInstallFailedMetadata,
1022}
1023
1024#[derive(Serialize)]
1025pub(crate) struct CodexOnboardingExternalAgentImportCompleteMetadata {
1026    pub(crate) import_id: String,
1027    pub(crate) source: String,
1028    pub(crate) provider_id: String,
1029    #[serde(rename = "type")]
1030    pub(crate) item_type: String,
1031    pub(crate) success_count: usize,
1032    pub(crate) failed_count: usize,
1033    pub(crate) product_client_id: Option<String>,
1034}
1035
1036#[derive(Serialize)]
1037pub(crate) struct CodexOnboardingExternalAgentImportCompleteEventRequest {
1038    pub(crate) event_type: &'static str,
1039    pub(crate) event_params: CodexOnboardingExternalAgentImportCompleteMetadata,
1040}
1041
1042#[derive(Serialize)]
1043pub(crate) struct CodexOnboardingExternalAgentImportFailureMetadata {
1044    pub(crate) import_id: String,
1045    pub(crate) source: String,
1046    pub(crate) provider_id: String,
1047    #[serde(rename = "type")]
1048    pub(crate) item_type: String,
1049    pub(crate) failure_stage: String,
1050    pub(crate) error_type: String,
1051    pub(crate) sub_error_type: Option<String>,
1052    pub(crate) product_client_id: Option<String>,
1053}
1054
1055#[derive(Serialize)]
1056pub(crate) struct CodexOnboardingExternalAgentImportFailureEventRequest {
1057    pub(crate) event_type: &'static str,
1058    pub(crate) event_params: CodexOnboardingExternalAgentImportFailureMetadata,
1059}
1060
1061#[derive(Serialize)]
1062pub(crate) struct CodexPluginUsedEventRequest {
1063    pub(crate) event_type: &'static str,
1064    pub(crate) event_params: CodexPluginUsedMetadata,
1065}
1066
1067pub(crate) fn plugin_state_event_type(state: PluginState) -> &'static str {
1068    match state {
1069        PluginState::Installed => "codex_plugin_installed",
1070        PluginState::Uninstalled => "codex_plugin_uninstalled",
1071        PluginState::Enabled => "codex_plugin_enabled",
1072        PluginState::Disabled => "codex_plugin_disabled",
1073    }
1074}
1075
1076pub(crate) fn codex_app_metadata(
1077    tracking: &TrackEventsContext,
1078    app: AppInvocation,
1079) -> CodexAppMetadata {
1080    CodexAppMetadata {
1081        connector_id: app.connector_id,
1082        thread_id: Some(tracking.thread_id.clone()),
1083        turn_id: Some(tracking.turn_id.clone()),
1084        app_name: app.app_name,
1085        product_client_id: Some(tracking.product_client_id.clone()),
1086        invoke_type: app.invocation_type,
1087        model_slug: Some(tracking.model_slug.clone()),
1088    }
1089}
1090
1091pub(crate) fn codex_plugin_metadata(plugin: PluginTelemetryMetadata) -> CodexPluginMetadata {
1092    codex_plugin_metadata_with_product_client_id(plugin, originator().value)
1093}
1094
1095fn codex_plugin_metadata_with_product_client_id(
1096    plugin: PluginTelemetryMetadata,
1097    product_client_id: String,
1098) -> CodexPluginMetadata {
1099    let PluginTelemetryMetadata {
1100        plugin_id,
1101        remote_plugin_id,
1102        capability_summary,
1103    } = plugin;
1104    CodexPluginMetadata {
1105        plugin_id: plugin_id.as_ref().map(PluginId::as_key),
1106        remote_plugin_id,
1107        plugin_name: plugin_id
1108            .as_ref()
1109            .map(|plugin_id| plugin_id.plugin_name.clone()),
1110        marketplace_name: plugin_id.map(|plugin_id| plugin_id.marketplace_name),
1111        has_skills: capability_summary
1112            .as_ref()
1113            .map(|summary| summary.has_skills),
1114        mcp_server_count: capability_summary
1115            .as_ref()
1116            .map(|summary| summary.mcp_server_names.len()),
1117        connector_ids: capability_summary.map(|summary| {
1118            summary
1119                .app_connector_ids
1120                .into_iter()
1121                .map(|connector_id| connector_id.0)
1122                .collect()
1123        }),
1124        product_client_id: Some(product_client_id),
1125    }
1126}
1127
1128pub(crate) fn codex_plugin_install_requested_metadata(
1129    tracking: &TrackEventsContext,
1130    request: PluginInstallRequested,
1131) -> CodexPluginInstallRequestedMetadata {
1132    CodexPluginInstallRequestedMetadata {
1133        suggestion_id: request.suggestion_id,
1134        plugins: request
1135            .plugins
1136            .into_iter()
1137            .map(|plugin| CodexPluginInstallRequestedPluginMetadata {
1138                plugin_id: plugin.plugin_id,
1139                remote_plugin_id: plugin.remote_plugin_id,
1140                plugin_name: plugin.plugin_name,
1141                connector_ids: plugin.connector_ids,
1142            })
1143            .collect(),
1144        source: request.source,
1145        thread_id: tracking.thread_id.clone(),
1146        turn_id: tracking.turn_id.clone(),
1147        model_slug: tracking.model_slug.clone(),
1148        product_client_id: Some(originator().value),
1149    }
1150}
1151
1152pub(crate) fn codex_compaction_event_params(
1153    input: CodexCompactionEvent,
1154    session_id: String,
1155    app_server_client: CodexAppServerClientMetadata,
1156    runtime: CodexRuntimeMetadata,
1157    thread_source: Option<ThreadSource>,
1158    subagent_source: Option<String>,
1159    parent_thread_id: Option<String>,
1160) -> CodexCompactionEventParams {
1161    CodexCompactionEventParams {
1162        thread_id: input.thread_id,
1163        session_id,
1164        turn_id: input.turn_id,
1165        app_server_client,
1166        runtime,
1167        thread_source,
1168        subagent_source,
1169        parent_thread_id,
1170        trigger: input.trigger,
1171        reason: input.reason,
1172        implementation: input.implementation,
1173        phase: input.phase,
1174        strategy: input.strategy,
1175        status: input.status,
1176        codex_error_kind: input.codex_error_kind,
1177        codex_error_http_status_code: input.codex_error_http_status_code,
1178        active_context_tokens_before: input.active_context_tokens_before,
1179        active_context_tokens_after: input.active_context_tokens_after,
1180        retained_image_count: input.retained_image_count,
1181        compaction_summary_tokens: input.compaction_summary_tokens,
1182        cached_input_tokens: input.cached_input_tokens,
1183        cache_write_input_tokens: input.cache_write_input_tokens,
1184        started_at: input.started_at,
1185        completed_at: input.completed_at,
1186        duration_ms: input.duration_ms,
1187    }
1188}
1189
1190pub(crate) fn codex_goal_event_params(
1191    input: CodexGoalEvent,
1192    session_id: String,
1193    app_server_client: CodexAppServerClientMetadata,
1194    runtime: CodexRuntimeMetadata,
1195    thread_source: Option<ThreadSource>,
1196    subagent_source: Option<String>,
1197    parent_thread_id: Option<String>,
1198) -> CodexGoalEventParams {
1199    CodexGoalEventParams {
1200        thread_id: input.thread_id,
1201        session_id,
1202        turn_id: input.turn_id,
1203        app_server_client,
1204        runtime,
1205        thread_source,
1206        subagent_source,
1207        parent_thread_id,
1208        goal_id: input.goal_id,
1209        event_kind: input.event_kind,
1210        goal_status: input.goal_status,
1211        has_token_budget: input.has_token_budget,
1212        cumulative_tokens_accounted: input.cumulative_tokens_accounted,
1213        cumulative_time_accounted_seconds: input.cumulative_time_accounted_seconds,
1214    }
1215}
1216
1217pub(crate) fn codex_plugin_used_metadata(
1218    tracking: &TrackEventsContext,
1219    plugin: PluginTelemetryMetadata,
1220) -> CodexPluginUsedMetadata {
1221    let mcp_server_names = plugin
1222        .capability_summary
1223        .as_ref()
1224        .map(|summary| summary.mcp_server_names.clone());
1225    CodexPluginUsedMetadata {
1226        plugin: codex_plugin_metadata_with_product_client_id(
1227            plugin,
1228            tracking.product_client_id.clone(),
1229        ),
1230        mcp_server_names,
1231        thread_id: Some(tracking.thread_id.clone()),
1232        turn_id: Some(tracking.turn_id.clone()),
1233        model_slug: Some(tracking.model_slug.clone()),
1234    }
1235}
1236
1237pub(crate) fn codex_hook_run_metadata(
1238    tracking: &TrackEventsContext,
1239    hook: HookRunFact,
1240) -> CodexHookRunMetadata {
1241    CodexHookRunMetadata {
1242        thread_id: Some(tracking.thread_id.clone()),
1243        turn_id: Some(tracking.turn_id.clone()),
1244        product_client_id: Some(tracking.product_client_id.clone()),
1245        model_slug: Some(tracking.model_slug.clone()),
1246        hook_name: Some(analytics_hook_event_name(hook.event_name).to_owned()),
1247        hook_source: Some(analytics_hook_source(hook.hook_source)),
1248        status: Some(analytics_hook_status(hook.status)),
1249    }
1250}
1251
1252fn analytics_hook_event_name(event_name: HookEventName) -> &'static str {
1253    match event_name {
1254        HookEventName::PreToolUse => "PreToolUse",
1255        HookEventName::PermissionRequest => "PermissionRequest",
1256        HookEventName::PostToolUse => "PostToolUse",
1257        HookEventName::PreCompact => "PreCompact",
1258        HookEventName::PostCompact => "PostCompact",
1259        HookEventName::SessionStart => "SessionStart",
1260        HookEventName::SessionEnd => "SessionEnd",
1261        HookEventName::UserPromptSubmit => "UserPromptSubmit",
1262        HookEventName::SubagentStart => "SubagentStart",
1263        HookEventName::SubagentStop => "SubagentStop",
1264        HookEventName::Stop => "Stop",
1265    }
1266}
1267
1268fn analytics_hook_source(source: HookSource) -> &'static str {
1269    match source {
1270        HookSource::System => "system",
1271        HookSource::User => "user",
1272        HookSource::Project => "project",
1273        HookSource::Mdm => "mdm",
1274        HookSource::SessionFlags => "session_flags",
1275        HookSource::Plugin => "plugin",
1276        HookSource::CloudRequirements => "cloud_requirements",
1277        HookSource::CloudManagedConfig => "cloud_managed_config",
1278        HookSource::LegacyManagedConfigFile => "legacy_managed_config_file",
1279        HookSource::LegacyManagedConfigMdm => "legacy_managed_config_mdm",
1280        HookSource::Unknown => "unknown",
1281    }
1282}
1283
1284pub(crate) fn current_runtime_metadata() -> CodexRuntimeMetadata {
1285    let os_info = os_info::get();
1286    CodexRuntimeMetadata {
1287        codex_rs_version: env!("CARGO_PKG_VERSION").to_string(),
1288        runtime_os: std::env::consts::OS.to_string(),
1289        runtime_os_version: os_info.version().to_string(),
1290        runtime_arch: std::env::consts::ARCH.to_string(),
1291    }
1292}
1293
1294pub(crate) fn subagent_thread_started_event_request(
1295    input: SubAgentThreadStartedInput,
1296) -> ThreadInitializedEvent {
1297    let event_params = ThreadInitializedEventParams {
1298        thread_id: input.thread_id,
1299        session_id: input.session_id,
1300        app_server_client: CodexAppServerClientMetadata {
1301            product_client_id: input.product_client_id,
1302            client_name: Some(input.client_name),
1303            client_version: Some(input.client_version),
1304            rpc_transport: AppServerRpcTransport::InProcess,
1305            experimental_api_enabled: None,
1306        },
1307        runtime: current_runtime_metadata(),
1308        model: input.model,
1309        ephemeral: input.ephemeral,
1310        thread_source: Some(ThreadSource::Subagent),
1311        initialization_mode: ThreadInitializationMode::New,
1312        subagent_source: Some(subagent_source_name(&input.subagent_source)),
1313        parent_thread_id: input.parent_thread_id,
1314        forked_from_thread_id: input.forked_from_thread_id,
1315        created_at: input.created_at,
1316    };
1317    ThreadInitializedEvent {
1318        event_type: "codex_thread_initialized",
1319        event_params,
1320    }
1321}
1322
1323pub(crate) fn subagent_source_name(subagent_source: &SubAgentSource) -> String {
1324    subagent_source.kind().to_string()
1325}
1326
1327fn analytics_hook_status(status: HookRunStatus) -> HookRunStatus {
1328    match status {
1329        // Running is unexpected here and normalized defensively.
1330        HookRunStatus::Running => HookRunStatus::Failed,
1331        other => other,
1332    }
1333}