Emit plugin ID on MCP tool call analytics events (#27483)

MCP tool-call items already carry the runtime-resolved plugin owner, but
the analytics reducer dropped that field. Forwarding the existing value
provides direct attribution without downstream server-name inference.

## Summary

- emit `plugin_id` on `codex_mcp_tool_call_event` payloads
- preserve `null` for MCP calls without a plugin owner
- verify the serialized field through the MCP item lifecycle test

## Test

- `cd codex-rs && just test -p codex-analytics`
- `cd codex-rs && just fix -p codex-analytics`
- `cd codex-rs && just fmt`
This commit is contained in:
Chris Dong
2026-06-11 09:55:53 -07:00
committed by GitHub
Unverified
parent 1e5b87b4d7
commit df9dd22248
3 changed files with 37 additions and 12 deletions
@@ -3863,6 +3863,32 @@ async fn turn_event_counts_completed_tool_items() {
)
.await;
let mcp_tool_call_item = |status, duration_ms| ThreadItem::McpToolCall {
id: "mcp-1".to_string(),
server: "server".to_string(),
tool: "search".to_string(),
status,
arguments: json!({}),
mcp_app_resource_uri: None,
plugin_id: Some("sample@test".to_string()),
result: None,
error: None,
duration_ms,
};
reducer
.ingest(
AnalyticsFact::Notification(Box::new(ServerNotification::ItemStarted(
ItemStartedNotification {
thread_id: "thread-2".to_string(),
turn_id: "turn-2".to_string(),
started_at_ms: 998,
item: mcp_tool_call_item(McpToolCallStatus::InProgress, None),
},
))),
&mut out,
)
.await;
let completed_tool_items = vec![
sample_command_execution_item(CommandExecutionStatus::Completed, Some(0), Some(1)),
ThreadItem::FileChange {
@@ -3870,18 +3896,7 @@ async fn turn_event_counts_completed_tool_items() {
changes: Vec::new(),
status: PatchApplyStatus::Completed,
},
ThreadItem::McpToolCall {
id: "mcp-1".to_string(),
server: "server".to_string(),
tool: "search".to_string(),
status: McpToolCallStatus::Completed,
arguments: json!({}),
mcp_app_resource_uri: None,
plugin_id: None,
result: None,
error: None,
duration_ms: Some(2),
},
mcp_tool_call_item(McpToolCallStatus::Completed, Some(2)),
ThreadItem::DynamicToolCall {
id: "dynamic-1".to_string(),
namespace: None,
@@ -3939,6 +3954,13 @@ async fn turn_event_counts_completed_tool_items() {
.await;
}
let mcp_tool_call_event = out
.iter()
.find(|event| matches!(event, TrackEventRequest::McpToolCall(_)))
.expect("MCP tool call event should be emitted");
let payload = serde_json::to_value(mcp_tool_call_event).expect("serialize MCP tool call event");
assert_eq!(payload["event_params"]["plugin_id"], json!("sample@test"));
reducer
.ingest(
AnalyticsFact::Notification(Box::new(sample_turn_completed_notification(
+1
View File
@@ -633,6 +633,7 @@ pub(crate) struct CodexMcpToolCallEventParams {
pub(crate) mcp_server_name: String,
pub(crate) mcp_tool_name: String,
pub(crate) mcp_error_present: bool,
pub(crate) plugin_id: Option<String>,
}
#[derive(Serialize)]
+2
View File
@@ -1751,6 +1751,7 @@ fn tool_item_event(input: ToolItemEventInput<'_>) -> Option<TrackEventRequest> {
status,
error,
duration_ms,
plugin_id,
..
} => {
let (terminal_status, failure_kind) = mcp_tool_call_outcome(status)?;
@@ -1780,6 +1781,7 @@ fn tool_item_event(input: ToolItemEventInput<'_>) -> Option<TrackEventRequest> {
mcp_server_name: server.clone(),
mcp_tool_name: tool.clone(),
mcp_error_present: error.is_some(),
plugin_id: plugin_id.clone(),
},
},
))