mirror of
https://github.com/openai/codex.git
synced 2026-10-27 18:48:37 +00:00
Add worktree classification to thread telemetry (#43621)
## What changed Add `is_worktree` to thread initialization analytics and the `codex.thread.started` metric using validated Git repository metadata. Classify linked worktrees as `true` and primary checkouts as `false`; report an unavailable classification as `null` in analytics and `unknown` in metrics. Subagent initialization analytics leave the field unset (`null`). ## Testing Add analytics coverage for primary checkouts, linked worktrees, unknown repositories, and event serialization. Extend the account-switch telemetry test to check that the `is_worktree` tag reaches the next account's collector. GitOrigin-RevId: bc9bcd3c0212c0a123e4bcfed0c7020eef17dda6
This commit is contained in:
committed by
copyberry
parent
c977cc0c19
commit
4b0f44d304
@@ -1674,6 +1674,7 @@ fn thread_initialized_event_serializes_expected_shape() {
|
||||
},
|
||||
model: "gpt-5".to_string(),
|
||||
ephemeral: true,
|
||||
is_worktree: Some(true),
|
||||
thread_source: Some(ThreadSource::Feature("automation".to_string())),
|
||||
initialization_mode: ThreadInitializationMode::New,
|
||||
subagent_source: None,
|
||||
@@ -1707,6 +1708,7 @@ fn thread_initialized_event_serializes_expected_shape() {
|
||||
},
|
||||
"model": "gpt-5",
|
||||
"ephemeral": true,
|
||||
"is_worktree": true,
|
||||
"thread_source": "automation",
|
||||
"initialization_mode": "new",
|
||||
"subagent_source": null,
|
||||
@@ -1718,6 +1720,65 @@ fn thread_initialized_event_serializes_expected_shape() {
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn thread_initialized_classifies_validated_linked_worktrees() {
|
||||
let root = std::env::temp_dir().join(format!(
|
||||
"codex-analytics-worktree-{}",
|
||||
codex_protocol::ThreadId::new()
|
||||
));
|
||||
let primary = root.join("primary");
|
||||
let linked = root.join("linked");
|
||||
let admin = primary.join(".git/worktrees/linked");
|
||||
std::fs::create_dir_all(&admin).expect("worktree administrative directory");
|
||||
std::fs::create_dir_all(&linked).expect("linked checkout");
|
||||
std::fs::write(primary.join(".git/HEAD"), "ref: refs/heads/main\n")
|
||||
.expect("primary repository HEAD");
|
||||
std::fs::write(admin.join("commondir"), "../..\n").expect("common directory");
|
||||
std::fs::write(
|
||||
admin.join("gitdir"),
|
||||
format!("{}\n", linked.join(".git").display()),
|
||||
)
|
||||
.expect("linked checkout backlink");
|
||||
std::fs::write(
|
||||
linked.join(".git"),
|
||||
format!("gitdir: {}\n", admin.display()),
|
||||
)
|
||||
.expect("linked checkout git file");
|
||||
|
||||
let mut reducer = AnalyticsReducer::default();
|
||||
let mut events = Vec::new();
|
||||
ingest_initialize(&mut reducer, &mut events).await;
|
||||
for (cwd, expected) in [
|
||||
(primary.as_path(), json!(false)),
|
||||
(linked.as_path(), json!(true)),
|
||||
(root.as_path(), serde_json::Value::Null),
|
||||
] {
|
||||
events.clear();
|
||||
let mut response =
|
||||
sample_thread_start_response("thread-1", /*ephemeral*/ false, "gpt-5");
|
||||
let ClientResponsePayload::ThreadStart(start) = &mut response else {
|
||||
panic!("expected thread/start response");
|
||||
};
|
||||
start.thread.cwd = codex_utils_absolute_path::AbsolutePathBuf::from_absolute_path(cwd)
|
||||
.expect("absolute checkout path");
|
||||
reducer
|
||||
.ingest(
|
||||
AnalyticsFact::ClientResponse {
|
||||
connection_id: 7,
|
||||
request_id: RequestId::Integer(1),
|
||||
response: Box::new(response),
|
||||
thread_originator: None,
|
||||
},
|
||||
&mut events,
|
||||
)
|
||||
.await;
|
||||
let payload = serde_json::to_value(&events).expect("serialize thread event");
|
||||
assert_eq!(payload[0]["event_params"]["is_worktree"], expected);
|
||||
}
|
||||
|
||||
std::fs::remove_dir_all(root).expect("remove test checkout");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn command_execution_event_serializes_expected_shape() {
|
||||
let event = TrackEventRequest::CommandExecution(CodexCommandExecutionEventRequest {
|
||||
|
||||
@@ -241,6 +241,8 @@ pub(crate) struct ThreadInitializedEventParams {
|
||||
pub(crate) runtime: CodexRuntimeMetadata,
|
||||
pub(crate) model: String,
|
||||
pub(crate) ephemeral: bool,
|
||||
/// Whether the thread's checkout is a validated linked Git worktree, if known.
|
||||
pub(crate) is_worktree: Option<bool>,
|
||||
pub(crate) thread_source: Option<ThreadSource>,
|
||||
pub(crate) initialization_mode: ThreadInitializationMode,
|
||||
pub(crate) subagent_source: Option<String>,
|
||||
@@ -1465,6 +1467,7 @@ pub(crate) fn subagent_thread_started_event_request(
|
||||
runtime: current_runtime_metadata(),
|
||||
model: input.model,
|
||||
ephemeral: input.ephemeral,
|
||||
is_worktree: None,
|
||||
thread_source: input.thread_source,
|
||||
initialization_mode: ThreadInitializationMode::New,
|
||||
subagent_source: Some(subagent_source_name(&input.subagent_source)),
|
||||
|
||||
@@ -2102,6 +2102,11 @@ impl AnalyticsReducer {
|
||||
out: &mut Vec<TrackEventRequest>,
|
||||
) {
|
||||
let session_source: SessionSource = thread.source.into();
|
||||
let is_worktree =
|
||||
codex_git_utils::repository_identity(thread.cwd.as_path()).and_then(|_| {
|
||||
codex_git_utils::get_git_repo_root(thread.cwd.canonicalize().ok()?.as_path())
|
||||
.map(|root| root.join(".git").is_file())
|
||||
});
|
||||
let session_id = thread.session_id;
|
||||
let thread_id = thread.id;
|
||||
let parent_thread_id = thread.parent_thread_id;
|
||||
@@ -2133,6 +2138,7 @@ impl AnalyticsReducer {
|
||||
runtime: connection_state.runtime.clone(),
|
||||
model,
|
||||
ephemeral: thread.ephemeral,
|
||||
is_worktree,
|
||||
thread_source: thread_metadata.thread_source,
|
||||
initialization_mode,
|
||||
subagent_source: thread_metadata.subagent_source.clone(),
|
||||
|
||||
@@ -129,6 +129,10 @@ async fn account_switch_reloads_telemetry_collectors_and_preserves_trace_context
|
||||
next_metrics.contains("codex.thread.started"),
|
||||
"the next account's metrics did not reach its collector: {next_metrics}"
|
||||
);
|
||||
assert!(
|
||||
next_metrics.contains("is_worktree"),
|
||||
"the thread-start worktree tag did not reach its collector: {next_metrics}"
|
||||
);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -1130,17 +1130,25 @@ impl Session {
|
||||
slug: Some(session_model),
|
||||
};
|
||||
config.features.emit_metrics(&session_telemetry);
|
||||
let is_worktree = session_configuration.cwd().canonicalize().ok().and_then(|cwd| {
|
||||
codex_git_utils::repository_identity(&cwd).and_then(|_| {
|
||||
get_git_repo_root(&cwd).map(|root| root.join(".git").is_file())
|
||||
})
|
||||
});
|
||||
let is_worktree_tag = match is_worktree {
|
||||
Some(true) => "true",
|
||||
Some(false) => "false",
|
||||
None => "unknown",
|
||||
};
|
||||
let is_git_tag = if get_git_repo_root(session_configuration.cwd()).is_some() {
|
||||
"true"
|
||||
} else {
|
||||
"false"
|
||||
};
|
||||
session_telemetry.counter(
|
||||
THREAD_STARTED_METRIC,
|
||||
/*inc*/ 1,
|
||||
&[(
|
||||
"is_git",
|
||||
if get_git_repo_root(session_configuration.cwd()).is_some() {
|
||||
"true"
|
||||
} else {
|
||||
"false"
|
||||
},
|
||||
)],
|
||||
&[("is_git", is_git_tag), ("is_worktree", is_worktree_tag)],
|
||||
);
|
||||
|
||||
let mcp_server_names =
|
||||
|
||||
Reference in New Issue
Block a user