diff --git a/codex-rs/analytics/src/analytics_client_tests.rs b/codex-rs/analytics/src/analytics_client_tests.rs index 7177952c37..2da8848e6e 100644 --- a/codex-rs/analytics/src/analytics_client_tests.rs +++ b/codex-rs/analytics/src/analytics_client_tests.rs @@ -2918,6 +2918,7 @@ fn hook_run_event_serializes_expected_shape() { event_name: HookEventName::PreToolUse, hook_source: HookSource::User, status: HookRunStatus::Completed, + duration_ms: Some(123), }, ), }); @@ -2934,7 +2935,8 @@ fn hook_run_event_serializes_expected_shape() { "model_slug": "gpt-5", "hook_name": "PreToolUse", "hook_source": "user", - "status": "completed" + "status": "completed", + "duration_ms": 123 } }) ); @@ -2954,6 +2956,7 @@ fn hook_run_metadata_maps_sources_and_statuses() { event_name: HookEventName::SessionStart, hook_source: HookSource::System, status: HookRunStatus::Completed, + duration_ms: None, }, )) .expect("serialize system hook"); @@ -2963,6 +2966,7 @@ fn hook_run_metadata_maps_sources_and_statuses() { event_name: HookEventName::Stop, hook_source: HookSource::Project, status: HookRunStatus::Blocked, + duration_ms: None, }, )) .expect("serialize project hook"); @@ -2972,6 +2976,7 @@ fn hook_run_metadata_maps_sources_and_statuses() { event_name: HookEventName::Stop, hook_source: HookSource::CloudRequirements, status: HookRunStatus::Blocked, + duration_ms: None, }, )) .expect("serialize cloud requirements hook"); @@ -2981,6 +2986,7 @@ fn hook_run_metadata_maps_sources_and_statuses() { event_name: HookEventName::UserPromptSubmit, hook_source: HookSource::Unknown, status: HookRunStatus::Failed, + duration_ms: None, }, )) .expect("serialize unknown hook"); @@ -3009,6 +3015,7 @@ fn hook_run_metadata_maps_stopped_status() { event_name: HookEventName::Stop, hook_source: HookSource::User, status: HookRunStatus::Stopped, + duration_ms: None, }, )) .expect("serialize stopped hook"); @@ -3149,6 +3156,7 @@ async fn reducer_ingests_hook_run_fact() { event_name: HookEventName::PostToolUse, hook_source: HookSource::Unknown, status: HookRunStatus::Failed, + duration_ms: None, }, })), &mut events, diff --git a/codex-rs/analytics/src/events.rs b/codex-rs/analytics/src/events.rs index bdc2c996dc..71edef37b0 100644 --- a/codex-rs/analytics/src/events.rs +++ b/codex-rs/analytics/src/events.rs @@ -731,6 +731,7 @@ pub(crate) struct CodexHookRunMetadata { pub(crate) hook_name: Option, pub(crate) hook_source: Option<&'static str>, pub(crate) status: Option, + pub(crate) duration_ms: Option, } #[derive(Serialize)] @@ -997,6 +998,7 @@ pub(crate) fn codex_hook_run_metadata( hook_name: Some(analytics_hook_event_name(hook.event_name).to_owned()), hook_source: Some(analytics_hook_source(hook.hook_source)), status: Some(analytics_hook_status(hook.status)), + duration_ms: hook.duration_ms, } } diff --git a/codex-rs/analytics/src/facts.rs b/codex-rs/analytics/src/facts.rs index 1dff5de89d..28de73c772 100644 --- a/codex-rs/analytics/src/facts.rs +++ b/codex-rs/analytics/src/facts.rs @@ -509,6 +509,7 @@ pub struct HookRunFact { pub event_name: HookEventName, pub hook_source: HookSource, pub status: HookRunStatus, + pub duration_ms: Option, } pub(crate) struct PluginUsedInput { diff --git a/codex-rs/core/src/hook_runtime.rs b/codex-rs/core/src/hook_runtime.rs index 6e2d4fe33c..ae5c44343d 100644 --- a/codex-rs/core/src/hook_runtime.rs +++ b/codex-rs/core/src/hook_runtime.rs @@ -679,6 +679,7 @@ fn hook_run_analytics_payload( event_name: completed.run.event_name, hook_source: completed.run.source, status: completed.run.status, + duration_ms: completed.run.duration_ms, }, ) } @@ -834,6 +835,7 @@ mod tests { assert_eq!(hook.event_name, HookEventName::Stop); assert_eq!(hook.hook_source, HookSource::Project); assert_eq!(hook.status, HookRunStatus::Blocked); + assert_eq!(hook.duration_ms, Some(27)); } #[tokio::test]