mirror of
https://github.com/openai/codex.git
synced 2026-11-13 14:46:14 +00:00
[codex] Attribute app-server analytics by thread originator (#29935)
## Why Desktop Work threads and regular Codex threads can share the same app-server connection. App-server analytics currently copy `product_client_id` from connection metadata for every thread-scoped event, so Work thread activity is attributed to the Desktop connection instead of the thread's resolved originator. This prevents analytics from distinguishing the two products on a shared connection. ## What changed - Publish the resolved originator after a thread is materialized, covering new, resumed, forked, and subagent threads. - Store that originator in the analytics reducer's existing per-thread state. - Override only `app_server_client.product_client_id` for thread, turn, tool, review, goal, guardian, and compaction events while preserving the connection's client name, version, and transport metadata. - Fall back to the connection-wide product client ID when a thread has no originator override. - Preserve persisted originators in thread initialization analytics for resume and fork flows. ## Validation - `just test -p codex-analytics thread_originator_overrides_shared_connection_across_thread_events subagent_events_keep_thread_originator_with_explicit_turn_connection` - `just test -p codex-app-server turn_start_tracks_thread_originator_in_analytics thread_start_tracks_thread_initialized_analytics thread_fork_tracks_thread_initialized_analytics thread_resume_tracks_thread_initialized_analytics` - `just test -p codex-core thread_manager`
This commit is contained in:
1 parent
da78d5fdc5
commit
841f30598c
12 files changed
+431
-57
No files matched your search
@@ -558,6 +558,7 @@ async fn ingest_rejected_turn_steer(
|
|||||||
response: Box::new(sample_thread_resume_response(
|
response: Box::new(sample_thread_resume_response(
|
||||||
"thread-2", /*ephemeral*/ false, "gpt-5",
|
"thread-2", /*ephemeral*/ false, "gpt-5",
|
||||||
)),
|
)),
|
||||||
|
thread_originator: None,
|
||||||
},
|
},
|
||||||
out,
|
out,
|
||||||
)
|
)
|
||||||
@@ -631,6 +632,7 @@ async fn ingest_turn_prerequisites(
|
|||||||
response: Box::new(sample_thread_start_response(
|
response: Box::new(sample_thread_start_response(
|
||||||
"thread-2", /*ephemeral*/ false, "gpt-5",
|
"thread-2", /*ephemeral*/ false, "gpt-5",
|
||||||
)),
|
)),
|
||||||
|
thread_originator: None,
|
||||||
},
|
},
|
||||||
out,
|
out,
|
||||||
)
|
)
|
||||||
@@ -654,6 +656,7 @@ async fn ingest_turn_prerequisites(
|
|||||||
connection_id: 7,
|
connection_id: 7,
|
||||||
request_id: RequestId::Integer(3),
|
request_id: RequestId::Integer(3),
|
||||||
response: Box::new(sample_turn_start_response("turn-2")),
|
response: Box::new(sample_turn_start_response("turn-2")),
|
||||||
|
thread_originator: None,
|
||||||
},
|
},
|
||||||
out,
|
out,
|
||||||
)
|
)
|
||||||
@@ -720,6 +723,7 @@ async fn ingest_review_prerequisites(
|
|||||||
response: Box::new(sample_thread_start_response(
|
response: Box::new(sample_thread_start_response(
|
||||||
"thread-1", /*ephemeral*/ false, "gpt-5",
|
"thread-1", /*ephemeral*/ false, "gpt-5",
|
||||||
)),
|
)),
|
||||||
|
thread_originator: None,
|
||||||
},
|
},
|
||||||
events,
|
events,
|
||||||
)
|
)
|
||||||
@@ -1652,6 +1656,7 @@ async fn initialize_caches_client_and_thread_lifecycle_publishes_once_initialize
|
|||||||
/*ephemeral*/ false,
|
/*ephemeral*/ false,
|
||||||
"gpt-5",
|
"gpt-5",
|
||||||
)),
|
)),
|
||||||
|
thread_originator: None,
|
||||||
},
|
},
|
||||||
&mut events,
|
&mut events,
|
||||||
)
|
)
|
||||||
@@ -1697,6 +1702,7 @@ async fn initialize_caches_client_and_thread_lifecycle_publishes_once_initialize
|
|||||||
response: Box::new(sample_thread_resume_response(
|
response: Box::new(sample_thread_resume_response(
|
||||||
"thread-1", /*ephemeral*/ true, "gpt-5",
|
"thread-1", /*ephemeral*/ true, "gpt-5",
|
||||||
)),
|
)),
|
||||||
|
thread_originator: None,
|
||||||
},
|
},
|
||||||
&mut events,
|
&mut events,
|
||||||
)
|
)
|
||||||
@@ -1741,6 +1747,157 @@ async fn initialize_caches_client_and_thread_lifecycle_publishes_once_initialize
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn thread_originator_overrides_shared_connection_across_thread_events() {
|
||||||
|
let mut reducer = AnalyticsReducer::default();
|
||||||
|
let mut events = Vec::new();
|
||||||
|
|
||||||
|
reducer
|
||||||
|
.ingest(sample_initialize_fact(/*connection_id*/ 7), &mut events)
|
||||||
|
.await;
|
||||||
|
for (request_id, thread_id, thread_originator) in [
|
||||||
|
(1, "thread-work", Some(TEST_PRODUCT_CLIENT_ID.to_string())),
|
||||||
|
(2, "thread-default", None),
|
||||||
|
] {
|
||||||
|
reducer
|
||||||
|
.ingest(
|
||||||
|
AnalyticsFact::ClientResponse {
|
||||||
|
connection_id: 7,
|
||||||
|
request_id: RequestId::Integer(request_id),
|
||||||
|
response: Box::new(sample_thread_start_response(
|
||||||
|
thread_id, /*ephemeral*/ false, "gpt-5",
|
||||||
|
)),
|
||||||
|
thread_originator,
|
||||||
|
},
|
||||||
|
&mut events,
|
||||||
|
)
|
||||||
|
.await;
|
||||||
|
}
|
||||||
|
|
||||||
|
let initialized = serde_json::to_value(&events).expect("serialize thread events");
|
||||||
|
assert_eq!(
|
||||||
|
initialized
|
||||||
|
.as_array()
|
||||||
|
.expect("thread events")
|
||||||
|
.iter()
|
||||||
|
.map(|event| {
|
||||||
|
json!({
|
||||||
|
"thread_id": event["event_params"]["thread_id"],
|
||||||
|
"app_server_client": event["event_params"]["app_server_client"],
|
||||||
|
})
|
||||||
|
})
|
||||||
|
.collect::<Vec<_>>(),
|
||||||
|
vec![
|
||||||
|
json!({
|
||||||
|
"thread_id": "thread-work",
|
||||||
|
"app_server_client": {
|
||||||
|
"product_client_id": TEST_PRODUCT_CLIENT_ID,
|
||||||
|
"client_name": "codex-tui",
|
||||||
|
"client_version": "1.0.0",
|
||||||
|
"rpc_transport": "websocket",
|
||||||
|
"experimental_api_enabled": false,
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
json!({
|
||||||
|
"thread_id": "thread-default",
|
||||||
|
"app_server_client": {
|
||||||
|
"product_client_id": DEFAULT_ORIGINATOR,
|
||||||
|
"client_name": "codex-tui",
|
||||||
|
"client_version": "1.0.0",
|
||||||
|
"rpc_transport": "websocket",
|
||||||
|
"experimental_api_enabled": false,
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
]
|
||||||
|
);
|
||||||
|
|
||||||
|
events.clear();
|
||||||
|
reducer
|
||||||
|
.ingest(
|
||||||
|
AnalyticsFact::ClientRequest {
|
||||||
|
connection_id: 7,
|
||||||
|
request_id: RequestId::Integer(3),
|
||||||
|
request: Box::new(sample_turn_start_request(
|
||||||
|
"thread-work",
|
||||||
|
/*request_id*/ 3,
|
||||||
|
)),
|
||||||
|
},
|
||||||
|
&mut events,
|
||||||
|
)
|
||||||
|
.await;
|
||||||
|
reducer
|
||||||
|
.ingest(
|
||||||
|
AnalyticsFact::ClientResponse {
|
||||||
|
connection_id: 7,
|
||||||
|
request_id: RequestId::Integer(3),
|
||||||
|
response: Box::new(sample_turn_start_response("turn-1")),
|
||||||
|
thread_originator: None,
|
||||||
|
},
|
||||||
|
&mut events,
|
||||||
|
)
|
||||||
|
.await;
|
||||||
|
ingest_completed_command_execution_item(&mut reducer, &mut events, "thread-work", "item-work")
|
||||||
|
.await;
|
||||||
|
ingest_complete_child_turn(&mut reducer, &mut events, "thread-work", "turn-1").await;
|
||||||
|
reducer
|
||||||
|
.ingest(
|
||||||
|
AnalyticsFact::Custom(CustomAnalyticsFact::Compaction(Box::new(
|
||||||
|
CodexCompactionEvent {
|
||||||
|
thread_id: "thread-work".to_string(),
|
||||||
|
turn_id: "turn-compact".to_string(),
|
||||||
|
trigger: CompactionTrigger::Manual,
|
||||||
|
reason: CompactionReason::UserRequested,
|
||||||
|
implementation: CompactionImplementation::Responses,
|
||||||
|
phase: CompactionPhase::StandaloneTurn,
|
||||||
|
strategy: CompactionStrategy::Memento,
|
||||||
|
status: CompactionStatus::Completed,
|
||||||
|
codex_error_kind: None,
|
||||||
|
codex_error_http_status_code: None,
|
||||||
|
active_context_tokens_before: 131_000,
|
||||||
|
active_context_tokens_after: 64_000,
|
||||||
|
retained_image_count: None,
|
||||||
|
compaction_summary_tokens: None,
|
||||||
|
cached_input_tokens: None,
|
||||||
|
started_at: 100,
|
||||||
|
completed_at: 101,
|
||||||
|
duration_ms: Some(1200),
|
||||||
|
},
|
||||||
|
))),
|
||||||
|
&mut events,
|
||||||
|
)
|
||||||
|
.await;
|
||||||
|
|
||||||
|
let lifecycle = serde_json::to_value(&events).expect("serialize lifecycle events");
|
||||||
|
assert_eq!(
|
||||||
|
lifecycle
|
||||||
|
.as_array()
|
||||||
|
.expect("lifecycle events")
|
||||||
|
.iter()
|
||||||
|
.map(|event| {
|
||||||
|
json!({
|
||||||
|
"event_type": event["event_type"],
|
||||||
|
"product_client_id":
|
||||||
|
event["event_params"]["app_server_client"]["product_client_id"],
|
||||||
|
})
|
||||||
|
})
|
||||||
|
.collect::<Vec<_>>(),
|
||||||
|
vec![
|
||||||
|
json!({
|
||||||
|
"event_type": "codex_command_execution_event",
|
||||||
|
"product_client_id": TEST_PRODUCT_CLIENT_ID,
|
||||||
|
}),
|
||||||
|
json!({
|
||||||
|
"event_type": "codex_turn_event",
|
||||||
|
"product_client_id": TEST_PRODUCT_CLIENT_ID,
|
||||||
|
}),
|
||||||
|
json!({
|
||||||
|
"event_type": "codex_compaction_event",
|
||||||
|
"product_client_id": TEST_PRODUCT_CLIENT_ID,
|
||||||
|
}),
|
||||||
|
]
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
async fn unrelated_client_requests_are_ignored_by_reducer() {
|
async fn unrelated_client_requests_are_ignored_by_reducer() {
|
||||||
let mut reducer = AnalyticsReducer::default();
|
let mut reducer = AnalyticsReducer::default();
|
||||||
@@ -1767,6 +1924,7 @@ async fn unrelated_client_requests_are_ignored_by_reducer() {
|
|||||||
connection_id: 7,
|
connection_id: 7,
|
||||||
request_id: RequestId::Integer(3),
|
request_id: RequestId::Integer(3),
|
||||||
response: Box::new(sample_turn_start_response("turn-2")),
|
response: Box::new(sample_turn_start_response("turn-2")),
|
||||||
|
thread_originator: None,
|
||||||
},
|
},
|
||||||
&mut events,
|
&mut events,
|
||||||
)
|
)
|
||||||
@@ -1792,6 +1950,7 @@ async fn unrelated_client_responses_are_ignored_by_reducer() {
|
|||||||
response: Box::new(ClientResponsePayload::ThreadArchive(
|
response: Box::new(ClientResponsePayload::ThreadArchive(
|
||||||
ThreadArchiveResponse {},
|
ThreadArchiveResponse {},
|
||||||
)),
|
)),
|
||||||
|
thread_originator: None,
|
||||||
},
|
},
|
||||||
&mut events,
|
&mut events,
|
||||||
)
|
)
|
||||||
@@ -1851,6 +2010,7 @@ async fn compaction_event_ingests_custom_fact() {
|
|||||||
Some(AppServerThreadSource::Subagent),
|
Some(AppServerThreadSource::Subagent),
|
||||||
Some(parent_thread_id.to_string()),
|
Some(parent_thread_id.to_string()),
|
||||||
)),
|
)),
|
||||||
|
thread_originator: None,
|
||||||
},
|
},
|
||||||
&mut events,
|
&mut events,
|
||||||
)
|
)
|
||||||
@@ -1971,6 +2131,7 @@ async fn guardian_review_event_ingests_custom_fact_with_optional_target_item() {
|
|||||||
/*ephemeral*/ false,
|
/*ephemeral*/ false,
|
||||||
"gpt-5",
|
"gpt-5",
|
||||||
)),
|
)),
|
||||||
|
thread_originator: None,
|
||||||
},
|
},
|
||||||
&mut events,
|
&mut events,
|
||||||
)
|
)
|
||||||
@@ -2499,6 +2660,7 @@ async fn item_review_summaries_do_not_cross_threads_with_reused_item_ids() {
|
|||||||
response: Box::new(sample_thread_start_response(
|
response: Box::new(sample_thread_start_response(
|
||||||
"thread-2", /*ephemeral*/ false, "gpt-5",
|
"thread-2", /*ephemeral*/ false, "gpt-5",
|
||||||
)),
|
)),
|
||||||
|
thread_originator: None,
|
||||||
},
|
},
|
||||||
&mut events,
|
&mut events,
|
||||||
)
|
)
|
||||||
@@ -2749,7 +2911,7 @@ async fn subagent_thread_started_publishes_without_initialize() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
async fn subagent_events_use_inherited_connection_unless_turn_connection_is_explicit() {
|
async fn subagent_events_keep_thread_originator_with_explicit_turn_connection() {
|
||||||
let mut reducer = AnalyticsReducer::default();
|
let mut reducer = AnalyticsReducer::default();
|
||||||
let mut events = Vec::new();
|
let mut events = Vec::new();
|
||||||
let parent_thread_id =
|
let parent_thread_id =
|
||||||
@@ -2786,6 +2948,7 @@ async fn subagent_events_use_inherited_connection_unless_turn_connection_is_expl
|
|||||||
/*ephemeral*/ false,
|
/*ephemeral*/ false,
|
||||||
"gpt-5",
|
"gpt-5",
|
||||||
)),
|
)),
|
||||||
|
thread_originator: None,
|
||||||
},
|
},
|
||||||
&mut events,
|
&mut events,
|
||||||
)
|
)
|
||||||
@@ -2908,6 +3071,7 @@ async fn subagent_events_use_inherited_connection_unless_turn_connection_is_expl
|
|||||||
connection_id: 8,
|
connection_id: 8,
|
||||||
request_id: RequestId::Integer(3),
|
request_id: RequestId::Integer(3),
|
||||||
response: Box::new(sample_turn_start_response("turn-explicit")),
|
response: Box::new(sample_turn_start_response("turn-explicit")),
|
||||||
|
thread_originator: None,
|
||||||
},
|
},
|
||||||
&mut events,
|
&mut events,
|
||||||
)
|
)
|
||||||
@@ -2918,7 +3082,11 @@ async fn subagent_events_use_inherited_connection_unless_turn_connection_is_expl
|
|||||||
};
|
};
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
event.event_params.app_server_client.product_client_id,
|
event.event_params.app_server_client.product_client_id,
|
||||||
DEFAULT_ORIGINATOR
|
"parent-client"
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
event.event_params.app_server_client.client_name.as_deref(),
|
||||||
|
Some("codex-tui")
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -3868,6 +4036,7 @@ async fn accepted_turn_steer_emits_expected_event() {
|
|||||||
connection_id: 7,
|
connection_id: 7,
|
||||||
request_id: RequestId::Integer(4),
|
request_id: RequestId::Integer(4),
|
||||||
response: Box::new(sample_turn_steer_response("turn-2")),
|
response: Box::new(sample_turn_steer_response("turn-2")),
|
||||||
|
thread_originator: None,
|
||||||
},
|
},
|
||||||
&mut out,
|
&mut out,
|
||||||
)
|
)
|
||||||
@@ -4039,6 +4208,7 @@ async fn turn_start_error_response_discards_pending_start_request() {
|
|||||||
connection_id: 7,
|
connection_id: 7,
|
||||||
request_id: RequestId::Integer(3),
|
request_id: RequestId::Integer(3),
|
||||||
response: Box::new(sample_turn_start_response("turn-2")),
|
response: Box::new(sample_turn_start_response("turn-2")),
|
||||||
|
thread_originator: None,
|
||||||
},
|
},
|
||||||
&mut out,
|
&mut out,
|
||||||
)
|
)
|
||||||
@@ -4367,6 +4537,7 @@ async fn accepted_steers_increment_turn_steer_count() {
|
|||||||
connection_id: 7,
|
connection_id: 7,
|
||||||
request_id: RequestId::Integer(4),
|
request_id: RequestId::Integer(4),
|
||||||
response: Box::new(sample_turn_steer_response("turn-2")),
|
response: Box::new(sample_turn_steer_response("turn-2")),
|
||||||
|
thread_originator: None,
|
||||||
},
|
},
|
||||||
&mut out,
|
&mut out,
|
||||||
)
|
)
|
||||||
@@ -4414,6 +4585,7 @@ async fn accepted_steers_increment_turn_steer_count() {
|
|||||||
connection_id: 7,
|
connection_id: 7,
|
||||||
request_id: RequestId::Integer(6),
|
request_id: RequestId::Integer(6),
|
||||||
response: Box::new(sample_turn_steer_response("turn-2")),
|
response: Box::new(sample_turn_steer_response("turn-2")),
|
||||||
|
thread_originator: None,
|
||||||
},
|
},
|
||||||
&mut out,
|
&mut out,
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -435,6 +435,31 @@ impl AnalyticsEventsClient {
|
|||||||
connection_id: u64,
|
connection_id: u64,
|
||||||
request_id: RequestId,
|
request_id: RequestId,
|
||||||
response: ClientResponsePayload,
|
response: ClientResponsePayload,
|
||||||
|
) {
|
||||||
|
self.track_response_inner(
|
||||||
|
connection_id,
|
||||||
|
request_id,
|
||||||
|
response,
|
||||||
|
/*thread_originator*/ None,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn track_response_with_thread_originator(
|
||||||
|
&self,
|
||||||
|
connection_id: u64,
|
||||||
|
request_id: RequestId,
|
||||||
|
response: ClientResponsePayload,
|
||||||
|
thread_originator: String,
|
||||||
|
) {
|
||||||
|
self.track_response_inner(connection_id, request_id, response, Some(thread_originator));
|
||||||
|
}
|
||||||
|
|
||||||
|
fn track_response_inner(
|
||||||
|
&self,
|
||||||
|
connection_id: u64,
|
||||||
|
request_id: RequestId,
|
||||||
|
response: ClientResponsePayload,
|
||||||
|
thread_originator: Option<String>,
|
||||||
) {
|
) {
|
||||||
if !matches!(
|
if !matches!(
|
||||||
response,
|
response,
|
||||||
@@ -450,6 +475,7 @@ impl AnalyticsEventsClient {
|
|||||||
connection_id,
|
connection_id,
|
||||||
request_id,
|
request_id,
|
||||||
response: Box::new(response),
|
response: Box::new(response),
|
||||||
|
thread_originator,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -464,6 +464,7 @@ pub(crate) enum AnalyticsFact {
|
|||||||
connection_id: u64,
|
connection_id: u64,
|
||||||
request_id: RequestId,
|
request_id: RequestId,
|
||||||
response: Box<ClientResponsePayload>,
|
response: Box<ClientResponsePayload>,
|
||||||
|
thread_originator: Option<String>,
|
||||||
},
|
},
|
||||||
ErrorResponse {
|
ErrorResponse {
|
||||||
connection_id: u64,
|
connection_id: u64,
|
||||||
|
|||||||
@@ -165,6 +165,20 @@ struct ConnectionState {
|
|||||||
struct ThreadAnalyticsState {
|
struct ThreadAnalyticsState {
|
||||||
connection_id: Option<u64>,
|
connection_id: Option<u64>,
|
||||||
metadata: Option<ThreadMetadataState>,
|
metadata: Option<ThreadMetadataState>,
|
||||||
|
originator: Option<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ThreadAnalyticsState {
|
||||||
|
fn app_server_client(
|
||||||
|
&self,
|
||||||
|
connection_state: &ConnectionState,
|
||||||
|
) -> CodexAppServerClientMetadata {
|
||||||
|
let mut app_server_client = connection_state.app_server_client.clone();
|
||||||
|
if let Some(originator) = self.originator.as_ref() {
|
||||||
|
app_server_client.product_client_id.clone_from(originator);
|
||||||
|
}
|
||||||
|
app_server_client
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone, Copy)]
|
#[derive(Clone, Copy)]
|
||||||
@@ -434,9 +448,11 @@ impl AnalyticsReducer {
|
|||||||
connection_id,
|
connection_id,
|
||||||
request_id,
|
request_id,
|
||||||
response,
|
response,
|
||||||
|
thread_originator,
|
||||||
} => {
|
} => {
|
||||||
if let Some(response) = response.into_client_response(request_id) {
|
if let Some(response) = response.into_client_response(request_id) {
|
||||||
self.ingest_response(connection_id, response, out).await;
|
self.ingest_response(connection_id, response, thread_originator, out)
|
||||||
|
.await;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
AnalyticsFact::ErrorResponse {
|
AnalyticsFact::ErrorResponse {
|
||||||
@@ -575,6 +591,9 @@ impl AnalyticsReducer {
|
|||||||
.and_then(|parent_thread_id| self.threads.get(parent_thread_id))
|
.and_then(|parent_thread_id| self.threads.get(parent_thread_id))
|
||||||
.and_then(|thread| thread.connection_id);
|
.and_then(|thread| thread.connection_id);
|
||||||
let thread_state = self.threads.entry(input.thread_id.clone()).or_default();
|
let thread_state = self.threads.entry(input.thread_id.clone()).or_default();
|
||||||
|
thread_state
|
||||||
|
.originator
|
||||||
|
.get_or_insert_with(|| input.product_client_id.clone());
|
||||||
thread_state
|
thread_state
|
||||||
.metadata
|
.metadata
|
||||||
.get_or_insert_with(|| ThreadMetadataState {
|
.get_or_insert_with(|| ThreadMetadataState {
|
||||||
@@ -597,7 +616,7 @@ impl AnalyticsReducer {
|
|||||||
input: GuardianReviewEventParams,
|
input: GuardianReviewEventParams,
|
||||||
out: &mut Vec<TrackEventRequest>,
|
out: &mut Vec<TrackEventRequest>,
|
||||||
) {
|
) {
|
||||||
let Some((connection_state, thread_metadata)) =
|
let Some((connection_state, thread_state, thread_metadata)) =
|
||||||
self.thread_context_or_warn(AnalyticsDropSite::guardian(&input))
|
self.thread_context_or_warn(AnalyticsDropSite::guardian(&input))
|
||||||
else {
|
else {
|
||||||
return;
|
return;
|
||||||
@@ -607,7 +626,7 @@ impl AnalyticsReducer {
|
|||||||
event_type: "codex_guardian_review",
|
event_type: "codex_guardian_review",
|
||||||
event_params: GuardianReviewEventPayload {
|
event_params: GuardianReviewEventPayload {
|
||||||
session_id: thread_metadata.session_id.clone(),
|
session_id: thread_metadata.session_id.clone(),
|
||||||
app_server_client: connection_state.app_server_client.clone(),
|
app_server_client: thread_state.app_server_client(connection_state),
|
||||||
runtime: connection_state.runtime.clone(),
|
runtime: connection_state.runtime.clone(),
|
||||||
guardian_review: input,
|
guardian_review: input,
|
||||||
},
|
},
|
||||||
@@ -874,6 +893,7 @@ impl AnalyticsReducer {
|
|||||||
&mut self,
|
&mut self,
|
||||||
connection_id: u64,
|
connection_id: u64,
|
||||||
response: ClientResponse,
|
response: ClientResponse,
|
||||||
|
thread_originator: Option<String>,
|
||||||
out: &mut Vec<TrackEventRequest>,
|
out: &mut Vec<TrackEventRequest>,
|
||||||
) {
|
) {
|
||||||
match response {
|
match response {
|
||||||
@@ -883,6 +903,7 @@ impl AnalyticsReducer {
|
|||||||
response.thread,
|
response.thread,
|
||||||
response.model,
|
response.model,
|
||||||
ThreadInitializationMode::New,
|
ThreadInitializationMode::New,
|
||||||
|
thread_originator,
|
||||||
out,
|
out,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -892,6 +913,7 @@ impl AnalyticsReducer {
|
|||||||
response.thread,
|
response.thread,
|
||||||
response.model,
|
response.model,
|
||||||
ThreadInitializationMode::Resumed,
|
ThreadInitializationMode::Resumed,
|
||||||
|
thread_originator,
|
||||||
out,
|
out,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -901,6 +923,7 @@ impl AnalyticsReducer {
|
|||||||
response.thread,
|
response.thread,
|
||||||
response.model,
|
response.model,
|
||||||
ThreadInitializationMode::Forked,
|
ThreadInitializationMode::Forked,
|
||||||
|
thread_originator,
|
||||||
out,
|
out,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -1248,7 +1271,7 @@ impl AnalyticsReducer {
|
|||||||
else {
|
else {
|
||||||
return;
|
return;
|
||||||
};
|
};
|
||||||
let Some((connection_state, thread_metadata)) = self
|
let Some((connection_state, thread_state, thread_metadata)) = self
|
||||||
.thread_context_or_warn(AnalyticsDropSite::tool_item(¬ification, item_id))
|
.thread_context_or_warn(AnalyticsDropSite::tool_item(¬ification, item_id))
|
||||||
else {
|
else {
|
||||||
return;
|
return;
|
||||||
@@ -1260,6 +1283,7 @@ impl AnalyticsReducer {
|
|||||||
started_at_ms,
|
started_at_ms,
|
||||||
completed_at_ms,
|
completed_at_ms,
|
||||||
connection_state,
|
connection_state,
|
||||||
|
thread_state,
|
||||||
thread_metadata,
|
thread_metadata,
|
||||||
review_summary: self.item_review_summaries.get(&key),
|
review_summary: self.item_review_summaries.get(&key),
|
||||||
}) {
|
}) {
|
||||||
@@ -1316,6 +1340,7 @@ impl AnalyticsReducer {
|
|||||||
thread: codex_app_server_protocol::Thread,
|
thread: codex_app_server_protocol::Thread,
|
||||||
model: String,
|
model: String,
|
||||||
initialization_mode: ThreadInitializationMode,
|
initialization_mode: ThreadInitializationMode,
|
||||||
|
thread_originator: Option<String>,
|
||||||
out: &mut Vec<TrackEventRequest>,
|
out: &mut Vec<TrackEventRequest>,
|
||||||
) {
|
) {
|
||||||
let session_source: SessionSource = thread.source.into();
|
let session_source: SessionSource = thread.source.into();
|
||||||
@@ -1333,20 +1358,20 @@ impl AnalyticsReducer {
|
|||||||
parent_thread_id,
|
parent_thread_id,
|
||||||
initialization_mode,
|
initialization_mode,
|
||||||
);
|
);
|
||||||
self.threads.insert(
|
let thread_state = self.threads.entry(thread_id.clone()).or_default();
|
||||||
thread_id.clone(),
|
if let Some(originator) = thread_originator {
|
||||||
ThreadAnalyticsState {
|
thread_state.originator = Some(originator);
|
||||||
connection_id: Some(connection_id),
|
}
|
||||||
metadata: Some(thread_metadata.clone()),
|
thread_state.connection_id = Some(connection_id);
|
||||||
},
|
thread_state.metadata = Some(thread_metadata.clone());
|
||||||
);
|
let app_server_client = thread_state.app_server_client(connection_state);
|
||||||
out.push(TrackEventRequest::ThreadInitialized(
|
out.push(TrackEventRequest::ThreadInitialized(
|
||||||
ThreadInitializedEvent {
|
ThreadInitializedEvent {
|
||||||
event_type: "codex_thread_initialized",
|
event_type: "codex_thread_initialized",
|
||||||
event_params: ThreadInitializedEventParams {
|
event_params: ThreadInitializedEventParams {
|
||||||
thread_id,
|
thread_id,
|
||||||
session_id,
|
session_id,
|
||||||
app_server_client: connection_state.app_server_client.clone(),
|
app_server_client,
|
||||||
runtime: connection_state.runtime.clone(),
|
runtime: connection_state.runtime.clone(),
|
||||||
model,
|
model,
|
||||||
ephemeral: thread.ephemeral,
|
ephemeral: thread.ephemeral,
|
||||||
@@ -1362,7 +1387,7 @@ impl AnalyticsReducer {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn ingest_compaction(&mut self, input: CodexCompactionEvent, out: &mut Vec<TrackEventRequest>) {
|
fn ingest_compaction(&mut self, input: CodexCompactionEvent, out: &mut Vec<TrackEventRequest>) {
|
||||||
let Some((connection_state, thread_metadata)) =
|
let Some((connection_state, thread_state, thread_metadata)) =
|
||||||
self.thread_context_or_warn(AnalyticsDropSite::compaction(&input))
|
self.thread_context_or_warn(AnalyticsDropSite::compaction(&input))
|
||||||
else {
|
else {
|
||||||
return;
|
return;
|
||||||
@@ -1373,7 +1398,7 @@ impl AnalyticsReducer {
|
|||||||
event_params: codex_compaction_event_params(
|
event_params: codex_compaction_event_params(
|
||||||
input,
|
input,
|
||||||
thread_metadata.session_id.clone(),
|
thread_metadata.session_id.clone(),
|
||||||
connection_state.app_server_client.clone(),
|
thread_state.app_server_client(connection_state),
|
||||||
connection_state.runtime.clone(),
|
connection_state.runtime.clone(),
|
||||||
thread_metadata.thread_source.clone(),
|
thread_metadata.thread_source.clone(),
|
||||||
thread_metadata.subagent_source.clone(),
|
thread_metadata.subagent_source.clone(),
|
||||||
@@ -1384,7 +1409,7 @@ impl AnalyticsReducer {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn ingest_goal(&mut self, input: CodexGoalEvent, out: &mut Vec<TrackEventRequest>) {
|
fn ingest_goal(&mut self, input: CodexGoalEvent, out: &mut Vec<TrackEventRequest>) {
|
||||||
let Some((connection_state, thread_metadata)) =
|
let Some((connection_state, thread_state, thread_metadata)) =
|
||||||
self.thread_context_or_warn(AnalyticsDropSite::goal(&input))
|
self.thread_context_or_warn(AnalyticsDropSite::goal(&input))
|
||||||
else {
|
else {
|
||||||
return;
|
return;
|
||||||
@@ -1394,7 +1419,7 @@ impl AnalyticsReducer {
|
|||||||
event_params: codex_goal_event_params(
|
event_params: codex_goal_event_params(
|
||||||
input,
|
input,
|
||||||
thread_metadata.session_id.clone(),
|
thread_metadata.session_id.clone(),
|
||||||
connection_state.app_server_client.clone(),
|
thread_state.app_server_client(connection_state),
|
||||||
connection_state.runtime.clone(),
|
connection_state.runtime.clone(),
|
||||||
thread_metadata.thread_source.clone(),
|
thread_metadata.thread_source.clone(),
|
||||||
thread_metadata.subagent_source.clone(),
|
thread_metadata.subagent_source.clone(),
|
||||||
@@ -1483,11 +1508,11 @@ impl AnalyticsReducer {
|
|||||||
return;
|
return;
|
||||||
};
|
};
|
||||||
let drop_site = AnalyticsDropSite::turn_steer(&pending_request.thread_id);
|
let drop_site = AnalyticsDropSite::turn_steer(&pending_request.thread_id);
|
||||||
let Some(thread_metadata) = self
|
let Some(thread_state) = self.threads.get(drop_site.thread_id) else {
|
||||||
.threads
|
warn_missing_analytics_context(&drop_site, MissingAnalyticsContext::ThreadMetadata);
|
||||||
.get(drop_site.thread_id)
|
return;
|
||||||
.and_then(|thread| thread.metadata.as_ref())
|
};
|
||||||
else {
|
let Some(thread_metadata) = thread_state.metadata.as_ref() else {
|
||||||
warn_missing_analytics_context(&drop_site, MissingAnalyticsContext::ThreadMetadata);
|
warn_missing_analytics_context(&drop_site, MissingAnalyticsContext::ThreadMetadata);
|
||||||
return;
|
return;
|
||||||
};
|
};
|
||||||
@@ -1498,7 +1523,7 @@ impl AnalyticsReducer {
|
|||||||
session_id: thread_metadata.session_id.clone(),
|
session_id: thread_metadata.session_id.clone(),
|
||||||
expected_turn_id: Some(pending_request.expected_turn_id),
|
expected_turn_id: Some(pending_request.expected_turn_id),
|
||||||
accepted_turn_id,
|
accepted_turn_id,
|
||||||
app_server_client: connection_state.app_server_client.clone(),
|
app_server_client: thread_state.app_server_client(connection_state),
|
||||||
runtime: connection_state.runtime.clone(),
|
runtime: connection_state.runtime.clone(),
|
||||||
thread_source: thread_metadata.thread_source.clone(),
|
thread_source: thread_metadata.thread_source.clone(),
|
||||||
subagent_source: thread_metadata.subagent_source.clone(),
|
subagent_source: thread_metadata.subagent_source.clone(),
|
||||||
@@ -1529,7 +1554,7 @@ impl AnalyticsReducer {
|
|||||||
&pending_review,
|
&pending_review,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
let Some((connection_state, thread_metadata)) =
|
let Some((connection_state, thread_state, thread_metadata)) =
|
||||||
self.thread_context_or_warn(AnalyticsDropSite::review(&pending_review))
|
self.thread_context_or_warn(AnalyticsDropSite::review(&pending_review))
|
||||||
else {
|
else {
|
||||||
return;
|
return;
|
||||||
@@ -1541,7 +1566,7 @@ impl AnalyticsReducer {
|
|||||||
turn_id: pending_review.turn_id,
|
turn_id: pending_review.turn_id,
|
||||||
item_id: pending_review.item_id,
|
item_id: pending_review.item_id,
|
||||||
review_id: pending_review.review_id,
|
review_id: pending_review.review_id,
|
||||||
app_server_client: connection_state.app_server_client.clone(),
|
app_server_client: thread_state.app_server_client(connection_state),
|
||||||
runtime: connection_state.runtime.clone(),
|
runtime: connection_state.runtime.clone(),
|
||||||
thread_source: thread_metadata.thread_source.clone(),
|
thread_source: thread_metadata.thread_source.clone(),
|
||||||
subagent_source: thread_metadata.subagent_source.clone(),
|
subagent_source: thread_metadata.subagent_source.clone(),
|
||||||
@@ -1610,18 +1635,18 @@ impl AnalyticsReducer {
|
|||||||
);
|
);
|
||||||
return;
|
return;
|
||||||
};
|
};
|
||||||
let Some(thread_metadata) = self
|
let Some(thread_state) = self.threads.get(drop_site.thread_id) else {
|
||||||
.threads
|
warn_missing_analytics_context(&drop_site, MissingAnalyticsContext::ThreadMetadata);
|
||||||
.get(drop_site.thread_id)
|
return;
|
||||||
.and_then(|thread| thread.metadata.as_ref())
|
};
|
||||||
else {
|
let Some(thread_metadata) = thread_state.metadata.as_ref() else {
|
||||||
warn_missing_analytics_context(&drop_site, MissingAnalyticsContext::ThreadMetadata);
|
warn_missing_analytics_context(&drop_site, MissingAnalyticsContext::ThreadMetadata);
|
||||||
return;
|
return;
|
||||||
};
|
};
|
||||||
let turn_event = TrackEventRequest::TurnEvent(Box::new(CodexTurnEventRequest {
|
let turn_event = TrackEventRequest::TurnEvent(Box::new(CodexTurnEventRequest {
|
||||||
event_type: "codex_turn_event",
|
event_type: "codex_turn_event",
|
||||||
event_params: codex_turn_event_params(
|
event_params: codex_turn_event_params(
|
||||||
connection_state.app_server_client.clone(),
|
thread_state.app_server_client(connection_state),
|
||||||
connection_state.runtime.clone(),
|
connection_state.runtime.clone(),
|
||||||
turn_id.to_string(),
|
turn_id.to_string(),
|
||||||
turn_state,
|
turn_state,
|
||||||
@@ -1663,17 +1688,18 @@ impl AnalyticsReducer {
|
|||||||
fn thread_context_or_warn(
|
fn thread_context_or_warn(
|
||||||
&self,
|
&self,
|
||||||
drop_site: AnalyticsDropSite<'_>,
|
drop_site: AnalyticsDropSite<'_>,
|
||||||
) -> Option<(&ConnectionState, &ThreadMetadataState)> {
|
) -> Option<(
|
||||||
|
&ConnectionState,
|
||||||
|
&ThreadAnalyticsState,
|
||||||
|
&ThreadMetadataState,
|
||||||
|
)> {
|
||||||
let connection_state = self.thread_connection_or_warn(drop_site)?;
|
let connection_state = self.thread_connection_or_warn(drop_site)?;
|
||||||
let Some(thread_metadata) = self
|
let thread_state = self.threads.get(drop_site.thread_id)?;
|
||||||
.threads
|
let Some(thread_metadata) = thread_state.metadata.as_ref() else {
|
||||||
.get(drop_site.thread_id)
|
|
||||||
.and_then(|thread| thread.metadata.as_ref())
|
|
||||||
else {
|
|
||||||
warn_missing_analytics_context(&drop_site, MissingAnalyticsContext::ThreadMetadata);
|
warn_missing_analytics_context(&drop_site, MissingAnalyticsContext::ThreadMetadata);
|
||||||
return None;
|
return None;
|
||||||
};
|
};
|
||||||
Some((connection_state, thread_metadata))
|
Some((connection_state, thread_state, thread_metadata))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1743,6 +1769,7 @@ struct ToolItemEventInput<'a> {
|
|||||||
started_at_ms: u64,
|
started_at_ms: u64,
|
||||||
completed_at_ms: u64,
|
completed_at_ms: u64,
|
||||||
connection_state: &'a ConnectionState,
|
connection_state: &'a ConnectionState,
|
||||||
|
thread_state: &'a ThreadAnalyticsState,
|
||||||
thread_metadata: &'a ThreadMetadataState,
|
thread_metadata: &'a ThreadMetadataState,
|
||||||
review_summary: Option<&'a ItemReviewSummary>,
|
review_summary: Option<&'a ItemReviewSummary>,
|
||||||
}
|
}
|
||||||
@@ -1755,6 +1782,7 @@ fn tool_item_event(input: ToolItemEventInput<'_>) -> Option<TrackEventRequest> {
|
|||||||
started_at_ms,
|
started_at_ms,
|
||||||
completed_at_ms,
|
completed_at_ms,
|
||||||
connection_state,
|
connection_state,
|
||||||
|
thread_state,
|
||||||
thread_metadata,
|
thread_metadata,
|
||||||
review_summary,
|
review_summary,
|
||||||
} = input;
|
} = input;
|
||||||
@@ -1784,6 +1812,7 @@ fn tool_item_event(input: ToolItemEventInput<'_>) -> Option<TrackEventRequest> {
|
|||||||
started_at_ms,
|
started_at_ms,
|
||||||
completed_at_ms,
|
completed_at_ms,
|
||||||
connection_state,
|
connection_state,
|
||||||
|
thread_state,
|
||||||
thread_metadata,
|
thread_metadata,
|
||||||
review_summary,
|
review_summary,
|
||||||
},
|
},
|
||||||
@@ -1825,6 +1854,7 @@ fn tool_item_event(input: ToolItemEventInput<'_>) -> Option<TrackEventRequest> {
|
|||||||
started_at_ms,
|
started_at_ms,
|
||||||
completed_at_ms,
|
completed_at_ms,
|
||||||
connection_state,
|
connection_state,
|
||||||
|
thread_state,
|
||||||
thread_metadata,
|
thread_metadata,
|
||||||
review_summary,
|
review_summary,
|
||||||
},
|
},
|
||||||
@@ -1866,6 +1896,7 @@ fn tool_item_event(input: ToolItemEventInput<'_>) -> Option<TrackEventRequest> {
|
|||||||
started_at_ms,
|
started_at_ms,
|
||||||
completed_at_ms,
|
completed_at_ms,
|
||||||
connection_state,
|
connection_state,
|
||||||
|
thread_state,
|
||||||
thread_metadata,
|
thread_metadata,
|
||||||
review_summary,
|
review_summary,
|
||||||
},
|
},
|
||||||
@@ -1910,6 +1941,7 @@ fn tool_item_event(input: ToolItemEventInput<'_>) -> Option<TrackEventRequest> {
|
|||||||
started_at_ms,
|
started_at_ms,
|
||||||
completed_at_ms,
|
completed_at_ms,
|
||||||
connection_state,
|
connection_state,
|
||||||
|
thread_state,
|
||||||
thread_metadata,
|
thread_metadata,
|
||||||
review_summary,
|
review_summary,
|
||||||
},
|
},
|
||||||
@@ -1954,6 +1986,7 @@ fn tool_item_event(input: ToolItemEventInput<'_>) -> Option<TrackEventRequest> {
|
|||||||
started_at_ms,
|
started_at_ms,
|
||||||
completed_at_ms,
|
completed_at_ms,
|
||||||
connection_state,
|
connection_state,
|
||||||
|
thread_state,
|
||||||
thread_metadata,
|
thread_metadata,
|
||||||
review_summary,
|
review_summary,
|
||||||
},
|
},
|
||||||
@@ -2009,6 +2042,7 @@ fn tool_item_event(input: ToolItemEventInput<'_>) -> Option<TrackEventRequest> {
|
|||||||
started_at_ms,
|
started_at_ms,
|
||||||
completed_at_ms,
|
completed_at_ms,
|
||||||
connection_state,
|
connection_state,
|
||||||
|
thread_state,
|
||||||
thread_metadata,
|
thread_metadata,
|
||||||
review_summary,
|
review_summary,
|
||||||
},
|
},
|
||||||
@@ -2045,6 +2079,7 @@ fn tool_item_event(input: ToolItemEventInput<'_>) -> Option<TrackEventRequest> {
|
|||||||
started_at_ms,
|
started_at_ms,
|
||||||
completed_at_ms,
|
completed_at_ms,
|
||||||
connection_state,
|
connection_state,
|
||||||
|
thread_state,
|
||||||
thread_metadata,
|
thread_metadata,
|
||||||
review_summary,
|
review_summary,
|
||||||
},
|
},
|
||||||
@@ -2100,6 +2135,7 @@ struct ToolItemContext<'a> {
|
|||||||
started_at_ms: u64,
|
started_at_ms: u64,
|
||||||
completed_at_ms: u64,
|
completed_at_ms: u64,
|
||||||
connection_state: &'a ConnectionState,
|
connection_state: &'a ConnectionState,
|
||||||
|
thread_state: &'a ThreadAnalyticsState,
|
||||||
thread_metadata: &'a ThreadMetadataState,
|
thread_metadata: &'a ThreadMetadataState,
|
||||||
review_summary: Option<&'a ItemReviewSummary>,
|
review_summary: Option<&'a ItemReviewSummary>,
|
||||||
}
|
}
|
||||||
@@ -2118,7 +2154,9 @@ fn tool_item_base(
|
|||||||
thread_id: thread_id.to_string(),
|
thread_id: thread_id.to_string(),
|
||||||
turn_id: turn_id.to_string(),
|
turn_id: turn_id.to_string(),
|
||||||
item_id,
|
item_id,
|
||||||
app_server_client: context.connection_state.app_server_client.clone(),
|
app_server_client: context
|
||||||
|
.thread_state
|
||||||
|
.app_server_client(context.connection_state),
|
||||||
runtime: context.connection_state.runtime.clone(),
|
runtime: context.connection_state.runtime.clone(),
|
||||||
thread_source: thread_metadata.thread_source.clone(),
|
thread_source: thread_metadata.thread_source.clone(),
|
||||||
subagent_source: thread_metadata.subagent_source.clone(),
|
subagent_source: thread_metadata.subagent_source.clone(),
|
||||||
|
|||||||
@@ -504,13 +504,36 @@ impl OutgoingMessageSender {
|
|||||||
where
|
where
|
||||||
T: Into<ClientResponsePayload>,
|
T: Into<ClientResponsePayload>,
|
||||||
{
|
{
|
||||||
self.send_response_as(request_id, response.into()).await;
|
self.send_response_as_inner(request_id, response.into(), /*thread_originator*/ None)
|
||||||
|
.await;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) async fn send_response_with_thread_originator<T>(
|
||||||
|
&self,
|
||||||
|
request_id: ConnectionRequestId,
|
||||||
|
response: T,
|
||||||
|
thread_originator: String,
|
||||||
|
) where
|
||||||
|
T: Into<ClientResponsePayload>,
|
||||||
|
{
|
||||||
|
self.send_response_as_inner(request_id, response.into(), Some(thread_originator))
|
||||||
|
.await;
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) async fn send_response_as(
|
pub(crate) async fn send_response_as(
|
||||||
&self,
|
&self,
|
||||||
request_id: ConnectionRequestId,
|
request_id: ConnectionRequestId,
|
||||||
response: ClientResponsePayload,
|
response: ClientResponsePayload,
|
||||||
|
) {
|
||||||
|
self.send_response_as_inner(request_id, response, /*thread_originator*/ None)
|
||||||
|
.await;
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn send_response_as_inner(
|
||||||
|
&self,
|
||||||
|
request_id: ConnectionRequestId,
|
||||||
|
response: ClientResponsePayload,
|
||||||
|
thread_originator: Option<String>,
|
||||||
) {
|
) {
|
||||||
let connection_id = request_id.connection_id;
|
let connection_id = request_id.connection_id;
|
||||||
let request_id_for_analytics = request_id.request_id.clone();
|
let request_id_for_analytics = request_id.request_id.clone();
|
||||||
@@ -518,11 +541,24 @@ impl OutgoingMessageSender {
|
|||||||
.into_jsonrpc_parts_and_payload(request_id.request_id.clone())
|
.into_jsonrpc_parts_and_payload(request_id.request_id.clone())
|
||||||
.map(|(id, result, response)| {
|
.map(|(id, result, response)| {
|
||||||
if let Some(response) = response {
|
if let Some(response) = response {
|
||||||
self.analytics_events_client.track_response(
|
match thread_originator {
|
||||||
connection_id.0,
|
Some(thread_originator) => {
|
||||||
request_id_for_analytics,
|
self.analytics_events_client
|
||||||
response,
|
.track_response_with_thread_originator(
|
||||||
);
|
connection_id.0,
|
||||||
|
request_id_for_analytics,
|
||||||
|
response,
|
||||||
|
thread_originator,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
None => {
|
||||||
|
self.analytics_events_client.track_response(
|
||||||
|
connection_id.0,
|
||||||
|
request_id_for_analytics,
|
||||||
|
response,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
(id, result)
|
(id, result)
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -640,6 +640,7 @@ pub(super) async fn handle_pending_thread_resume_request(
|
|||||||
active_permission_profile,
|
active_permission_profile,
|
||||||
workspace_roots,
|
workspace_roots,
|
||||||
reasoning_effort,
|
reasoning_effort,
|
||||||
|
originator,
|
||||||
..
|
..
|
||||||
} = config_snapshot;
|
} = config_snapshot;
|
||||||
let instruction_sources = pending.instruction_sources;
|
let instruction_sources = pending.instruction_sources;
|
||||||
@@ -665,7 +666,9 @@ pub(super) async fn handle_pending_thread_resume_request(
|
|||||||
multi_agent_mode: MultiAgentMode::ExplicitRequestOnly,
|
multi_agent_mode: MultiAgentMode::ExplicitRequestOnly,
|
||||||
initial_turns_page,
|
initial_turns_page,
|
||||||
};
|
};
|
||||||
outgoing.send_response(request_id, response).await;
|
outgoing
|
||||||
|
.send_response_with_thread_originator(request_id, response, originator)
|
||||||
|
.await;
|
||||||
// Match cold resume: metadata-only resume should attach the listener without
|
// Match cold resume: metadata-only resume should attach the listener without
|
||||||
// paying the cost of turn reconstruction for historical usage replay.
|
// paying the cost of turn reconstruction for historical usage replay.
|
||||||
if let Some(token_usage_thread) = token_usage_thread {
|
if let Some(token_usage_thread) = token_usage_thread {
|
||||||
|
|||||||
@@ -1268,6 +1268,7 @@ impl ThreadRequestProcessor {
|
|||||||
let cwd = config_snapshot.cwd().clone();
|
let cwd = config_snapshot.cwd().clone();
|
||||||
let active_permission_profile =
|
let active_permission_profile =
|
||||||
thread_response_active_permission_profile(config_snapshot.active_permission_profile);
|
thread_response_active_permission_profile(config_snapshot.active_permission_profile);
|
||||||
|
let thread_originator = config_snapshot.originator.clone();
|
||||||
|
|
||||||
let response = ThreadStartResponse {
|
let response = ThreadStartResponse {
|
||||||
thread: thread.clone(),
|
thread: thread.clone(),
|
||||||
@@ -1287,7 +1288,7 @@ impl ThreadRequestProcessor {
|
|||||||
let notif = thread_started_notification(thread);
|
let notif = thread_started_notification(thread);
|
||||||
listener_task_context
|
listener_task_context
|
||||||
.outgoing
|
.outgoing
|
||||||
.send_response(request_id, response)
|
.send_response_with_thread_originator(request_id, response, thread_originator)
|
||||||
.instrument(tracing::info_span!(
|
.instrument(tracing::info_span!(
|
||||||
"app_server.thread_start.send_response",
|
"app_server.thread_start.send_response",
|
||||||
otel.name = "app_server.thread_start.send_response",
|
otel.name = "app_server.thread_start.send_response",
|
||||||
@@ -2863,6 +2864,7 @@ impl ThreadRequestProcessor {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let thread_originator = config_snapshot.originator.clone();
|
||||||
let response = ThreadResumeResponse {
|
let response = ThreadResumeResponse {
|
||||||
thread,
|
thread,
|
||||||
model: session_configured.model,
|
model: session_configured.model,
|
||||||
@@ -2881,7 +2883,9 @@ impl ThreadRequestProcessor {
|
|||||||
};
|
};
|
||||||
|
|
||||||
let connection_id = request_id.connection_id;
|
let connection_id = request_id.connection_id;
|
||||||
self.outgoing.send_response(request_id, response).await;
|
self.outgoing
|
||||||
|
.send_response_with_thread_originator(request_id, response, thread_originator)
|
||||||
|
.await;
|
||||||
// `excludeTurns` is explicitly the cheap resume path, so avoid
|
// `excludeTurns` is explicitly the cheap resume path, so avoid
|
||||||
// rebuilding history only to attribute a replayed usage update.
|
// rebuilding history only to attribute a replayed usage update.
|
||||||
if let Some(token_usage_thread) = token_usage_thread {
|
if let Some(token_usage_thread) = token_usage_thread {
|
||||||
@@ -3582,6 +3586,7 @@ impl ThreadRequestProcessor {
|
|||||||
);
|
);
|
||||||
let active_permission_profile =
|
let active_permission_profile =
|
||||||
thread_response_active_permission_profile(config_snapshot.active_permission_profile);
|
thread_response_active_permission_profile(config_snapshot.active_permission_profile);
|
||||||
|
let thread_originator = config_snapshot.originator.clone();
|
||||||
|
|
||||||
let response = ThreadForkResponse {
|
let response = ThreadForkResponse {
|
||||||
thread: thread.clone(),
|
thread: thread.clone(),
|
||||||
@@ -3602,7 +3607,9 @@ impl ThreadRequestProcessor {
|
|||||||
let notif = thread_started_notification(thread);
|
let notif = thread_started_notification(thread);
|
||||||
let connection_id = request_id.connection_id;
|
let connection_id = request_id.connection_id;
|
||||||
let token_usage_thread = include_turns.then(|| response.thread.clone());
|
let token_usage_thread = include_turns.then(|| response.thread.clone());
|
||||||
self.outgoing.send_response(request_id, response).await;
|
self.outgoing
|
||||||
|
.send_response_with_thread_originator(request_id, response, thread_originator)
|
||||||
|
.await;
|
||||||
// `excludeTurns` is the cheap fork path, so skip restored usage replay
|
// `excludeTurns` is the cheap fork path, so skip restored usage replay
|
||||||
// instead of rebuilding history only to attribute a historical update.
|
// instead of rebuilding history only to attribute a historical update.
|
||||||
if let Some(token_usage_thread) = token_usage_thread {
|
if let Some(token_usage_thread) = token_usage_thread {
|
||||||
|
|||||||
@@ -145,7 +145,7 @@ pub(crate) async fn wait_for_goal_event(
|
|||||||
.await
|
.await
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn wait_for_matching_analytics_event(
|
pub(crate) async fn wait_for_matching_analytics_event(
|
||||||
server: &MockServer,
|
server: &MockServer,
|
||||||
read_timeout: Duration,
|
read_timeout: Duration,
|
||||||
matches: impl Fn(&Value) -> bool,
|
matches: impl Fn(&Value) -> bool,
|
||||||
@@ -191,6 +191,7 @@ pub(crate) fn assert_basic_thread_initialized_event(
|
|||||||
event: &Value,
|
event: &Value,
|
||||||
thread_id: &str,
|
thread_id: &str,
|
||||||
session_id: &str,
|
session_id: &str,
|
||||||
|
expected_product_client_id: &str,
|
||||||
expected_model: &str,
|
expected_model: &str,
|
||||||
initialization_mode: &str,
|
initialization_mode: &str,
|
||||||
expected_thread_source: &str,
|
expected_thread_source: &str,
|
||||||
@@ -199,7 +200,7 @@ pub(crate) fn assert_basic_thread_initialized_event(
|
|||||||
assert_eq!(event["event_params"]["session_id"], session_id);
|
assert_eq!(event["event_params"]["session_id"], session_id);
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
event["event_params"]["app_server_client"]["product_client_id"],
|
event["event_params"]["app_server_client"]["product_client_id"],
|
||||||
DEFAULT_CLIENT_NAME
|
expected_product_client_id
|
||||||
);
|
);
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
event["event_params"]["app_server_client"]["client_name"],
|
event["event_params"]["app_server_client"]["client_name"],
|
||||||
|
|||||||
@@ -488,6 +488,7 @@ async fn thread_fork_tracks_thread_initialized_analytics() -> Result<()> {
|
|||||||
event,
|
event,
|
||||||
&thread.id,
|
&thread.id,
|
||||||
&thread.session_id,
|
&thread.session_id,
|
||||||
|
"codex",
|
||||||
"mock-model",
|
"mock-model",
|
||||||
"forked",
|
"forked",
|
||||||
"user",
|
"user",
|
||||||
|
|||||||
@@ -107,6 +107,7 @@ use super::analytics::mount_analytics_capture;
|
|||||||
use super::analytics::thread_initialized_event;
|
use super::analytics::thread_initialized_event;
|
||||||
use super::analytics::wait_for_analytics_payload;
|
use super::analytics::wait_for_analytics_payload;
|
||||||
use super::analytics::wait_for_goal_event;
|
use super::analytics::wait_for_goal_event;
|
||||||
|
use super::analytics::wait_for_matching_analytics_event;
|
||||||
|
|
||||||
#[cfg(windows)]
|
#[cfg(windows)]
|
||||||
const DEFAULT_READ_TIMEOUT: std::time::Duration = std::time::Duration::from_secs(25);
|
const DEFAULT_READ_TIMEOUT: std::time::Duration = std::time::Duration::from_secs(25);
|
||||||
@@ -573,11 +574,12 @@ async fn thread_resume_tracks_thread_initialized_analytics() -> Result<()> {
|
|||||||
Some("mock_provider"),
|
Some("mock_provider"),
|
||||||
/*git_info*/ None,
|
/*git_info*/ None,
|
||||||
)?;
|
)?;
|
||||||
set_thread_source_on_fake_rollout(
|
set_session_meta_on_fake_rollout(
|
||||||
codex_home.path(),
|
codex_home.path(),
|
||||||
"2025-01-05T12-00-00",
|
"2025-01-05T12-00-00",
|
||||||
&conversation_id,
|
&conversation_id,
|
||||||
"user",
|
"user",
|
||||||
|
"codex_work_desktop",
|
||||||
)?;
|
)?;
|
||||||
|
|
||||||
let mut mcp = TestAppServer::new_without_managed_config(codex_home.path()).await?;
|
let mut mcp = TestAppServer::new_without_managed_config(codex_home.path()).await?;
|
||||||
@@ -607,6 +609,7 @@ async fn thread_resume_tracks_thread_initialized_analytics() -> Result<()> {
|
|||||||
event,
|
event,
|
||||||
&thread.id,
|
&thread.id,
|
||||||
&thread.session_id,
|
&thread.session_id,
|
||||||
|
"codex_work_desktop",
|
||||||
"gpt-5.3-codex",
|
"gpt-5.3-codex",
|
||||||
"resumed",
|
"resumed",
|
||||||
"user",
|
"user",
|
||||||
@@ -615,11 +618,94 @@ async fn thread_resume_tracks_thread_initialized_analytics() -> Result<()> {
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
fn set_thread_source_on_fake_rollout(
|
#[tokio::test]
|
||||||
|
async fn thread_resume_running_thread_tracks_thread_originator_in_analytics() -> Result<()> {
|
||||||
|
let server = create_mock_responses_server_repeating_assistant("Done").await;
|
||||||
|
|
||||||
|
let codex_home = TempDir::new()?;
|
||||||
|
create_config_toml_with_chatgpt_base_url(codex_home.path(), &server.uri(), &server.uri())?;
|
||||||
|
mount_analytics_capture(&server, codex_home.path()).await?;
|
||||||
|
|
||||||
|
let mut mcp = TestAppServer::new_without_managed_config(codex_home.path()).await?;
|
||||||
|
timeout(DEFAULT_READ_TIMEOUT, mcp.initialize()).await??;
|
||||||
|
|
||||||
|
let start_id = mcp
|
||||||
|
.send_thread_start_request(ThreadStartParams {
|
||||||
|
model: Some("mock-model".to_string()),
|
||||||
|
thread_source: Some(ThreadSource::User),
|
||||||
|
service_name: Some("codex_work_desktop".to_string()),
|
||||||
|
..Default::default()
|
||||||
|
})
|
||||||
|
.await?;
|
||||||
|
let start_resp: JSONRPCResponse = timeout(
|
||||||
|
DEFAULT_READ_TIMEOUT,
|
||||||
|
mcp.read_stream_until_response_message(RequestId::Integer(start_id)),
|
||||||
|
)
|
||||||
|
.await??;
|
||||||
|
let ThreadStartResponse { thread, .. } = to_response::<ThreadStartResponse>(start_resp)?;
|
||||||
|
|
||||||
|
let turn_id = mcp
|
||||||
|
.send_turn_start_request(TurnStartParams {
|
||||||
|
thread_id: thread.id.clone(),
|
||||||
|
client_user_message_id: None,
|
||||||
|
input: vec![UserInput::Text {
|
||||||
|
text: "materialize rollout".to_string(),
|
||||||
|
text_elements: Vec::new(),
|
||||||
|
}],
|
||||||
|
..Default::default()
|
||||||
|
})
|
||||||
|
.await?;
|
||||||
|
timeout(
|
||||||
|
DEFAULT_READ_TIMEOUT,
|
||||||
|
mcp.read_stream_until_response_message(RequestId::Integer(turn_id)),
|
||||||
|
)
|
||||||
|
.await??;
|
||||||
|
timeout(
|
||||||
|
DEFAULT_READ_TIMEOUT,
|
||||||
|
mcp.read_stream_until_notification_message("turn/completed"),
|
||||||
|
)
|
||||||
|
.await??;
|
||||||
|
|
||||||
|
let resume_id = mcp
|
||||||
|
.send_thread_resume_request(ThreadResumeParams {
|
||||||
|
thread_id: thread.id.clone(),
|
||||||
|
exclude_turns: true,
|
||||||
|
..Default::default()
|
||||||
|
})
|
||||||
|
.await?;
|
||||||
|
let resume_resp: JSONRPCResponse = timeout(
|
||||||
|
DEFAULT_READ_TIMEOUT,
|
||||||
|
mcp.read_stream_until_response_message(RequestId::Integer(resume_id)),
|
||||||
|
)
|
||||||
|
.await??;
|
||||||
|
let ThreadResumeResponse {
|
||||||
|
thread: resumed, ..
|
||||||
|
} = to_response::<ThreadResumeResponse>(resume_resp)?;
|
||||||
|
|
||||||
|
let event = wait_for_matching_analytics_event(&server, DEFAULT_READ_TIMEOUT, |event| {
|
||||||
|
event["event_type"] == "codex_thread_initialized"
|
||||||
|
&& event["event_params"]["thread_id"] == resumed.id
|
||||||
|
&& event["event_params"]["initialization_mode"] == "resumed"
|
||||||
|
})
|
||||||
|
.await?;
|
||||||
|
assert_basic_thread_initialized_event(
|
||||||
|
&event,
|
||||||
|
&resumed.id,
|
||||||
|
&resumed.session_id,
|
||||||
|
"codex_work_desktop",
|
||||||
|
"mock-model",
|
||||||
|
"resumed",
|
||||||
|
"user",
|
||||||
|
);
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn set_session_meta_on_fake_rollout(
|
||||||
codex_home: &std::path::Path,
|
codex_home: &std::path::Path,
|
||||||
filename_ts: &str,
|
filename_ts: &str,
|
||||||
thread_id: &str,
|
thread_id: &str,
|
||||||
thread_source: &str,
|
thread_source: &str,
|
||||||
|
originator: &str,
|
||||||
) -> Result<()> {
|
) -> Result<()> {
|
||||||
let path = rollout_path(codex_home, filename_ts, thread_id);
|
let path = rollout_path(codex_home, filename_ts, thread_id);
|
||||||
let contents = std::fs::read_to_string(&path)?;
|
let contents = std::fs::read_to_string(&path)?;
|
||||||
@@ -629,6 +715,7 @@ fn set_thread_source_on_fake_rollout(
|
|||||||
.ok_or_else(|| anyhow::anyhow!("fake rollout missing session meta"))?;
|
.ok_or_else(|| anyhow::anyhow!("fake rollout missing session meta"))?;
|
||||||
let mut session_meta: serde_json::Value = serde_json::from_str(session_meta)?;
|
let mut session_meta: serde_json::Value = serde_json::from_str(session_meta)?;
|
||||||
session_meta["payload"]["thread_source"] = serde_json::json!(thread_source);
|
session_meta["payload"]["thread_source"] = serde_json::json!(thread_source);
|
||||||
|
session_meta["payload"]["originator"] = serde_json::json!(originator);
|
||||||
let remaining = lines.collect::<Vec<_>>().join("\n");
|
let remaining = lines.collect::<Vec<_>>().join("\n");
|
||||||
std::fs::write(&path, format!("{session_meta}\n{remaining}\n"))?;
|
std::fs::write(&path, format!("{session_meta}\n{remaining}\n"))?;
|
||||||
Ok(())
|
Ok(())
|
||||||
|
|||||||
@@ -655,6 +655,7 @@ async fn thread_start_tracks_thread_initialized_analytics() -> Result<()> {
|
|||||||
let req_id = mcp
|
let req_id = mcp
|
||||||
.send_thread_start_request(ThreadStartParams {
|
.send_thread_start_request(ThreadStartParams {
|
||||||
thread_source: Some(ThreadSource::User),
|
thread_source: Some(ThreadSource::User),
|
||||||
|
service_name: Some("codex_work_desktop".to_string()),
|
||||||
..Default::default()
|
..Default::default()
|
||||||
})
|
})
|
||||||
.await?;
|
.await?;
|
||||||
@@ -672,6 +673,7 @@ async fn thread_start_tracks_thread_initialized_analytics() -> Result<()> {
|
|||||||
event,
|
event,
|
||||||
&thread.id,
|
&thread.id,
|
||||||
&thread.session_id,
|
&thread.session_id,
|
||||||
|
"codex_work_desktop",
|
||||||
"mock-model",
|
"mock-model",
|
||||||
"new",
|
"new",
|
||||||
"user",
|
"user",
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
use anyhow::Context;
|
use anyhow::Context;
|
||||||
use anyhow::Result;
|
use anyhow::Result;
|
||||||
use app_test_support::DEFAULT_CLIENT_NAME;
|
|
||||||
use app_test_support::TestAppServer;
|
use app_test_support::TestAppServer;
|
||||||
use app_test_support::create_apply_patch_sse_response;
|
use app_test_support::create_apply_patch_sse_response;
|
||||||
use app_test_support::create_exec_command_sse_response;
|
use app_test_support::create_exec_command_sse_response;
|
||||||
@@ -840,7 +839,7 @@ async fn thread_start_omits_empty_instruction_overrides_from_model_request() ->
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
async fn turn_start_tracks_turn_event_analytics() -> Result<()> {
|
async fn turn_start_tracks_thread_originator_in_analytics() -> Result<()> {
|
||||||
let server = responses::start_mock_server().await;
|
let server = responses::start_mock_server().await;
|
||||||
let response_mock = responses::mount_response_sequence(
|
let response_mock = responses::mount_response_sequence(
|
||||||
&server,
|
&server,
|
||||||
@@ -875,6 +874,7 @@ async fn turn_start_tracks_turn_event_analytics() -> Result<()> {
|
|||||||
.send_thread_start_request(ThreadStartParams {
|
.send_thread_start_request(ThreadStartParams {
|
||||||
model: Some("mock-model".to_string()),
|
model: Some("mock-model".to_string()),
|
||||||
thread_source: Some(ThreadSource::User),
|
thread_source: Some(ThreadSource::User),
|
||||||
|
service_name: Some("codex_work_desktop".to_string()),
|
||||||
..Default::default()
|
..Default::default()
|
||||||
})
|
})
|
||||||
.await?;
|
.await?;
|
||||||
@@ -919,7 +919,7 @@ async fn turn_start_tracks_turn_event_analytics() -> Result<()> {
|
|||||||
assert_eq!(event["event_params"]["turn_id"], turn.id);
|
assert_eq!(event["event_params"]["turn_id"], turn.id);
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
event["event_params"]["app_server_client"]["product_client_id"],
|
event["event_params"]["app_server_client"]["product_client_id"],
|
||||||
DEFAULT_CLIENT_NAME
|
"codex_work_desktop"
|
||||||
);
|
);
|
||||||
assert_eq!(event["event_params"]["model"], "mock-model");
|
assert_eq!(event["event_params"]["model"], "mock-model");
|
||||||
assert_eq!(event["event_params"]["model_provider"], "mock_provider");
|
assert_eq!(event["event_params"]["model_provider"], "mock_provider");
|
||||||
|
|||||||
Reference in new issue
Block a user