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 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}