mirror of
https://github.com/pchuan98/codex.git
synced 2026-07-01 00:31:56 +08:00
[codex-analytics] add session source to client metadata (#17374)
## Summary Adds `thread_source` field to the existing Codex turn metadata sent to Responses API - Sends `thread_source: "user"` for user-initiated sessions: CLI, VS Code, and Exec - Sends `thread_source: "subagent"` for subagent sessions - Omits `thread_source` for MCP, custom, and unknown session sources - Uses the existing turn metadata transport: - HTTP requests send through the `x-codex-turn-metadata` header - WebSocket `response.create` requests send through `client_metadata["x-codex-turn-metadata"]` ## Testing - `cargo test -p codex-protocol session_source_thread_source_name_classifies_user_and_subagent_sources` - `cargo test -p codex-core turn_metadata_state` - `cargo test -p codex-core --test responses_headers responses_stream_includes_turn_metadata_header_for_git_workspace_e2e -- --nocapture`
This commit is contained in:
committed by
GitHub
Unverified
parent
f030ab62eb
commit
61fe23159e
@@ -15,7 +15,6 @@ use codex_plugin::PluginTelemetryMetadata;
|
||||
use codex_protocol::approvals::NetworkApprovalProtocol;
|
||||
use codex_protocol::models::PermissionProfile;
|
||||
use codex_protocol::models::SandboxPermissions;
|
||||
use codex_protocol::protocol::SessionSource;
|
||||
use codex_protocol::protocol::SubAgentSource;
|
||||
use serde::Serialize;
|
||||
|
||||
@@ -530,14 +529,6 @@ pub(crate) fn codex_plugin_used_metadata(
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn thread_source_name(thread_source: &SessionSource) -> Option<&'static str> {
|
||||
match thread_source {
|
||||
SessionSource::Cli | SessionSource::VSCode | SessionSource::Exec => Some("user"),
|
||||
SessionSource::SubAgent(_) => Some("subagent"),
|
||||
SessionSource::Mcp | SessionSource::Custom(_) | SessionSource::Unknown => None,
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn current_runtime_metadata() -> CodexRuntimeMetadata {
|
||||
let os_info = os_info::get();
|
||||
CodexRuntimeMetadata {
|
||||
|
||||
@@ -26,7 +26,6 @@ use crate::events::plugin_state_event_type;
|
||||
use crate::events::subagent_parent_thread_id;
|
||||
use crate::events::subagent_source_name;
|
||||
use crate::events::subagent_thread_started_event_request;
|
||||
use crate::events::thread_source_name;
|
||||
use crate::facts::AnalyticsFact;
|
||||
use crate::facts::AnalyticsJsonRpcError;
|
||||
use crate::facts::AppMentionedInput;
|
||||
@@ -107,7 +106,7 @@ impl ThreadMetadataState {
|
||||
| SessionSource::Unknown => (None, None),
|
||||
};
|
||||
Self {
|
||||
thread_source: thread_source_name(session_source),
|
||||
thread_source: session_source.thread_source_name(),
|
||||
initialization_mode,
|
||||
subagent_source,
|
||||
parent_thread_id,
|
||||
|
||||
@@ -1570,6 +1570,7 @@ impl Session {
|
||||
let per_turn_config = Arc::new(per_turn_config);
|
||||
let turn_metadata_state = Arc::new(TurnMetadataState::new(
|
||||
conversation_id.to_string(),
|
||||
&session_source,
|
||||
sub_id.clone(),
|
||||
cwd.to_path_buf(),
|
||||
session_configuration.sandbox_policy.get(),
|
||||
@@ -5983,6 +5984,7 @@ async fn spawn_review_thread(
|
||||
let review_turn_id = sub_id.to_string();
|
||||
let turn_metadata_state = Arc::new(TurnMetadataState::new(
|
||||
sess.conversation_id.to_string(),
|
||||
&session_source,
|
||||
review_turn_id.clone(),
|
||||
parent_turn_context.cwd.to_path_buf(),
|
||||
parent_turn_context.sandbox_policy.get(),
|
||||
|
||||
@@ -17,6 +17,7 @@ use codex_git_utils::get_has_changes;
|
||||
use codex_git_utils::get_head_commit_hash;
|
||||
use codex_protocol::config_types::WindowsSandboxLevel;
|
||||
use codex_protocol::protocol::SandboxPolicy;
|
||||
use codex_protocol::protocol::SessionSource;
|
||||
|
||||
#[derive(Clone, Debug, Default)]
|
||||
struct WorkspaceGitMetadata {
|
||||
@@ -58,6 +59,8 @@ pub(crate) struct TurnMetadataBag {
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
session_id: Option<String>,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
thread_source: Option<&'static str>,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
turn_id: Option<String>,
|
||||
#[serde(default, skip_serializing_if = "BTreeMap::is_empty")]
|
||||
workspaces: BTreeMap<String, TurnMetadataWorkspace>,
|
||||
@@ -87,6 +90,7 @@ fn merge_responsesapi_client_metadata(
|
||||
|
||||
fn build_turn_metadata_bag(
|
||||
session_id: Option<String>,
|
||||
thread_source: Option<&'static str>,
|
||||
turn_id: Option<String>,
|
||||
sandbox: Option<String>,
|
||||
repo_root: Option<String>,
|
||||
@@ -101,6 +105,7 @@ fn build_turn_metadata_bag(
|
||||
|
||||
TurnMetadataBag {
|
||||
session_id,
|
||||
thread_source,
|
||||
turn_id,
|
||||
workspaces,
|
||||
sandbox,
|
||||
@@ -126,6 +131,7 @@ pub async fn build_turn_metadata_header(cwd: &Path, sandbox: Option<&str>) -> Op
|
||||
|
||||
build_turn_metadata_bag(
|
||||
/*session_id*/ None,
|
||||
/*thread_source*/ None,
|
||||
/*turn_id*/ None,
|
||||
sandbox.map(ToString::to_string),
|
||||
repo_root,
|
||||
@@ -152,6 +158,7 @@ pub(crate) struct TurnMetadataState {
|
||||
impl TurnMetadataState {
|
||||
pub(crate) fn new(
|
||||
session_id: String,
|
||||
session_source: &SessionSource,
|
||||
turn_id: String,
|
||||
cwd: PathBuf,
|
||||
sandbox_policy: &SandboxPolicy,
|
||||
@@ -161,6 +168,7 @@ impl TurnMetadataState {
|
||||
let sandbox = Some(sandbox_tag(sandbox_policy, windows_sandbox_level).to_string());
|
||||
let base_metadata = build_turn_metadata_bag(
|
||||
Some(session_id),
|
||||
session_source.thread_source_name(),
|
||||
Some(turn_id),
|
||||
sandbox,
|
||||
/*repo_root*/ None,
|
||||
@@ -240,6 +248,7 @@ impl TurnMetadataState {
|
||||
|
||||
let enriched_metadata = build_turn_metadata_bag(
|
||||
state.base_metadata.session_id.clone(),
|
||||
state.base_metadata.thread_source,
|
||||
state.base_metadata.turn_id.clone(),
|
||||
state.base_metadata.sandbox.clone(),
|
||||
Some(repo_root),
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
use super::*;
|
||||
|
||||
use codex_protocol::protocol::SessionSource;
|
||||
use codex_protocol::protocol::SubAgentSource;
|
||||
use serde_json::Value;
|
||||
use std::collections::HashMap;
|
||||
use tempfile::TempDir;
|
||||
@@ -69,6 +71,7 @@ fn turn_metadata_state_uses_platform_sandbox_tag() {
|
||||
|
||||
let state = TurnMetadataState::new(
|
||||
"session-a".to_string(),
|
||||
&SessionSource::Exec,
|
||||
"turn-a".to_string(),
|
||||
cwd,
|
||||
&sandbox_policy,
|
||||
@@ -79,10 +82,36 @@ fn turn_metadata_state_uses_platform_sandbox_tag() {
|
||||
let json: Value = serde_json::from_str(&header).expect("json");
|
||||
let sandbox_name = json.get("sandbox").and_then(Value::as_str);
|
||||
let session_id = json.get("session_id").and_then(Value::as_str);
|
||||
let thread_source = json.get("thread_source").and_then(Value::as_str);
|
||||
|
||||
let expected_sandbox = sandbox_tag(&sandbox_policy, WindowsSandboxLevel::Disabled);
|
||||
assert_eq!(sandbox_name, Some(expected_sandbox));
|
||||
assert_eq!(session_id, Some("session-a"));
|
||||
assert_eq!(thread_source, Some("user"));
|
||||
assert!(json.get("session_source").is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn turn_metadata_state_classifies_subagent_thread_source() {
|
||||
let temp_dir = TempDir::new().expect("temp dir");
|
||||
let cwd = temp_dir.path().to_path_buf();
|
||||
let sandbox_policy = SandboxPolicy::new_read_only_policy();
|
||||
let session_source = SessionSource::SubAgent(SubAgentSource::Review);
|
||||
|
||||
let state = TurnMetadataState::new(
|
||||
"session-a".to_string(),
|
||||
&session_source,
|
||||
"turn-a".to_string(),
|
||||
cwd,
|
||||
&sandbox_policy,
|
||||
WindowsSandboxLevel::Disabled,
|
||||
);
|
||||
|
||||
let header = state.current_header_value().expect("header");
|
||||
let json: Value = serde_json::from_str(&header).expect("json");
|
||||
|
||||
assert_eq!(json["thread_source"].as_str(), Some("subagent"));
|
||||
assert!(json.get("session_source").is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -93,6 +122,7 @@ fn turn_metadata_state_merges_client_metadata_without_replacing_reserved_fields(
|
||||
|
||||
let state = TurnMetadataState::new(
|
||||
"session-a".to_string(),
|
||||
&SessionSource::Exec,
|
||||
"turn-a".to_string(),
|
||||
cwd,
|
||||
&sandbox_policy,
|
||||
@@ -101,6 +131,7 @@ fn turn_metadata_state_merges_client_metadata_without_replacing_reserved_fields(
|
||||
state.set_responsesapi_client_metadata(HashMap::from([
|
||||
("fiber_run_id".to_string(), "fiber-123".to_string()),
|
||||
("session_id".to_string(), "client-supplied".to_string()),
|
||||
("thread_source".to_string(), "client-supplied".to_string()),
|
||||
]));
|
||||
|
||||
let header = state.current_header_value().expect("header");
|
||||
@@ -108,5 +139,6 @@ fn turn_metadata_state_merges_client_metadata_without_replacing_reserved_fields(
|
||||
|
||||
assert_eq!(json["fiber_run_id"].as_str(), Some("fiber-123"));
|
||||
assert_eq!(json["session_id"].as_str(), Some("session-a"));
|
||||
assert_eq!(json["thread_source"].as_str(), Some("user"));
|
||||
assert_eq!(json["turn_id"].as_str(), Some("turn-a"));
|
||||
}
|
||||
|
||||
@@ -437,6 +437,12 @@ async fn responses_stream_includes_turn_metadata_header_for_git_workspace_e2e()
|
||||
.and_then(serde_json::Value::as_str),
|
||||
Some("none")
|
||||
);
|
||||
assert_eq!(
|
||||
initial_parsed
|
||||
.get("thread_source")
|
||||
.and_then(serde_json::Value::as_str),
|
||||
Some("user")
|
||||
);
|
||||
|
||||
let git_config_global = cwd.join("empty-git-config");
|
||||
std::fs::write(&git_config_global, "").expect("write empty git config");
|
||||
@@ -528,6 +534,18 @@ async fn responses_stream_includes_turn_metadata_header_for_git_workspace_e2e()
|
||||
.get("turn_id")
|
||||
.and_then(serde_json::Value::as_str)
|
||||
.expect("second turn_id should be present");
|
||||
assert_eq!(
|
||||
first_parsed
|
||||
.get("thread_source")
|
||||
.and_then(serde_json::Value::as_str),
|
||||
Some("user")
|
||||
);
|
||||
assert_eq!(
|
||||
second_parsed
|
||||
.get("thread_source")
|
||||
.and_then(serde_json::Value::as_str),
|
||||
Some("user")
|
||||
);
|
||||
assert_eq!(
|
||||
first_turn_id, second_turn_id,
|
||||
"requests should share turn_id"
|
||||
|
||||
@@ -1209,8 +1209,9 @@ async fn responses_websocket_forwards_turn_metadata_on_initial_and_incremental_c
|
||||
|
||||
let harness = websocket_harness(&server).await;
|
||||
let mut client_session = harness.client.new_session();
|
||||
let first_turn_metadata = r#"{"turn_id":"turn-123","sandbox":"workspace-write"}"#;
|
||||
let enriched_turn_metadata = r#"{"turn_id":"turn-123","sandbox":"workspace-write","workspaces":[{"root_path":"/tmp/repo","latest_git_commit_hash":"abc123","associated_remote_urls":["git@github.com:openai/codex.git"],"has_changes":true}]}"#;
|
||||
let first_turn_metadata =
|
||||
r#"{"turn_id":"turn-123","thread_source":"user","sandbox":"workspace-write"}"#;
|
||||
let enriched_turn_metadata = r#"{"turn_id":"turn-123","thread_source":"user","sandbox":"workspace-write","workspaces":[{"root_path":"/tmp/repo","latest_git_commit_hash":"abc123","associated_remote_urls":["git@github.com:openai/codex.git"],"has_changes":true}]}"#;
|
||||
let prompt_one = prompt_with_input(vec![message_item("hello")]);
|
||||
let prompt_two = prompt_with_input(vec![
|
||||
message_item("hello"),
|
||||
@@ -1259,6 +1260,8 @@ async fn responses_websocket_forwards_turn_metadata_on_initial_and_incremental_c
|
||||
|
||||
assert_eq!(first_metadata["turn_id"].as_str(), Some("turn-123"));
|
||||
assert_eq!(second_metadata["turn_id"].as_str(), Some("turn-123"));
|
||||
assert_eq!(first_metadata["thread_source"].as_str(), Some("user"));
|
||||
assert_eq!(second_metadata["thread_source"].as_str(), Some("user"));
|
||||
assert_eq!(
|
||||
second_metadata["workspaces"][0]["has_changes"].as_bool(),
|
||||
Some(true)
|
||||
|
||||
@@ -2633,6 +2633,15 @@ impl SessionSource {
|
||||
})
|
||||
}
|
||||
|
||||
/// Low cardinality thread source label for analytics.
|
||||
pub fn thread_source_name(&self) -> Option<&'static str> {
|
||||
match self {
|
||||
SessionSource::Cli | SessionSource::VSCode | SessionSource::Exec => Some("user"),
|
||||
SessionSource::SubAgent(_) => Some("subagent"),
|
||||
SessionSource::Mcp | SessionSource::Custom(_) | SessionSource::Unknown => None,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn get_nickname(&self) -> Option<String> {
|
||||
match self {
|
||||
SessionSource::SubAgent(SubAgentSource::ThreadSpawn { agent_nickname, .. }) => {
|
||||
@@ -3882,6 +3891,24 @@ mod tests {
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn session_source_thread_source_name_classifies_user_and_subagent_sources() {
|
||||
for (source, expected) in [
|
||||
(SessionSource::Cli, Some("user")),
|
||||
(SessionSource::VSCode, Some("user")),
|
||||
(SessionSource::Exec, Some("user")),
|
||||
(
|
||||
SessionSource::SubAgent(SubAgentSource::Review),
|
||||
Some("subagent"),
|
||||
),
|
||||
(SessionSource::Mcp, None),
|
||||
(SessionSource::Custom("atlas".to_string()), None),
|
||||
(SessionSource::Unknown, None),
|
||||
] {
|
||||
assert_eq!(source.thread_source_name(), expected);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn session_source_restriction_product_defaults_non_subagent_sources_to_codex() {
|
||||
assert_eq!(
|
||||
|
||||
Reference in New Issue
Block a user