mirror of
https://github.com/openai/codex.git
synced 2026-10-23 18:08:45 +00:00
Add rejected turn/start analytics
This commit is contained in:
@@ -15,7 +15,10 @@ use crate::events::CodexReviewEventParams;
|
||||
use crate::events::CodexReviewEventRequest;
|
||||
use crate::events::CodexRuntimeMetadata;
|
||||
use crate::events::CodexToolItemEventBase;
|
||||
use crate::events::CodexTurnEventParams;
|
||||
use crate::events::CodexTurnEventRequest;
|
||||
use crate::events::CodexTurnStartRejectedEventParams;
|
||||
use crate::events::CodexTurnStartRejectedEventRequest;
|
||||
use crate::events::FinalApprovalOutcome;
|
||||
use crate::events::GuardianApprovalRequestSource;
|
||||
use crate::events::GuardianReviewDecision;
|
||||
@@ -63,6 +66,7 @@ use crate::facts::SubAgentThreadStartedInput;
|
||||
use crate::facts::ThreadInitializationMode;
|
||||
use crate::facts::TrackEventsContext;
|
||||
use crate::facts::TurnResolvedConfigFact;
|
||||
use crate::facts::TurnStartRejectionReason;
|
||||
use crate::facts::TurnStatus;
|
||||
use crate::facts::TurnSteerRequestError;
|
||||
use crate::facts::TurnTokenUsageFact;
|
||||
@@ -531,6 +535,52 @@ async fn ingest_rejected_turn_steer(
|
||||
serde_json::to_value(&out[0]).expect("serialize turn steer event")
|
||||
}
|
||||
|
||||
async fn ingest_rejected_turn_start(
|
||||
reducer: &mut AnalyticsReducer,
|
||||
out: &mut Vec<TrackEventRequest>,
|
||||
error: JSONRPCErrorError,
|
||||
error_type: Option<AnalyticsJsonRpcError>,
|
||||
) -> serde_json::Value {
|
||||
ingest_initialize(reducer, out).await;
|
||||
reducer
|
||||
.ingest(
|
||||
AnalyticsFact::ClientResponse {
|
||||
connection_id: 7,
|
||||
request_id: RequestId::Integer(1),
|
||||
response: Box::new(sample_thread_start_response(
|
||||
"thread-2", /*ephemeral*/ false, "gpt-5",
|
||||
)),
|
||||
},
|
||||
out,
|
||||
)
|
||||
.await;
|
||||
out.clear();
|
||||
reducer
|
||||
.ingest(
|
||||
AnalyticsFact::ClientRequest {
|
||||
connection_id: 7,
|
||||
request_id: RequestId::Integer(3),
|
||||
request: Box::new(sample_turn_start_request("thread-2", /*request_id*/ 3)),
|
||||
},
|
||||
out,
|
||||
)
|
||||
.await;
|
||||
reducer
|
||||
.ingest(
|
||||
AnalyticsFact::ErrorResponse {
|
||||
connection_id: 7,
|
||||
request_id: RequestId::Integer(3),
|
||||
error,
|
||||
error_type,
|
||||
},
|
||||
out,
|
||||
)
|
||||
.await;
|
||||
|
||||
assert_eq!(out.len(), 1);
|
||||
serde_json::to_value(&out[0]).expect("serialize rejected turn start event")
|
||||
}
|
||||
|
||||
async fn ingest_initialize(reducer: &mut AnalyticsReducer, out: &mut Vec<TrackEventRequest>) {
|
||||
reducer
|
||||
.ingest(
|
||||
@@ -3188,7 +3238,7 @@ async fn reducer_ingests_plugin_state_changed_fact() {
|
||||
fn turn_event_serializes_expected_shape() {
|
||||
let event = TrackEventRequest::TurnEvent(Box::new(CodexTurnEventRequest {
|
||||
event_type: "codex_turn_event",
|
||||
event_params: crate::events::CodexTurnEventParams {
|
||||
event_params: CodexTurnEventParams {
|
||||
thread_id: "thread-2".to_string(),
|
||||
turn_id: "turn-2".to_string(),
|
||||
app_server_client: sample_app_server_client_metadata(),
|
||||
@@ -3300,6 +3350,56 @@ fn turn_event_serializes_expected_shape() {
|
||||
assert_eq!(payload, expected);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn rejected_turn_start_event_serializes_expected_shape() {
|
||||
let event = TrackEventRequest::TurnStartRejected(CodexTurnStartRejectedEventRequest {
|
||||
event_type: "codex_turn_start_rejected_event",
|
||||
event_params: CodexTurnStartRejectedEventParams {
|
||||
thread_id: "thread-2".to_string(),
|
||||
app_server_client: sample_app_server_client_metadata(),
|
||||
runtime: sample_runtime_metadata(),
|
||||
thread_source: Some(ThreadSource::User),
|
||||
subagent_source: None,
|
||||
parent_thread_id: None,
|
||||
num_input_images: 2,
|
||||
rejection_reason: Some(TurnStartRejectionReason::InputTooLarge),
|
||||
created_at: 456,
|
||||
},
|
||||
});
|
||||
|
||||
let payload = serde_json::to_value(&event).expect("serialize rejected turn start event");
|
||||
let expected = serde_json::from_str::<serde_json::Value>(
|
||||
r#"{
|
||||
"event_type": "codex_turn_start_rejected_event",
|
||||
"event_params": {
|
||||
"thread_id": "thread-2",
|
||||
"app_server_client": {
|
||||
"product_client_id": "codex_cli_rs",
|
||||
"client_name": "codex-tui",
|
||||
"client_version": "1.0.0",
|
||||
"rpc_transport": "stdio",
|
||||
"experimental_api_enabled": true
|
||||
},
|
||||
"runtime": {
|
||||
"codex_rs_version": "0.1.0",
|
||||
"runtime_os": "macos",
|
||||
"runtime_os_version": "15.3.1",
|
||||
"runtime_arch": "aarch64"
|
||||
},
|
||||
"thread_source": "user",
|
||||
"subagent_source": null,
|
||||
"parent_thread_id": null,
|
||||
"num_input_images": 2,
|
||||
"rejection_reason": "input_too_large",
|
||||
"created_at": 456
|
||||
}
|
||||
}"#,
|
||||
)
|
||||
.expect("parse expected rejected turn start event");
|
||||
|
||||
assert_eq!(payload, expected);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn accepted_turn_steer_emits_expected_event() {
|
||||
let mut reducer = AnalyticsReducer::default();
|
||||
@@ -3463,12 +3563,66 @@ async fn turn_steer_does_not_emit_without_pending_request() {
|
||||
assert!(out.is_empty());
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn rejected_turn_start_uses_request_connection_metadata() {
|
||||
let mut reducer = AnalyticsReducer::default();
|
||||
let mut out = Vec::new();
|
||||
let payload = ingest_rejected_turn_start(
|
||||
&mut reducer,
|
||||
&mut out,
|
||||
input_too_large_steer_error(),
|
||||
Some(input_too_large_error_type()),
|
||||
)
|
||||
.await;
|
||||
|
||||
assert_eq!(
|
||||
payload["event_type"],
|
||||
json!("codex_turn_start_rejected_event")
|
||||
);
|
||||
assert_eq!(payload["event_params"]["thread_id"], json!("thread-2"));
|
||||
assert_eq!(payload["event_params"]["num_input_images"], json!(1));
|
||||
assert_eq!(
|
||||
payload["event_params"]["app_server_client"]["product_client_id"],
|
||||
json!("codex-tui")
|
||||
);
|
||||
assert_eq!(
|
||||
payload["event_params"]["runtime"]["codex_rs_version"],
|
||||
json!("0.1.0")
|
||||
);
|
||||
assert_eq!(payload["event_params"]["thread_source"], json!("user"));
|
||||
assert_eq!(payload["event_params"]["subagent_source"], json!(null));
|
||||
assert_eq!(payload["event_params"]["parent_thread_id"], json!(null));
|
||||
assert_eq!(
|
||||
payload["event_params"]["rejection_reason"],
|
||||
json!("input_too_large")
|
||||
);
|
||||
assert!(
|
||||
payload["event_params"]["created_at"]
|
||||
.as_u64()
|
||||
.expect("created_at")
|
||||
> 0
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn turn_start_error_response_discards_pending_start_request() {
|
||||
let mut reducer = AnalyticsReducer::default();
|
||||
let mut out = Vec::new();
|
||||
|
||||
ingest_initialize(&mut reducer, &mut out).await;
|
||||
reducer
|
||||
.ingest(
|
||||
AnalyticsFact::ClientResponse {
|
||||
connection_id: 7,
|
||||
request_id: RequestId::Integer(1),
|
||||
response: Box::new(sample_thread_start_response(
|
||||
"thread-2", /*ephemeral*/ false, "gpt-5",
|
||||
)),
|
||||
},
|
||||
&mut out,
|
||||
)
|
||||
.await;
|
||||
out.clear();
|
||||
reducer
|
||||
.ingest(
|
||||
AnalyticsFact::ClientRequest {
|
||||
@@ -3484,12 +3638,24 @@ async fn turn_start_error_response_discards_pending_start_request() {
|
||||
AnalyticsFact::ErrorResponse {
|
||||
connection_id: 7,
|
||||
request_id: RequestId::Integer(3),
|
||||
error: no_active_turn_steer_error(),
|
||||
error: JSONRPCErrorError {
|
||||
code: -32600,
|
||||
message: "turn start failed".to_string(),
|
||||
data: None,
|
||||
},
|
||||
error_type: None,
|
||||
},
|
||||
&mut out,
|
||||
)
|
||||
.await;
|
||||
assert_eq!(out.len(), 1);
|
||||
let payload = serde_json::to_value(&out[0]).expect("serialize rejected turn start event");
|
||||
assert_eq!(
|
||||
payload["event_type"],
|
||||
json!("codex_turn_start_rejected_event")
|
||||
);
|
||||
assert_eq!(payload["event_params"]["rejection_reason"], json!(null));
|
||||
out.clear();
|
||||
|
||||
// A late/synthetic response for the same request id must not resurrect the
|
||||
// failed turn/start request and attach request-scoped connection metadata.
|
||||
|
||||
@@ -15,6 +15,7 @@ use crate::facts::PluginState;
|
||||
use crate::facts::SubAgentThreadStartedInput;
|
||||
use crate::facts::ThreadInitializationMode;
|
||||
use crate::facts::TrackEventsContext;
|
||||
use crate::facts::TurnStartRejectionReason;
|
||||
use crate::facts::TurnStatus;
|
||||
use crate::facts::TurnSteerRejectionReason;
|
||||
use crate::facts::TurnSteerResult;
|
||||
@@ -63,6 +64,7 @@ pub(crate) enum TrackEventRequest {
|
||||
HookRun(CodexHookRunEventRequest),
|
||||
Compaction(Box<CodexCompactionEventRequest>),
|
||||
TurnEvent(Box<CodexTurnEventRequest>),
|
||||
TurnStartRejected(CodexTurnStartRejectedEventRequest),
|
||||
TurnSteer(CodexTurnSteerEventRequest),
|
||||
CommandExecution(CodexCommandExecutionEventRequest),
|
||||
FileChange(CodexFileChangeEventRequest),
|
||||
@@ -818,6 +820,25 @@ pub(crate) struct CodexTurnEventRequest {
|
||||
pub(crate) event_params: CodexTurnEventParams,
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
pub(crate) struct CodexTurnStartRejectedEventParams {
|
||||
pub(crate) thread_id: String,
|
||||
pub(crate) app_server_client: CodexAppServerClientMetadata,
|
||||
pub(crate) runtime: CodexRuntimeMetadata,
|
||||
pub(crate) thread_source: Option<ThreadSource>,
|
||||
pub(crate) subagent_source: Option<String>,
|
||||
pub(crate) parent_thread_id: Option<String>,
|
||||
pub(crate) num_input_images: usize,
|
||||
pub(crate) rejection_reason: Option<TurnStartRejectionReason>,
|
||||
pub(crate) created_at: u64,
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
pub(crate) struct CodexTurnStartRejectedEventRequest {
|
||||
pub(crate) event_type: &'static str,
|
||||
pub(crate) event_params: CodexTurnStartRejectedEventParams,
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
pub(crate) struct CodexTurnSteerEventParams {
|
||||
pub(crate) thread_id: String,
|
||||
|
||||
@@ -107,6 +107,12 @@ pub enum TurnStatus {
|
||||
Interrupted,
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, Serialize)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub enum TurnStartRejectionReason {
|
||||
InputTooLarge,
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, Serialize)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub enum TurnSteerResult {
|
||||
@@ -135,6 +141,13 @@ pub struct CodexTurnSteerEvent {
|
||||
pub created_at: u64,
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct CodexTurnStartRejectedEvent {
|
||||
pub num_input_images: usize,
|
||||
pub rejection_reason: Option<TurnStartRejectionReason>,
|
||||
pub created_at: u64,
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug)]
|
||||
pub enum AnalyticsJsonRpcError {
|
||||
TurnSteer(TurnSteerRequestError),
|
||||
|
||||
@@ -24,6 +24,7 @@ pub use facts::AcceptedLineFingerprint;
|
||||
pub use facts::AnalyticsJsonRpcError;
|
||||
pub use facts::AppInvocation;
|
||||
pub use facts::CodexCompactionEvent;
|
||||
pub use facts::CodexTurnStartRejectedEvent;
|
||||
pub use facts::CodexTurnSteerEvent;
|
||||
pub use facts::CompactionImplementation;
|
||||
pub use facts::CompactionPhase;
|
||||
@@ -39,6 +40,7 @@ pub use facts::SubAgentThreadStartedInput;
|
||||
pub use facts::ThreadInitializationMode;
|
||||
pub use facts::TrackEventsContext;
|
||||
pub use facts::TurnResolvedConfigFact;
|
||||
pub use facts::TurnStartRejectionReason;
|
||||
pub use facts::TurnStatus;
|
||||
pub use facts::TurnSteerRejectionReason;
|
||||
pub use facts::TurnSteerRequestError;
|
||||
|
||||
@@ -28,6 +28,8 @@ use crate::events::CodexRuntimeMetadata;
|
||||
use crate::events::CodexToolItemEventBase;
|
||||
use crate::events::CodexTurnEventParams;
|
||||
use crate::events::CodexTurnEventRequest;
|
||||
use crate::events::CodexTurnStartRejectedEventParams;
|
||||
use crate::events::CodexTurnStartRejectedEventRequest;
|
||||
use crate::events::CodexTurnSteerEventParams;
|
||||
use crate::events::CodexTurnSteerEventRequest;
|
||||
use crate::events::CodexWebSearchEventParams;
|
||||
@@ -65,6 +67,7 @@ use crate::facts::AppUsedInput;
|
||||
use crate::facts::CodexCompactionEvent;
|
||||
use crate::facts::CustomAnalyticsFact;
|
||||
use crate::facts::HookRunInput;
|
||||
use crate::facts::InputError;
|
||||
use crate::facts::PluginState;
|
||||
use crate::facts::PluginStateChangedInput;
|
||||
use crate::facts::PluginUsedInput;
|
||||
@@ -72,6 +75,7 @@ use crate::facts::SkillInvokedInput;
|
||||
use crate::facts::SubAgentThreadStartedInput;
|
||||
use crate::facts::ThreadInitializationMode;
|
||||
use crate::facts::TurnResolvedConfigFact;
|
||||
use crate::facts::TurnStartRejectionReason;
|
||||
use crate::facts::TurnStatus;
|
||||
use crate::facts::TurnSteerRejectionReason;
|
||||
use crate::facts::TurnSteerResult;
|
||||
@@ -212,6 +216,16 @@ impl<'a> AnalyticsDropSite<'a> {
|
||||
}
|
||||
}
|
||||
|
||||
fn turn_start(thread_id: &'a str) -> Self {
|
||||
Self {
|
||||
event_name: "turn start rejected",
|
||||
thread_id,
|
||||
turn_id: None,
|
||||
review_id: None,
|
||||
item_id: None,
|
||||
}
|
||||
}
|
||||
|
||||
fn turn(thread_id: &'a str, turn_id: &'a str) -> Self {
|
||||
Self {
|
||||
event_name: "turn",
|
||||
@@ -297,6 +311,7 @@ enum RequestState {
|
||||
struct PendingTurnStartState {
|
||||
thread_id: String,
|
||||
num_input_images: usize,
|
||||
created_at: u64,
|
||||
}
|
||||
|
||||
struct PendingTurnSteerState {
|
||||
@@ -573,6 +588,7 @@ impl AnalyticsReducer {
|
||||
RequestState::TurnStart(PendingTurnStartState {
|
||||
thread_id: params.thread_id,
|
||||
num_input_images: num_input_images(¶ms.input),
|
||||
created_at: now_unix_seconds(),
|
||||
}),
|
||||
);
|
||||
}
|
||||
@@ -1037,7 +1053,14 @@ impl AnalyticsReducer {
|
||||
out: &mut Vec<TrackEventRequest>,
|
||||
) {
|
||||
match request {
|
||||
RequestState::TurnStart(_) => {}
|
||||
RequestState::TurnStart(pending_request) => {
|
||||
self.ingest_turn_start_error_response(
|
||||
connection_id,
|
||||
pending_request,
|
||||
error_type,
|
||||
out,
|
||||
);
|
||||
}
|
||||
RequestState::TurnSteer(pending_request) => {
|
||||
self.ingest_turn_steer_error_response(
|
||||
connection_id,
|
||||
@@ -1049,6 +1072,21 @@ impl AnalyticsReducer {
|
||||
}
|
||||
}
|
||||
|
||||
fn ingest_turn_start_error_response(
|
||||
&mut self,
|
||||
connection_id: u64,
|
||||
pending_request: PendingTurnStartState,
|
||||
error_type: Option<AnalyticsJsonRpcError>,
|
||||
out: &mut Vec<TrackEventRequest>,
|
||||
) {
|
||||
self.emit_turn_start_rejected_event(
|
||||
connection_id,
|
||||
pending_request,
|
||||
turn_start_rejection_reason_from_error_type(error_type),
|
||||
out,
|
||||
);
|
||||
}
|
||||
|
||||
fn ingest_turn_steer_error_response(
|
||||
&mut self,
|
||||
connection_id: u64,
|
||||
@@ -1061,7 +1099,7 @@ impl AnalyticsReducer {
|
||||
pending_request,
|
||||
/*accepted_turn_id*/ None,
|
||||
TurnSteerResult::Rejected,
|
||||
rejection_reason_from_error_type(error_type),
|
||||
turn_steer_rejection_reason_from_error_type(error_type),
|
||||
out,
|
||||
);
|
||||
}
|
||||
@@ -1354,6 +1392,43 @@ impl AnalyticsReducer {
|
||||
);
|
||||
}
|
||||
|
||||
fn emit_turn_start_rejected_event(
|
||||
&mut self,
|
||||
connection_id: u64,
|
||||
pending_request: PendingTurnStartState,
|
||||
rejection_reason: Option<TurnStartRejectionReason>,
|
||||
out: &mut Vec<TrackEventRequest>,
|
||||
) {
|
||||
let Some(connection_state) = self.connections.get(&connection_id) else {
|
||||
return;
|
||||
};
|
||||
let drop_site = AnalyticsDropSite::turn_start(&pending_request.thread_id);
|
||||
let Some(thread_metadata) = self
|
||||
.threads
|
||||
.get(drop_site.thread_id)
|
||||
.and_then(|thread| thread.metadata.as_ref())
|
||||
else {
|
||||
warn_missing_analytics_context(&drop_site, MissingAnalyticsContext::ThreadMetadata);
|
||||
return;
|
||||
};
|
||||
out.push(TrackEventRequest::TurnStartRejected(
|
||||
CodexTurnStartRejectedEventRequest {
|
||||
event_type: "codex_turn_start_rejected_event",
|
||||
event_params: CodexTurnStartRejectedEventParams {
|
||||
thread_id: pending_request.thread_id,
|
||||
app_server_client: connection_state.app_server_client.clone(),
|
||||
runtime: connection_state.runtime.clone(),
|
||||
thread_source: thread_metadata.thread_source,
|
||||
subagent_source: thread_metadata.subagent_source.clone(),
|
||||
parent_thread_id: thread_metadata.parent_thread_id.clone(),
|
||||
num_input_images: pending_request.num_input_images,
|
||||
rejection_reason,
|
||||
created_at: pending_request.created_at,
|
||||
},
|
||||
},
|
||||
));
|
||||
}
|
||||
|
||||
fn emit_turn_steer_event(
|
||||
&mut self,
|
||||
connection_id: u64,
|
||||
@@ -2567,7 +2642,19 @@ fn num_input_images(input: &[UserInput]) -> usize {
|
||||
.count()
|
||||
}
|
||||
|
||||
fn rejection_reason_from_error_type(
|
||||
fn turn_start_rejection_reason_from_error_type(
|
||||
error_type: Option<AnalyticsJsonRpcError>,
|
||||
) -> Option<TurnStartRejectionReason> {
|
||||
match error_type? {
|
||||
AnalyticsJsonRpcError::TurnSteer(_) => None,
|
||||
AnalyticsJsonRpcError::Input(InputError::Empty) => None,
|
||||
AnalyticsJsonRpcError::Input(InputError::TooLarge) => {
|
||||
Some(TurnStartRejectionReason::InputTooLarge)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn turn_steer_rejection_reason_from_error_type(
|
||||
error_type: Option<AnalyticsJsonRpcError>,
|
||||
) -> Option<TurnSteerRejectionReason> {
|
||||
match error_type? {
|
||||
|
||||
Reference in New Issue
Block a user