mirror of
https://github.com/openai/codex.git
synced 2026-10-20 17:38:45 +00:00
Add plugin script lifecycle analytics contract
This commit is contained in:
@@ -48,6 +48,7 @@ use crate::facts::AppMentionedInput;
|
||||
use crate::facts::AppUsedInput;
|
||||
use crate::facts::CodexCompactionEvent;
|
||||
use crate::facts::CodexErrKind;
|
||||
use crate::facts::CodexPluginScriptLifecycleEvent;
|
||||
use crate::facts::CompactionImplementation;
|
||||
use crate::facts::CompactionPhase;
|
||||
use crate::facts::CompactionReason;
|
||||
@@ -67,6 +68,8 @@ use crate::facts::PluginInstallRequested;
|
||||
use crate::facts::PluginInstallRequestedInput;
|
||||
use crate::facts::PluginInstallRequestedPlugin;
|
||||
use crate::facts::PluginInstallSource;
|
||||
use crate::facts::PluginScriptLifecycleStatus;
|
||||
use crate::facts::PluginScriptSkill;
|
||||
use crate::facts::PluginState;
|
||||
use crate::facts::PluginStateChangedInput;
|
||||
use crate::facts::PluginUsedInput;
|
||||
@@ -3501,6 +3504,123 @@ async fn reducer_includes_plugin_id_for_plugin_skill_invocations() {
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn reducer_enriches_plugin_script_lifecycle_events_without_sensitive_paths() {
|
||||
let mut reducer = AnalyticsReducer::default();
|
||||
let mut events = Vec::new();
|
||||
reducer
|
||||
.ingest(
|
||||
AnalyticsFact::Initialize {
|
||||
connection_id: 7,
|
||||
params: InitializeParams {
|
||||
client_info: ClientInfo {
|
||||
name: "codex-tui".to_string(),
|
||||
title: None,
|
||||
version: "1.0.0".to_string(),
|
||||
},
|
||||
capabilities: Some(InitializeCapabilities {
|
||||
experimental_api: true,
|
||||
request_attestation: false,
|
||||
opt_out_notification_methods: None,
|
||||
mcp_server_openai_form_elicitation: false,
|
||||
}),
|
||||
},
|
||||
product_client_id: DEFAULT_ORIGINATOR.to_string(),
|
||||
runtime: sample_runtime_metadata(),
|
||||
rpc_transport: AppServerRpcTransport::Stdio,
|
||||
},
|
||||
&mut events,
|
||||
)
|
||||
.await;
|
||||
reducer
|
||||
.ingest(
|
||||
AnalyticsFact::ClientResponse {
|
||||
connection_id: 7,
|
||||
request_id: RequestId::Integer(1),
|
||||
response: Box::new(sample_thread_start_response(
|
||||
"thread-1", /*ephemeral*/ false, "gpt-5",
|
||||
)),
|
||||
thread_originator: None,
|
||||
},
|
||||
&mut events,
|
||||
)
|
||||
.await;
|
||||
events.clear();
|
||||
|
||||
let sensitive_skill_path =
|
||||
PathBuf::from("/Users/private/.codex/plugins/cache/openai/sample/skills/doc/SKILL.md");
|
||||
let skill = PluginScriptSkill {
|
||||
skill_name: "sample:doc".to_string(),
|
||||
skill_path: sensitive_skill_path.clone(),
|
||||
};
|
||||
let expected_skill_id = skill_id_for_local_skill(
|
||||
/*repo_url*/ None,
|
||||
/*repo_root*/ None,
|
||||
sensitive_skill_path.as_path(),
|
||||
"sample:doc",
|
||||
);
|
||||
for (status, duration_ms, exit_code) in [
|
||||
(PluginScriptLifecycleStatus::Started, None, None),
|
||||
(PluginScriptLifecycleStatus::Completed, Some(42), Some(0)),
|
||||
] {
|
||||
reducer
|
||||
.ingest(
|
||||
AnalyticsFact::Custom(CustomAnalyticsFact::PluginScriptLifecycle(Box::new(
|
||||
CodexPluginScriptLifecycleEvent {
|
||||
thread_id: "thread-1".to_string(),
|
||||
turn_id: "turn-1".to_string(),
|
||||
plugin_id: "sample@openai-curated".to_string(),
|
||||
execution_id: "execution-1".to_string(),
|
||||
script_path: "scripts/run.py".to_string(),
|
||||
timestamp: "2026-06-12T17:39:31.000Z".to_string(),
|
||||
status,
|
||||
duration_ms,
|
||||
exit_code,
|
||||
skill: Some(skill.clone()),
|
||||
},
|
||||
))),
|
||||
&mut events,
|
||||
)
|
||||
.await;
|
||||
}
|
||||
|
||||
let payload = serde_json::to_value(&events).expect("serialize events");
|
||||
assert_eq!(payload.as_array().map(Vec::len), Some(2));
|
||||
let started = &payload[0];
|
||||
let completed = &payload[1];
|
||||
assert_eq!(started["event_type"], "codex_plugin_lifecycle_event");
|
||||
assert_eq!(started["event_params"]["version"], 1);
|
||||
assert_eq!(started["event_params"]["thread_id"], "thread-1");
|
||||
assert_eq!(started["event_params"]["session_id"], "session-thread-1");
|
||||
assert_eq!(started["event_params"]["turn_id"], "turn-1");
|
||||
assert_eq!(
|
||||
started["event_params"]["app_server_client"]["product_client_id"],
|
||||
DEFAULT_ORIGINATOR
|
||||
);
|
||||
assert_eq!(
|
||||
started["event_params"]["plugin_id"],
|
||||
"sample@openai-curated"
|
||||
);
|
||||
assert_eq!(started["event_params"]["execution_id"], "execution-1");
|
||||
assert_eq!(started["event_params"]["script_path"], "scripts/run.py");
|
||||
assert_eq!(
|
||||
started["event_params"]["timestamp"],
|
||||
"2026-06-12T17:39:31.000Z"
|
||||
);
|
||||
assert_eq!(started["event_params"]["status"], "started");
|
||||
assert_eq!(started["event_params"]["skill_id"], expected_skill_id);
|
||||
assert!(started["event_params"].get("duration_ms").is_none());
|
||||
assert!(started["event_params"].get("exit_code").is_none());
|
||||
|
||||
assert_eq!(completed["event_params"]["status"], "completed");
|
||||
assert_eq!(completed["event_params"]["duration_ms"], 42);
|
||||
assert_eq!(completed["event_params"]["exit_code"], 0);
|
||||
assert_eq!(completed["event_params"]["skill_id"], expected_skill_id);
|
||||
let serialized = payload.to_string();
|
||||
assert!(!serialized.contains("/Users/private"));
|
||||
assert!(!serialized.contains("SKILL.md"));
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn reducer_ingests_hook_run_fact() {
|
||||
let mut reducer = AnalyticsReducer::default();
|
||||
|
||||
@@ -338,6 +338,15 @@ impl AnalyticsEventsClient {
|
||||
))));
|
||||
}
|
||||
|
||||
pub fn track_plugin_script_lifecycle(
|
||||
&self,
|
||||
event: crate::facts::CodexPluginScriptLifecycleEvent,
|
||||
) {
|
||||
self.record_fact(AnalyticsFact::Custom(
|
||||
CustomAnalyticsFact::PluginScriptLifecycle(Box::new(event)),
|
||||
));
|
||||
}
|
||||
|
||||
pub fn track_turn_resolved_config(&self, fact: TurnResolvedConfigFact) {
|
||||
self.record_fact(AnalyticsFact::Custom(
|
||||
CustomAnalyticsFact::TurnResolvedConfig(Box::new(fact)),
|
||||
|
||||
@@ -15,6 +15,7 @@ use crate::facts::GoalEventKind;
|
||||
use crate::facts::HookRunFact;
|
||||
use crate::facts::InvocationType;
|
||||
use crate::facts::PluginInstallRequested;
|
||||
use crate::facts::PluginScriptLifecycleStatus;
|
||||
use crate::facts::PluginState;
|
||||
use crate::facts::SubAgentThreadStartedInput;
|
||||
use crate::facts::ThreadInitializationMode;
|
||||
@@ -68,6 +69,7 @@ pub(crate) enum TrackEventRequest {
|
||||
HookRun(CodexHookRunEventRequest),
|
||||
Compaction(Box<CodexCompactionEventRequest>),
|
||||
Goal(Box<CodexGoalEventRequest>),
|
||||
PluginScriptLifecycle(Box<CodexPluginScriptLifecycleEventRequest>),
|
||||
TurnEvent(Box<CodexTurnEventRequest>),
|
||||
TurnSteer(CodexTurnSteerEventRequest),
|
||||
CommandExecution(CodexCommandExecutionEventRequest),
|
||||
@@ -846,6 +848,35 @@ pub(crate) struct CodexGoalEventRequest {
|
||||
pub(crate) event_params: CodexGoalEventParams,
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
pub(crate) struct CodexPluginScriptLifecycleEventParams {
|
||||
pub(crate) version: u32,
|
||||
pub(crate) thread_id: String,
|
||||
pub(crate) session_id: String,
|
||||
pub(crate) turn_id: String,
|
||||
pub(crate) app_server_client: CodexAppServerClientMetadata,
|
||||
pub(crate) runtime: CodexRuntimeMetadata,
|
||||
pub(crate) thread_source: Option<ThreadSource>,
|
||||
pub(crate) subagent_source: Option<String>,
|
||||
pub(crate) parent_thread_id: Option<String>,
|
||||
pub(crate) plugin_id: String,
|
||||
pub(crate) execution_id: String,
|
||||
pub(crate) script_path: String,
|
||||
pub(crate) timestamp: String,
|
||||
pub(crate) status: PluginScriptLifecycleStatus,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub(crate) duration_ms: Option<u64>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub(crate) exit_code: Option<i32>,
|
||||
pub(crate) skill_id: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
pub(crate) struct CodexPluginScriptLifecycleEventRequest {
|
||||
pub(crate) event_type: &'static str,
|
||||
pub(crate) event_params: CodexPluginScriptLifecycleEventParams,
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
pub(crate) struct CodexTurnEventParams {
|
||||
pub(crate) thread_id: String,
|
||||
|
||||
@@ -446,6 +446,35 @@ pub struct CodexGoalEvent {
|
||||
pub cumulative_time_accounted_seconds: Option<i64>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, Serialize)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub enum PluginScriptLifecycleStatus {
|
||||
Started,
|
||||
Completed,
|
||||
Failed,
|
||||
Interrupted,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct PluginScriptSkill {
|
||||
pub skill_name: String,
|
||||
pub skill_path: PathBuf,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct CodexPluginScriptLifecycleEvent {
|
||||
pub thread_id: String,
|
||||
pub turn_id: String,
|
||||
pub plugin_id: String,
|
||||
pub execution_id: String,
|
||||
pub script_path: String,
|
||||
pub timestamp: String,
|
||||
pub status: PluginScriptLifecycleStatus,
|
||||
pub duration_ms: Option<u64>,
|
||||
pub exit_code: Option<i32>,
|
||||
pub skill: Option<PluginScriptSkill>,
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
pub(crate) enum AnalyticsFact {
|
||||
Initialize {
|
||||
@@ -499,6 +528,7 @@ pub(crate) enum CustomAnalyticsFact {
|
||||
SubAgentThreadStarted(SubAgentThreadStartedInput),
|
||||
Compaction(Box<CodexCompactionEvent>),
|
||||
Goal(Box<CodexGoalEvent>),
|
||||
PluginScriptLifecycle(Box<CodexPluginScriptLifecycleEvent>),
|
||||
GuardianReview(Box<GuardianReviewEventParams>),
|
||||
TurnResolvedConfig(Box<TurnResolvedConfigFact>),
|
||||
TurnTokenUsage(Box<TurnTokenUsageFact>),
|
||||
|
||||
@@ -29,6 +29,7 @@ pub use facts::AppInvocation;
|
||||
pub use facts::CodexCompactionEvent;
|
||||
pub use facts::CodexErrKind;
|
||||
pub use facts::CodexGoalEvent;
|
||||
pub use facts::CodexPluginScriptLifecycleEvent;
|
||||
pub use facts::CodexTurnSteerEvent;
|
||||
pub use facts::CompactionImplementation;
|
||||
pub use facts::CompactionPhase;
|
||||
@@ -46,6 +47,8 @@ pub use facts::PluginInstallRequestSource;
|
||||
pub use facts::PluginInstallRequested;
|
||||
pub use facts::PluginInstallRequestedPlugin;
|
||||
pub use facts::PluginInstallSource;
|
||||
pub use facts::PluginScriptLifecycleStatus;
|
||||
pub use facts::PluginScriptSkill;
|
||||
pub use facts::SkillInvocation;
|
||||
pub use facts::SubAgentThreadStartedInput;
|
||||
pub use facts::ThreadInitializationMode;
|
||||
|
||||
@@ -29,6 +29,8 @@ use crate::events::CodexPluginEventRequest;
|
||||
use crate::events::CodexPluginInstallFailedEventRequest;
|
||||
use crate::events::CodexPluginInstallFailedMetadata;
|
||||
use crate::events::CodexPluginInstallRequestedEventRequest;
|
||||
use crate::events::CodexPluginScriptLifecycleEventParams;
|
||||
use crate::events::CodexPluginScriptLifecycleEventRequest;
|
||||
use crate::events::CodexPluginUsedEventRequest;
|
||||
use crate::events::CodexReviewEventParams;
|
||||
use crate::events::CodexReviewEventRequest;
|
||||
@@ -73,6 +75,7 @@ use crate::facts::AppMentionedInput;
|
||||
use crate::facts::AppUsedInput;
|
||||
use crate::facts::CodexCompactionEvent;
|
||||
use crate::facts::CodexGoalEvent;
|
||||
use crate::facts::CodexPluginScriptLifecycleEvent;
|
||||
use crate::facts::CustomAnalyticsFact;
|
||||
use crate::facts::ExternalAgentConfigImportCompletedInput;
|
||||
use crate::facts::ExternalAgentConfigImportFailureInput;
|
||||
@@ -231,6 +234,16 @@ impl<'a> AnalyticsDropSite<'a> {
|
||||
}
|
||||
}
|
||||
|
||||
fn plugin_script_lifecycle(input: &'a CodexPluginScriptLifecycleEvent) -> Self {
|
||||
Self {
|
||||
event_name: "plugin script lifecycle",
|
||||
thread_id: &input.thread_id,
|
||||
turn_id: Some(&input.turn_id),
|
||||
review_id: None,
|
||||
item_id: None,
|
||||
}
|
||||
}
|
||||
|
||||
fn tool_item(
|
||||
notification: &'a codex_app_server_protocol::ItemCompletedNotification,
|
||||
item_id: &'a str,
|
||||
@@ -506,6 +519,9 @@ impl AnalyticsReducer {
|
||||
CustomAnalyticsFact::Goal(input) => {
|
||||
self.ingest_goal(*input, out);
|
||||
}
|
||||
CustomAnalyticsFact::PluginScriptLifecycle(input) => {
|
||||
self.ingest_plugin_script_lifecycle(*input, out).await;
|
||||
}
|
||||
CustomAnalyticsFact::GuardianReview(input) => {
|
||||
self.ingest_guardian_review(*input, out);
|
||||
}
|
||||
@@ -1433,6 +1449,60 @@ impl AnalyticsReducer {
|
||||
})));
|
||||
}
|
||||
|
||||
async fn ingest_plugin_script_lifecycle(
|
||||
&mut self,
|
||||
input: CodexPluginScriptLifecycleEvent,
|
||||
out: &mut Vec<TrackEventRequest>,
|
||||
) {
|
||||
let Some((connection_state, thread_state, thread_metadata)) =
|
||||
self.thread_context_or_warn(AnalyticsDropSite::plugin_script_lifecycle(&input))
|
||||
else {
|
||||
return;
|
||||
};
|
||||
let skill_id = if let Some(skill) = input.skill.as_ref() {
|
||||
let repo_root = get_git_repo_root(skill.skill_path.as_path());
|
||||
let repo_url = if let Some(root) = repo_root.as_ref() {
|
||||
collect_git_info(root)
|
||||
.await
|
||||
.and_then(|info| info.repository_url)
|
||||
} else {
|
||||
None
|
||||
};
|
||||
Some(skill_id_for_local_skill(
|
||||
repo_url.as_deref(),
|
||||
repo_root.as_deref(),
|
||||
skill.skill_path.as_path(),
|
||||
skill.skill_name.as_str(),
|
||||
))
|
||||
} else {
|
||||
None
|
||||
};
|
||||
out.push(TrackEventRequest::PluginScriptLifecycle(Box::new(
|
||||
CodexPluginScriptLifecycleEventRequest {
|
||||
event_type: "codex_plugin_lifecycle_event",
|
||||
event_params: CodexPluginScriptLifecycleEventParams {
|
||||
version: 1,
|
||||
thread_id: input.thread_id,
|
||||
session_id: thread_metadata.session_id.clone(),
|
||||
turn_id: input.turn_id,
|
||||
app_server_client: thread_state.app_server_client(connection_state),
|
||||
runtime: connection_state.runtime.clone(),
|
||||
thread_source: thread_metadata.thread_source.clone(),
|
||||
subagent_source: thread_metadata.subagent_source.clone(),
|
||||
parent_thread_id: thread_metadata.parent_thread_id.clone(),
|
||||
plugin_id: input.plugin_id,
|
||||
execution_id: input.execution_id,
|
||||
script_path: input.script_path,
|
||||
timestamp: input.timestamp,
|
||||
status: input.status,
|
||||
duration_ms: input.duration_ms,
|
||||
exit_code: input.exit_code,
|
||||
skill_id,
|
||||
},
|
||||
},
|
||||
)));
|
||||
}
|
||||
|
||||
fn ingest_guardian_review_completed(
|
||||
&mut self,
|
||||
notification: codex_app_server_protocol::ItemGuardianApprovalReviewCompletedNotification,
|
||||
|
||||
Reference in New Issue
Block a user