Attach initialization timing to response analytics

This commit is contained in:
Ahmed Ibrahim
2026-06-08 12:06:04 -07:00
parent d2ea7d973d
commit e58edbbef6
6 changed files with 38 additions and 73 deletions

View File

@@ -530,6 +530,7 @@ async fn ingest_rejected_turn_steer(
response: Box::new(sample_thread_resume_response(
"thread-2", /*ephemeral*/ false, "gpt-5",
)),
thread_initialization: None,
},
out,
)
@@ -603,6 +604,7 @@ async fn ingest_turn_prerequisites(
response: Box::new(sample_thread_start_response(
"thread-2", /*ephemeral*/ false, "gpt-5",
)),
thread_initialization: None,
},
out,
)
@@ -626,6 +628,7 @@ async fn ingest_turn_prerequisites(
connection_id: 7,
request_id: RequestId::Integer(3),
response: Box::new(sample_turn_start_response("turn-2")),
thread_initialization: None,
},
out,
)
@@ -692,6 +695,7 @@ async fn ingest_review_prerequisites(
response: Box::new(sample_thread_start_response(
"thread-1", /*ephemeral*/ false, "gpt-5",
)),
thread_initialization: None,
},
events,
)
@@ -1604,6 +1608,7 @@ async fn initialize_caches_client_and_thread_lifecycle_publishes_once_initialize
/*ephemeral*/ false,
"gpt-5",
)),
thread_initialization: None,
},
&mut events,
)
@@ -1648,6 +1653,7 @@ async fn initialize_caches_client_and_thread_lifecycle_publishes_once_initialize
response: Box::new(sample_thread_resume_response(
"thread-1", /*ephemeral*/ true, "gpt-5",
)),
thread_initialization: None,
},
&mut events,
)
@@ -1718,6 +1724,7 @@ async fn unrelated_client_requests_are_ignored_by_reducer() {
connection_id: 7,
request_id: RequestId::Integer(3),
response: Box::new(sample_turn_start_response("turn-2")),
thread_initialization: None,
},
&mut events,
)
@@ -1743,6 +1750,7 @@ async fn unrelated_client_responses_are_ignored_by_reducer() {
response: Box::new(ClientResponsePayload::ThreadArchive(
ThreadArchiveResponse {},
)),
thread_initialization: None,
},
&mut events,
)
@@ -1801,6 +1809,7 @@ async fn compaction_event_ingests_custom_fact() {
Some(AppServerThreadSource::Subagent),
Some(parent_thread_id.to_string()),
)),
thread_initialization: None,
},
&mut events,
)
@@ -1908,6 +1917,7 @@ async fn guardian_review_event_ingests_custom_fact_with_optional_target_item() {
/*ephemeral*/ false,
"gpt-5",
)),
thread_initialization: None,
},
&mut events,
)
@@ -2409,6 +2419,7 @@ async fn item_review_summaries_do_not_cross_threads_with_reused_item_ids() {
response: Box::new(sample_thread_start_response(
"thread-2", /*ephemeral*/ false, "gpt-5",
)),
thread_initialization: None,
},
&mut events,
)
@@ -2696,6 +2707,7 @@ async fn subagent_thread_started_inherits_parent_connection_for_new_thread() {
/*ephemeral*/ false,
"gpt-5",
)),
thread_initialization: None,
},
&mut events,
)
@@ -3450,6 +3462,7 @@ async fn accepted_turn_steer_emits_expected_event() {
connection_id: 7,
request_id: RequestId::Integer(4),
response: Box::new(sample_turn_steer_response("turn-2")),
thread_initialization: None,
},
&mut out,
)
@@ -3621,6 +3634,7 @@ async fn turn_start_error_response_discards_pending_start_request() {
connection_id: 7,
request_id: RequestId::Integer(3),
response: Box::new(sample_turn_start_response("turn-2")),
thread_initialization: None,
},
&mut out,
)
@@ -3920,6 +3934,7 @@ async fn accepted_steers_increment_turn_steer_count() {
connection_id: 7,
request_id: RequestId::Integer(4),
response: Box::new(sample_turn_steer_response("turn-2")),
thread_initialization: None,
},
&mut out,
)
@@ -3967,6 +3982,7 @@ async fn accepted_steers_increment_turn_steer_count() {
connection_id: 7,
request_id: RequestId::Integer(6),
response: Box::new(sample_turn_steer_response("turn-2")),
thread_initialization: None,
},
&mut out,
)

View File

@@ -18,7 +18,6 @@ use crate::facts::SkillInvocation;
use crate::facts::SkillInvokedInput;
use crate::facts::SubAgentThreadStartedInput;
use crate::facts::ThreadInitializationFact;
use crate::facts::ThreadInitializationInput;
use crate::facts::TrackEventsContext;
use crate::facts::TurnCodexErrorFact;
use crate::facts::TurnProfileFact;
@@ -319,6 +318,7 @@ impl AnalyticsEventsClient {
connection_id: u64,
request_id: RequestId,
response: ClientResponsePayload,
thread_initialization: Option<ThreadInitializationFact>,
) {
if !matches!(
response,
@@ -334,24 +334,10 @@ impl AnalyticsEventsClient {
connection_id,
request_id,
response: Box::new(response),
thread_initialization,
});
}
pub fn track_thread_initialization(
&self,
connection_id: u64,
request_id: RequestId,
fact: ThreadInitializationFact,
) {
self.record_fact(AnalyticsFact::Custom(
CustomAnalyticsFact::ThreadInitialization(Box::new(ThreadInitializationInput {
connection_id,
request_id,
fact,
})),
));
}
pub fn track_error_response(
&self,
connection_id: u64,

View File

@@ -252,7 +252,9 @@ fn track_response_only_enqueues_analytics_relevant_responses() {
(RequestId::Integer(4), sample_turn_start_response()),
(RequestId::Integer(5), sample_turn_steer_response()),
] {
client.track_response(/*connection_id*/ 7, request_id, response);
client.track_response(
/*connection_id*/ 7, request_id, response, /*thread_initialization*/ None,
);
assert!(matches!(
receiver.try_recv(),
Ok(AnalyticsFact::ClientResponse { .. })
@@ -263,6 +265,7 @@ fn track_response_only_enqueues_analytics_relevant_responses() {
/*connection_id*/ 7,
RequestId::Integer(6),
ClientResponsePayload::ThreadArchive(ThreadArchiveResponse {}),
/*thread_initialization*/ None,
);
assert!(matches!(receiver.try_recv(), Err(TryRecvError::Empty)));
}

View File

@@ -122,13 +122,6 @@ pub struct ThreadInitializationFact {
pub profile: ThreadInitializationProfile,
}
#[derive(Clone)]
pub(crate) struct ThreadInitializationInput {
pub connection_id: u64,
pub request_id: RequestId,
pub fact: ThreadInitializationFact,
}
#[derive(Clone)]
pub struct TurnTokenUsageFact {
pub turn_id: String,
@@ -489,6 +482,7 @@ pub(crate) enum AnalyticsFact {
connection_id: u64,
request_id: RequestId,
response: Box<ClientResponsePayload>,
thread_initialization: Option<ThreadInitializationFact>,
},
ErrorResponse {
connection_id: u64,
@@ -526,7 +520,6 @@ pub(crate) enum CustomAnalyticsFact {
TurnResolvedConfig(Box<TurnResolvedConfigFact>),
TurnTokenUsage(Box<TurnTokenUsageFact>),
TurnProfile(Box<TurnProfileFact>),
ThreadInitialization(Box<ThreadInitializationInput>),
TurnCodexError(Box<TurnCodexErrorFact>),
SkillInvoked(SkillInvokedInput),
AppMentioned(AppMentionedInput),

View File

@@ -70,7 +70,6 @@ use crate::facts::PluginUsedInput;
use crate::facts::SkillInvokedInput;
use crate::facts::SubAgentThreadStartedInput;
use crate::facts::ThreadInitializationFact;
use crate::facts::ThreadInitializationInput;
use crate::facts::ThreadInitializationMode;
use crate::facts::ThreadInitializationProfile;
use crate::facts::TurnCodexError;
@@ -136,7 +135,6 @@ use std::path::PathBuf;
#[derive(Default)]
pub(crate) struct AnalyticsReducer {
requests: HashMap<(u64, RequestId), RequestState>,
thread_initializations: HashMap<(u64, RequestId), ThreadInitializationFact>,
turns: HashMap<String, TurnState>,
connections: HashMap<u64, ConnectionState>,
threads: HashMap<String, ThreadAnalyticsState>,
@@ -410,9 +408,11 @@ impl AnalyticsReducer {
connection_id,
request_id,
response,
thread_initialization,
} => {
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_initialization, out)
.await;
}
}
AnalyticsFact::ErrorResponse {
@@ -475,9 +475,6 @@ impl AnalyticsReducer {
CustomAnalyticsFact::TurnProfile(input) => {
self.ingest_turn_profile(*input, out).await;
}
CustomAnalyticsFact::ThreadInitialization(input) => {
self.ingest_thread_initialization(*input);
}
CustomAnalyticsFact::TurnCodexError(input) => {
self.ingest_turn_codex_error(*input);
}
@@ -767,58 +764,39 @@ impl AnalyticsReducer {
&mut self,
connection_id: u64,
response: ClientResponse,
thread_initialization: Option<ThreadInitializationFact>,
out: &mut Vec<TrackEventRequest>,
) {
match response {
ClientResponse::ThreadStart {
request_id,
response,
} => {
let initialization = self
.thread_initializations
.remove(&(connection_id, request_id));
ClientResponse::ThreadStart { response, .. } => {
self.emit_thread_initialized(
connection_id,
response.thread,
response.model,
initialization
thread_initialization
.map(|initialization| initialization.initialization_mode)
.unwrap_or(ThreadInitializationMode::New),
initialization.map(|initialization| initialization.profile),
thread_initialization.map(|initialization| initialization.profile),
out,
);
}
ClientResponse::ThreadResume {
request_id,
response,
} => {
let profile = self
.thread_initializations
.remove(&(connection_id, request_id))
.map(|initialization| initialization.profile);
ClientResponse::ThreadResume { response, .. } => {
self.emit_thread_initialized(
connection_id,
response.thread,
response.model,
ThreadInitializationMode::Resumed,
profile,
thread_initialization.map(|initialization| initialization.profile),
out,
);
}
ClientResponse::ThreadFork {
request_id,
response,
} => {
let profile = self
.thread_initializations
.remove(&(connection_id, request_id))
.map(|initialization| initialization.profile);
ClientResponse::ThreadFork { response, .. } => {
self.emit_thread_initialized(
connection_id,
response.thread,
response.model,
ThreadInitializationMode::Forked,
profile,
thread_initialization.map(|initialization| initialization.profile),
out,
);
}
@@ -848,11 +826,6 @@ impl AnalyticsReducer {
}
}
fn ingest_thread_initialization(&mut self, input: ThreadInitializationInput) {
self.thread_initializations
.insert((input.connection_id, input.request_id), input.fact);
}
fn ingest_server_request(&mut self, _connection_id: u64, request: ServerRequest) {
match request {
ServerRequest::CommandExecutionRequestApproval { request_id, params } => {

View File

@@ -526,20 +526,14 @@ impl OutgoingMessageSender {
.into_jsonrpc_parts_and_payload(request_id.request_id.clone())
.map(|(id, result, response)| {
if let Some(response) = response {
if let Some(thread_initialization) = request_context
.as_ref()
.and_then(|context| context.thread_initialization_timing.complete_request())
{
self.analytics_events_client.track_thread_initialization(
connection_id.0,
request_id_for_analytics.clone(),
thread_initialization,
);
}
let thread_initialization = request_context.as_ref().and_then(|context| {
context.thread_initialization_timing.complete_request()
});
self.analytics_events_client.track_response(
connection_id.0,
request_id_for_analytics,
response,
thread_initialization,
);
}
(id, result)