diff --git a/codex-rs/analytics/src/analytics_client_tests.rs b/codex-rs/analytics/src/analytics_client_tests.rs index eb46674156d..f57941670f0 100644 --- a/codex-rs/analytics/src/analytics_client_tests.rs +++ b/codex-rs/analytics/src/analytics_client_tests.rs @@ -4,6 +4,7 @@ use crate::events::CodexAppMentionedEventRequest; use crate::events::CodexAppServerClientMetadata; use crate::events::CodexAppUsedEventRequest; use crate::events::CodexCompactionEventRequest; +use crate::events::CodexHookRunEventRequest; use crate::events::CodexPluginEventRequest; use crate::events::CodexPluginUsedEventRequest; use crate::events::CodexRuntimeMetadata; @@ -12,6 +13,7 @@ use crate::events::ThreadInitializedEvent; use crate::events::ThreadInitializedEventParams; use crate::events::TrackEventRequest; use crate::events::codex_app_metadata; +use crate::events::codex_hook_run_metadata; use crate::events::codex_plugin_metadata; use crate::events::codex_plugin_used_metadata; use crate::events::subagent_thread_started_event_request; @@ -21,6 +23,7 @@ use crate::facts::AppInvocation; use crate::facts::AppMentionedInput; use crate::facts::AppUsedInput; use crate::facts::CodexCompactionEvent; +use crate::facts::CodexHookSource; use crate::facts::CompactionImplementation; use crate::facts::CompactionPhase; use crate::facts::CompactionReason; @@ -28,6 +31,8 @@ use crate::facts::CompactionStatus; use crate::facts::CompactionStrategy; use crate::facts::CompactionTrigger; use crate::facts::CustomAnalyticsFact; +use crate::facts::HookRunFact; +use crate::facts::HookRunInput; use crate::facts::InputError; use crate::facts::InvocationType; use crate::facts::PluginState; @@ -81,6 +86,8 @@ use codex_plugin::PluginTelemetryMetadata; use codex_protocol::config_types::ApprovalsReviewer; use codex_protocol::config_types::ModeKind; use codex_protocol::protocol::AskForApproval; +use codex_protocol::protocol::HookEventName; +use codex_protocol::protocol::HookRunStatus; use codex_protocol::protocol::SandboxPolicy; use codex_protocol::protocol::SessionSource; use codex_protocol::protocol::SubAgentSource; @@ -1282,6 +1289,109 @@ fn plugin_management_event_serializes_expected_shape() { ); } +#[test] +fn hook_run_event_serializes_expected_shape() { + let tracking = TrackEventsContext { + model_slug: "gpt-5".to_string(), + thread_id: "thread-3".to_string(), + turn_id: "turn-3".to_string(), + }; + let event = TrackEventRequest::HookRun(CodexHookRunEventRequest { + event_type: "codex_hook_run", + event_params: codex_hook_run_metadata( + &tracking, + HookRunFact { + event_name: HookEventName::PreToolUse, + hook_source: CodexHookSource::User, + status: HookRunStatus::Completed, + }, + ), + }); + + let payload = serde_json::to_value(&event).expect("serialize hook run event"); + + assert_eq!( + payload, + json!({ + "event_type": "codex_hook_run", + "event_params": { + "thread_id": "thread-3", + "turn_id": "turn-3", + "model_slug": "gpt-5", + "hook_name": "PreToolUse", + "hook_source": "user", + "status": "completed" + } + }) + ); +} + +#[test] +fn hook_run_metadata_maps_sources_and_statuses() { + let tracking = TrackEventsContext { + model_slug: "gpt-5".to_string(), + thread_id: "thread-1".to_string(), + turn_id: "turn-1".to_string(), + }; + + let system = serde_json::to_value(codex_hook_run_metadata( + &tracking, + HookRunFact { + event_name: HookEventName::SessionStart, + hook_source: CodexHookSource::System, + status: HookRunStatus::Completed, + }, + )) + .expect("serialize system hook"); + let project = serde_json::to_value(codex_hook_run_metadata( + &tracking, + HookRunFact { + event_name: HookEventName::Stop, + hook_source: CodexHookSource::Project, + status: HookRunStatus::Blocked, + }, + )) + .expect("serialize project hook"); + let unknown = serde_json::to_value(codex_hook_run_metadata( + &tracking, + HookRunFact { + event_name: HookEventName::UserPromptSubmit, + hook_source: CodexHookSource::Unknown, + status: HookRunStatus::Failed, + }, + )) + .expect("serialize unknown hook"); + + assert_eq!(system["hook_source"], "system"); + assert_eq!(system["status"], "completed"); + assert_eq!(project["hook_source"], "project"); + assert_eq!(project["status"], "blocked"); + assert_eq!(unknown["hook_source"], "unknown"); + assert_eq!(unknown["status"], "failed"); +} + +#[test] +fn hook_run_metadata_maps_stopped_status() { + let tracking = TrackEventsContext { + model_slug: "gpt-5".to_string(), + thread_id: "thread-1".to_string(), + turn_id: "turn-1".to_string(), + }; + + let stopped = serde_json::to_value(codex_hook_run_metadata( + &tracking, + HookRunFact { + event_name: HookEventName::Stop, + hook_source: CodexHookSource::User, + status: HookRunStatus::Stopped, + }, + )) + .expect("serialize stopped hook"); + + assert_eq!(stopped["hook_source"], "user"); + assert_eq!(stopped["status"], "stopped"); +} + #[test] fn plugin_used_dedupe_is_keyed_by_turn_and_plugin() { let (sender, _receiver) = mpsc::channel(1); @@ -1359,6 +1469,37 @@ async fn reducer_ingests_skill_invoked_fact() { ); } +#[tokio::test] +async fn reducer_ingests_hook_run_fact() { + let mut reducer = AnalyticsReducer::default(); + let mut events = Vec::new(); + + reducer + .ingest( + AnalyticsFact::Custom(CustomAnalyticsFact::HookRun(HookRunInput { + tracking: TrackEventsContext { + model_slug: "gpt-5".to_string(), + thread_id: "thread-1".to_string(), + turn_id: "turn-1".to_string(), + }, + hook: HookRunFact { + event_name: HookEventName::PostToolUse, + hook_source: CodexHookSource::Unknown, + status: HookRunStatus::Failed, + }, + })), + &mut events, + ) + .await; + + let payload = serde_json::to_value(&events).expect("serialize events"); + assert_eq!(payload.as_array().expect("events array").len(), 1); + assert_eq!(payload[0]["event_type"], "codex_hook_run"); + assert_eq!(payload[0]["event_params"]["hook_name"], "PostToolUse"); + assert_eq!(payload[0]["event_params"]["hook_source"], "unknown"); + assert_eq!(payload[0]["event_params"]["status"], "failed"); +} + #[tokio::test] async fn reducer_ingests_app_and_plugin_facts() { let mut reducer = AnalyticsReducer::default(); diff --git a/codex-rs/analytics/src/client.rs b/codex-rs/analytics/src/client.rs index 5ba60e3ef44..1a4b5defe9c 100644 --- a/codex-rs/analytics/src/client.rs +++ b/codex-rs/analytics/src/client.rs @@ -9,6 +9,8 @@ use crate::facts::AppInvocation; use crate::facts::AppMentionedInput; use crate::facts::AppUsedInput; use crate::facts::CustomAnalyticsFact; +use crate::facts::HookRunFact; +use crate::facts::HookRunInput; use crate::facts::PluginState; use crate::facts::PluginStateChangedInput; use crate::facts::SkillInvocation; @@ -191,6 +193,12 @@ impl AnalyticsEventsClient { ))); } + pub fn track_hook_run(&self, tracking: TrackEventsContext, hook: HookRunFact) { + self.record_fact(AnalyticsFact::Custom(CustomAnalyticsFact::HookRun( + HookRunInput { tracking, hook }, + ))); + } + pub fn track_plugin_used(&self, tracking: TrackEventsContext, plugin: PluginTelemetryMetadata) { if !self.queue.should_enqueue_plugin_used(&tracking, &plugin) { return; diff --git a/codex-rs/analytics/src/events.rs b/codex-rs/analytics/src/events.rs index eb312da1405..eabf1cbfafc 100644 --- a/codex-rs/analytics/src/events.rs +++ b/codex-rs/analytics/src/events.rs @@ -1,5 +1,7 @@ use crate::facts::AppInvocation; use crate::facts::CodexCompactionEvent; +use crate::facts::CodexHookSource; +use crate::facts::HookRunFact; use crate::facts::InvocationType; use crate::facts::PluginState; use crate::facts::SubAgentThreadStartedInput; @@ -15,6 +17,7 @@ use codex_plugin::PluginTelemetryMetadata; use codex_protocol::approvals::NetworkApprovalProtocol; use codex_protocol::models::PermissionProfile; use codex_protocol::models::SandboxPermissions; +use codex_protocol::protocol::HookRunStatus; use codex_protocol::protocol::SubAgentSource; use serde::Serialize; @@ -39,6 +42,7 @@ pub(crate) enum TrackEventRequest { GuardianReview(Box), AppMentioned(CodexAppMentionedEventRequest), AppUsed(CodexAppUsedEventRequest), + HookRun(CodexHookRunEventRequest), Compaction(Box), TurnEvent(Box), TurnSteer(CodexTurnSteerEventRequest), @@ -300,6 +304,22 @@ pub(crate) struct CodexAppUsedEventRequest { pub(crate) event_params: CodexAppMetadata, } +#[derive(Serialize)] +pub(crate) struct CodexHookRunMetadata { + pub(crate) thread_id: Option, + pub(crate) turn_id: Option, + pub(crate) model_slug: Option, + pub(crate) hook_name: Option, + pub(crate) hook_source: Option, + pub(crate) status: Option, +} + +#[derive(Serialize)] +pub(crate) struct CodexHookRunEventRequest { + pub(crate) event_type: &'static str, + pub(crate) event_params: CodexHookRunMetadata, +} + #[derive(Serialize)] pub(crate) struct CodexCompactionEventParams { pub(crate) thread_id: String, @@ -529,6 +549,20 @@ pub(crate) fn codex_plugin_used_metadata( } } +pub(crate) fn codex_hook_run_metadata( + tracking: &TrackEventsContext, + hook: HookRunFact, +) -> CodexHookRunMetadata { + CodexHookRunMetadata { + thread_id: Some(tracking.thread_id.clone()), + turn_id: Some(tracking.turn_id.clone()), + model_slug: Some(tracking.model_slug.clone()), + hook_name: Some(hook.event_name.analytics_name().to_owned()), + hook_source: Some(hook.hook_source), + status: Some(analytics_hook_status(hook.status)), + } +} + pub(crate) fn current_runtime_metadata() -> CodexRuntimeMetadata { let os_info = os_info::get(); CodexRuntimeMetadata { @@ -586,3 +620,11 @@ pub(crate) fn subagent_parent_thread_id(subagent_source: &SubAgentSource) -> Opt _ => None, } } + +fn analytics_hook_status(status: HookRunStatus) -> HookRunStatus { + match status { + // Running is unexpected here and normalized defensively. + HookRunStatus::Running => HookRunStatus::Failed, + other => other, + } +} diff --git a/codex-rs/analytics/src/facts.rs b/codex-rs/analytics/src/facts.rs index 5590fbfa6dc..374fdb85fa1 100644 --- a/codex-rs/analytics/src/facts.rs +++ b/codex-rs/analytics/src/facts.rs @@ -15,6 +15,8 @@ use codex_protocol::config_types::ReasoningSummary; use codex_protocol::config_types::ServiceTier; use codex_protocol::openai_models::ReasoningEffort; use codex_protocol::protocol::AskForApproval; +use codex_protocol::protocol::HookEventName; +use codex_protocol::protocol::HookRunStatus; use codex_protocol::protocol::SandboxPolicy; use codex_protocol::protocol::SessionSource; use codex_protocol::protocol::SkillScope; @@ -241,6 +243,15 @@ pub enum CompactionStatus { Interrupted, } +#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize)] +#[serde(rename_all = "snake_case")] +pub enum CodexHookSource { + System, + User, + Project, + Unknown, +} + #[derive(Clone)] pub struct CodexCompactionEvent { pub thread_id: String, @@ -298,6 +309,7 @@ pub(crate) enum CustomAnalyticsFact { SkillInvoked(SkillInvokedInput), AppMentioned(AppMentionedInput), AppUsed(AppUsedInput), + HookRun(HookRunInput), PluginUsed(PluginUsedInput), PluginStateChanged(PluginStateChangedInput), } @@ -317,6 +329,17 @@ pub(crate) struct AppUsedInput { pub app: AppInvocation, } +pub(crate) struct HookRunInput { + pub tracking: TrackEventsContext, + pub hook: HookRunFact, +} + +pub struct HookRunFact { + pub event_name: HookEventName, + pub hook_source: CodexHookSource, + pub status: HookRunStatus, +} + pub(crate) struct PluginUsedInput { pub tracking: TrackEventsContext, pub plugin: PluginTelemetryMetadata, diff --git a/codex-rs/analytics/src/lib.rs b/codex-rs/analytics/src/lib.rs index 1a1a1231523..fb2d7e68035 100644 --- a/codex-rs/analytics/src/lib.rs +++ b/codex-rs/analytics/src/lib.rs @@ -22,6 +22,7 @@ pub use events::GuardianReviewedAction; pub use facts::AnalyticsJsonRpcError; pub use facts::AppInvocation; pub use facts::CodexCompactionEvent; +pub use facts::CodexHookSource; pub use facts::CodexTurnSteerEvent; pub use facts::CompactionImplementation; pub use facts::CompactionPhase; @@ -29,6 +30,7 @@ pub use facts::CompactionReason; pub use facts::CompactionStatus; pub use facts::CompactionStrategy; pub use facts::CompactionTrigger; +pub use facts::HookRunFact; pub use facts::InputError; pub use facts::InvocationType; pub use facts::SkillInvocation; diff --git a/codex-rs/analytics/src/reducer.rs b/codex-rs/analytics/src/reducer.rs index a5e5165bd2b..a6ce3fc831d 100644 --- a/codex-rs/analytics/src/reducer.rs +++ b/codex-rs/analytics/src/reducer.rs @@ -3,6 +3,7 @@ use crate::events::CodexAppMentionedEventRequest; use crate::events::CodexAppServerClientMetadata; use crate::events::CodexAppUsedEventRequest; use crate::events::CodexCompactionEventRequest; +use crate::events::CodexHookRunEventRequest; use crate::events::CodexPluginEventRequest; use crate::events::CodexPluginUsedEventRequest; use crate::events::CodexRuntimeMetadata; @@ -20,6 +21,7 @@ use crate::events::ThreadInitializedEventParams; use crate::events::TrackEventRequest; use crate::events::codex_app_metadata; use crate::events::codex_compaction_event_params; +use crate::events::codex_hook_run_metadata; use crate::events::codex_plugin_metadata; use crate::events::codex_plugin_used_metadata; use crate::events::plugin_state_event_type; @@ -32,6 +34,7 @@ use crate::facts::AppMentionedInput; use crate::facts::AppUsedInput; use crate::facts::CodexCompactionEvent; use crate::facts::CustomAnalyticsFact; +use crate::facts::HookRunInput; use crate::facts::PluginState; use crate::facts::PluginStateChangedInput; use crate::facts::PluginUsedInput; @@ -217,6 +220,9 @@ impl AnalyticsReducer { CustomAnalyticsFact::AppUsed(input) => { self.ingest_app_used(input, out); } + CustomAnalyticsFact::HookRun(input) => { + self.ingest_hook_run(input, out); + } CustomAnalyticsFact::PluginUsed(input) => { self.ingest_plugin_used(input, out); } @@ -442,6 +448,14 @@ impl AnalyticsReducer { })); } + fn ingest_hook_run(&mut self, input: HookRunInput, out: &mut Vec) { + let HookRunInput { tracking, hook } = input; + out.push(TrackEventRequest::HookRun(CodexHookRunEventRequest { + event_type: "codex_hook_run", + event_params: codex_hook_run_metadata(&tracking, hook), + })); + } + fn ingest_plugin_used(&mut self, input: PluginUsedInput, out: &mut Vec) { let PluginUsedInput { tracking, plugin } = input; out.push(TrackEventRequest::PluginUsed(CodexPluginUsedEventRequest { diff --git a/codex-rs/core/src/codex.rs b/codex-rs/core/src/codex.rs index 66a0dc527ef..fb5f27fb23d 100644 --- a/codex-rs/core/src/codex.rs +++ b/codex-rs/core/src/codex.rs @@ -24,6 +24,7 @@ use crate::compact_remote::run_inline_remote_auto_compact_task; use crate::config::ManagedFeatures; use crate::connectors; use crate::exec_policy::ExecPolicyManager; +use crate::hook_runtime::emit_hook_completed_events; use crate::installation_id::resolve_installation_id; use crate::mcp_tool_exposure::build_mcp_tool_exposure; use crate::parse_turn_item; @@ -6575,10 +6576,8 @@ pub(crate) async fn run_turn( .await; } let stop_outcome = sess.hooks().run_stop(stop_request).await; - for completed in stop_outcome.hook_events { - sess.send_event(&turn_context, EventMsg::HookCompleted(completed)) - .await; - } + emit_hook_completed_events(&sess, &turn_context, stop_outcome.hook_events) + .await; if stop_outcome.should_block { if let Some(hook_prompt_message) = build_hook_prompt_message(&stop_outcome.continuation_fragments) diff --git a/codex-rs/core/src/config_loader/mod.rs b/codex-rs/core/src/config_loader/mod.rs index 36fc956bf44..4396ead56be 100644 --- a/codex-rs/core/src/config_loader/mod.rs +++ b/codex-rs/core/src/config_loader/mod.rs @@ -411,12 +411,12 @@ fn system_requirements_toml_file() -> io::Result { } #[cfg(unix)] -fn system_config_toml_file() -> io::Result { +pub(crate) fn system_config_toml_file() -> io::Result { AbsolutePathBuf::from_absolute_path(Path::new(SYSTEM_CONFIG_TOML_FILE_UNIX)) } #[cfg(windows)] -fn system_config_toml_file() -> io::Result { +pub(crate) fn system_config_toml_file() -> io::Result { windows_system_config_toml_file() } diff --git a/codex-rs/core/src/hook_runtime.rs b/codex-rs/core/src/hook_runtime.rs index d2d672fcd48..f09059eba10 100644 --- a/codex-rs/core/src/hook_runtime.rs +++ b/codex-rs/core/src/hook_runtime.rs @@ -1,6 +1,10 @@ use std::future::Future; +use std::path::Path; use std::sync::Arc; +use codex_analytics::CodexHookSource; +use codex_analytics::HookRunFact; +use codex_analytics::build_track_events_context; use codex_hooks::PostToolUseOutcome; use codex_hooks::PostToolUseRequest; use codex_hooks::PreToolUseOutcome; @@ -22,6 +26,7 @@ use serde_json::Value; use crate::codex::Session; use crate::codex::TurnContext; +use crate::config_loader::system_config_toml_file; use crate::event_mapping::parse_turn_item; pub(crate) struct HookRuntimeOutcome { @@ -316,17 +321,81 @@ async fn emit_hook_started_events( } } -async fn emit_hook_completed_events( +pub(crate) async fn emit_hook_completed_events( sess: &Arc, turn_context: &Arc, completed_events: Vec, ) { for completed in completed_events { + track_hook_completed_analytics(sess, turn_context, &completed); sess.send_event(turn_context, EventMsg::HookCompleted(completed)) .await; } } +fn track_hook_completed_analytics( + sess: &Arc, + turn_context: &Arc, + completed: &HookCompletedEvent, +) { + let (tracking, hook) = + hook_run_analytics_payload(sess.conversation_id.to_string(), turn_context, completed); + sess.services + .analytics_events_client + .track_hook_run(tracking, hook); +} + +fn hook_run_analytics_payload( + thread_id: String, + turn_context: &TurnContext, + completed: &HookCompletedEvent, +) -> (codex_analytics::TrackEventsContext, HookRunFact) { + ( + build_track_events_context( + turn_context.model_info.slug.clone(), + thread_id, + completed + .turn_id + .clone() + .unwrap_or_else(|| turn_context.sub_id.clone()), + ), + HookRunFact { + event_name: completed.run.event_name, + hook_source: hook_source_for_path( + completed.run.source_path.as_path(), + turn_context.config.codex_home.as_path(), + turn_context.cwd.as_path(), + ), + status: completed.run.status, + }, + ) +} + +fn hook_source_for_path(source_path: &Path, codex_home: &Path, cwd: &Path) -> CodexHookSource { + if let Ok(system_config_path) = system_config_toml_file() + && let Some(system_config_dir) = system_config_path.as_path().parent() + && source_path.starts_with(system_config_dir) + { + return CodexHookSource::System; + } + + if source_path == codex_home.join("hooks.json") { + return CodexHookSource::User; + } + + // Project hooks are loaded from a `.codex/hooks.json` rooted at or above the + // current working directory, so classify by walking cwd ancestors. + if source_path.ends_with(Path::new(".codex").join("hooks.json")) + && cwd + .ancestors() + .any(|ancestor| source_path.starts_with(ancestor.join(".codex"))) + { + return CodexHookSource::Project; + } + + CodexHookSource::Unknown +} + fn hook_permission_mode(turn_context: &TurnContext) -> String { match turn_context.approval_policy.value() { AskForApproval::Never => "bypassPermissions", @@ -340,10 +409,22 @@ fn hook_permission_mode(turn_context: &TurnContext) -> String { #[cfg(test)] mod tests { + use codex_analytics::CodexHookSource; use codex_protocol::models::ContentItem; + use codex_protocol::protocol::HookEventName; + use codex_protocol::protocol::HookExecutionMode; + use codex_protocol::protocol::HookHandlerType; + use codex_protocol::protocol::HookRunStatus; + use codex_protocol::protocol::HookScope; use pretty_assertions::assert_eq; use super::additional_context_messages; + use super::hook_run_analytics_payload; + use crate::codex::make_session_and_context; + use codex_protocol::protocol::HookCompletedEvent; + use codex_protocol::protocol::HookRunSummary; + use codex_utils_absolute_path::test_support::PathBufExt; + use codex_utils_absolute_path::test_support::test_path_buf; #[test] fn additional_context_messages_stay_separate_and_ordered() { @@ -378,4 +459,103 @@ mod tests { ], ); } + + #[tokio::test] + async fn hook_run_analytics_payload_uses_completed_turn_id() { + let (_session, turn_context) = make_session_and_context().await; + let completed = HookCompletedEvent { + turn_id: Some("turn-from-hook".to_string()), + run: sample_hook_run(HookRunStatus::Blocked), + }; + + let (tracking, hook) = + hook_run_analytics_payload("thread-123".to_string(), &turn_context, &completed); + + assert_eq!(tracking.thread_id, "thread-123"); + assert_eq!(tracking.turn_id, "turn-from-hook"); + assert_eq!(tracking.model_slug, turn_context.model_info.slug); + assert_eq!(hook.event_name, HookEventName::Stop); + assert_eq!(hook.hook_source, CodexHookSource::Unknown); + assert_eq!(hook.status, HookRunStatus::Blocked); + } + + #[tokio::test] + async fn hook_run_analytics_payload_falls_back_to_turn_context_id() { + let (_session, turn_context) = make_session_and_context().await; + let completed = HookCompletedEvent { + turn_id: None, + run: sample_hook_run(HookRunStatus::Failed), + }; + + let (tracking, hook) = + hook_run_analytics_payload("thread-123".to_string(), &turn_context, &completed); + + assert_eq!(tracking.turn_id, turn_context.sub_id); + assert_eq!(hook.status, HookRunStatus::Failed); + } + + #[test] + fn hook_source_for_path_classifies_user_project_and_unknown() { + let codex_home = test_path_buf("/tmp/custom-codex-home").abs(); + let cwd = test_path_buf("/tmp/worktree/src").abs(); + + let system_hooks_path = super::system_config_toml_file() + .expect("system config path") + .parent() + .expect("system config directory") + .join("hooks.json"); + + assert_eq!( + super::hook_source_for_path( + system_hooks_path.as_path(), + codex_home.as_path(), + cwd.as_path(), + ), + CodexHookSource::System, + ); + assert_eq!( + super::hook_source_for_path( + codex_home.join("hooks.json").as_path(), + codex_home.as_path(), + cwd.as_path(), + ), + CodexHookSource::User, + ); + assert_eq!( + super::hook_source_for_path( + test_path_buf("/tmp/worktree/.codex/hooks.json") + .abs() + .as_path(), + codex_home.as_path(), + cwd.as_path(), + ), + CodexHookSource::Project, + ); + assert_eq!( + super::hook_source_for_path( + test_path_buf("/tmp/hooks.json").abs().as_path(), + codex_home.as_path(), + cwd.as_path(), + ), + CodexHookSource::Unknown, + ); + } + + fn sample_hook_run(status: HookRunStatus) -> HookRunSummary { + HookRunSummary { + id: "stop:0:/tmp/hooks.json".to_string(), + event_name: HookEventName::Stop, + handler_type: HookHandlerType::Command, + execution_mode: HookExecutionMode::Sync, + scope: HookScope::Turn, + source_path: test_path_buf("/tmp/hooks.json").abs(), + display_order: 0, + status, + status_message: None, + started_at: 10, + completed_at: Some(37), + duration_ms: Some(27), + entries: Vec::new(), + } + } } diff --git a/codex-rs/protocol/src/protocol.rs b/codex-rs/protocol/src/protocol.rs index 244f748c820..71d916aaedb 100644 --- a/codex-rs/protocol/src/protocol.rs +++ b/codex-rs/protocol/src/protocol.rs @@ -1609,6 +1609,18 @@ pub enum HookEventName { Stop, } +impl HookEventName { + pub fn analytics_name(self) -> &'static str { + match self { + Self::PreToolUse => "PreToolUse", + Self::PostToolUse => "PostToolUse", + Self::SessionStart => "SessionStart", + Self::UserPromptSubmit => "UserPromptSubmit", + Self::Stop => "Stop", + } + } +} + #[derive(Debug, Clone, Copy, Deserialize, Serialize, PartialEq, Eq, JsonSchema, TS)] #[serde(rename_all = "snake_case")] pub enum HookHandlerType {