mirror of
https://github.com/pchuan98/codex.git
synced 2026-07-01 00:31:56 +08:00
[codex-analytics] feature plumbing and emittance (#16640)
--- [//]: # (BEGIN SAPLING FOOTER) Stack created with [Sapling](https://sapling-scm.com). Best reviewed with [ReviewStack](https://reviewstack.dev/openai/codex/pull/16640). * #16870 * #16706 * #16641 * __->__ #16640
This commit is contained in:
committed by
GitHub
Unverified
parent
05c5829923
commit
b704df85b8
@@ -12,6 +12,7 @@ use crate::thread_state::TurnSummary;
|
||||
use crate::thread_state::resolve_server_request_on_thread_listener;
|
||||
use crate::thread_status::ThreadWatchActiveGuard;
|
||||
use crate::thread_status::ThreadWatchManager;
|
||||
use codex_analytics::AnalyticsEventsClient;
|
||||
use codex_app_server_protocol::AccountRateLimitsUpdatedNotification;
|
||||
use codex_app_server_protocol::AdditionalPermissionProfile as V2AdditionalPermissionProfile;
|
||||
use codex_app_server_protocol::AgentMessageDeltaNotification;
|
||||
@@ -167,6 +168,7 @@ pub(crate) async fn apply_bespoke_event_handling(
|
||||
conversation_id: ThreadId,
|
||||
conversation: Arc<CodexThread>,
|
||||
thread_manager: Arc<ThreadManager>,
|
||||
analytics_events_client: Option<AnalyticsEventsClient>,
|
||||
outgoing: ThreadScopedOutgoingMessageSender,
|
||||
thread_state: Arc<tokio::sync::Mutex<ThreadState>>,
|
||||
thread_watch_manager: ThreadWatchManager,
|
||||
@@ -202,6 +204,10 @@ pub(crate) async fn apply_bespoke_event_handling(
|
||||
thread_id: conversation_id.to_string(),
|
||||
turn,
|
||||
};
|
||||
if let Some(analytics_events_client) = analytics_events_client.as_ref() {
|
||||
analytics_events_client
|
||||
.track_notification(ServerNotification::TurnStarted(notification.clone()));
|
||||
}
|
||||
outgoing
|
||||
.send_server_notification(ServerNotification::TurnStarted(notification))
|
||||
.await;
|
||||
@@ -218,6 +224,7 @@ pub(crate) async fn apply_bespoke_event_handling(
|
||||
conversation_id,
|
||||
event_turn_id,
|
||||
turn_complete_event,
|
||||
analytics_events_client.as_ref(),
|
||||
&outgoing,
|
||||
&thread_state,
|
||||
)
|
||||
@@ -1773,6 +1780,7 @@ pub(crate) async fn apply_bespoke_event_handling(
|
||||
conversation_id,
|
||||
event_turn_id,
|
||||
turn_aborted_event,
|
||||
analytics_events_client.as_ref(),
|
||||
&outgoing,
|
||||
&thread_state,
|
||||
)
|
||||
@@ -1950,6 +1958,7 @@ async fn emit_turn_completed_with_status(
|
||||
conversation_id: ThreadId,
|
||||
event_turn_id: String,
|
||||
turn_completion_metadata: TurnCompletionMetadata,
|
||||
analytics_events_client: Option<&AnalyticsEventsClient>,
|
||||
outgoing: &ThreadScopedOutgoingMessageSender,
|
||||
) {
|
||||
let notification = TurnCompletedNotification {
|
||||
@@ -1964,6 +1973,10 @@ async fn emit_turn_completed_with_status(
|
||||
duration_ms: turn_completion_metadata.duration_ms,
|
||||
},
|
||||
};
|
||||
if let Some(analytics_events_client) = analytics_events_client {
|
||||
analytics_events_client
|
||||
.track_notification(ServerNotification::TurnCompleted(notification.clone()));
|
||||
}
|
||||
outgoing
|
||||
.send_server_notification(ServerNotification::TurnCompleted(notification))
|
||||
.await;
|
||||
@@ -2156,6 +2169,7 @@ async fn handle_turn_complete(
|
||||
conversation_id: ThreadId,
|
||||
event_turn_id: String,
|
||||
turn_complete_event: TurnCompleteEvent,
|
||||
analytics_events_client: Option<&AnalyticsEventsClient>,
|
||||
outgoing: &ThreadScopedOutgoingMessageSender,
|
||||
thread_state: &Arc<Mutex<ThreadState>>,
|
||||
) {
|
||||
@@ -2176,6 +2190,7 @@ async fn handle_turn_complete(
|
||||
completed_at: turn_complete_event.completed_at,
|
||||
duration_ms: turn_complete_event.duration_ms,
|
||||
},
|
||||
analytics_events_client,
|
||||
outgoing,
|
||||
)
|
||||
.await;
|
||||
@@ -2185,6 +2200,7 @@ async fn handle_turn_interrupted(
|
||||
conversation_id: ThreadId,
|
||||
event_turn_id: String,
|
||||
turn_aborted_event: TurnAbortedEvent,
|
||||
analytics_events_client: Option<&AnalyticsEventsClient>,
|
||||
outgoing: &ThreadScopedOutgoingMessageSender,
|
||||
thread_state: &Arc<Mutex<ThreadState>>,
|
||||
) {
|
||||
@@ -2200,6 +2216,7 @@ async fn handle_turn_interrupted(
|
||||
completed_at: turn_aborted_event.completed_at,
|
||||
duration_ms: turn_aborted_event.duration_ms,
|
||||
},
|
||||
analytics_events_client,
|
||||
outgoing,
|
||||
)
|
||||
.await;
|
||||
@@ -2940,6 +2957,7 @@ mod tests {
|
||||
use codex_app_server_protocol::GuardianApprovalReviewStatus;
|
||||
use codex_app_server_protocol::JSONRPCErrorError;
|
||||
use codex_app_server_protocol::TurnPlanStepStatus;
|
||||
use codex_login::AuthManager;
|
||||
use codex_login::CodexAuth;
|
||||
use codex_protocol::items::HookPromptFragment;
|
||||
use codex_protocol::items::build_hook_prompt_message;
|
||||
@@ -3070,6 +3088,7 @@ mod tests {
|
||||
outgoing: ThreadScopedOutgoingMessageSender,
|
||||
thread_state: Arc<Mutex<ThreadState>>,
|
||||
thread_watch_manager: ThreadWatchManager,
|
||||
analytics_events_client: AnalyticsEventsClient,
|
||||
codex_home: PathBuf,
|
||||
}
|
||||
|
||||
@@ -3084,6 +3103,7 @@ mod tests {
|
||||
self.conversation_id,
|
||||
self.conversation.clone(),
|
||||
self.thread_manager.clone(),
|
||||
Some(self.analytics_events_client.clone()),
|
||||
self.outgoing.clone(),
|
||||
self.thread_state.clone(),
|
||||
self.thread_watch_manager.clone(),
|
||||
@@ -3410,6 +3430,13 @@ mod tests {
|
||||
outgoing: outgoing.clone(),
|
||||
thread_state: thread_state.clone(),
|
||||
thread_watch_manager: thread_watch_manager.clone(),
|
||||
analytics_events_client: AnalyticsEventsClient::new(
|
||||
AuthManager::from_auth_for_testing(
|
||||
CodexAuth::create_dummy_chatgpt_auth_for_testing(),
|
||||
),
|
||||
"http://localhost".to_string(),
|
||||
Some(false),
|
||||
),
|
||||
codex_home: codex_home.path().to_path_buf(),
|
||||
};
|
||||
|
||||
@@ -3860,6 +3887,7 @@ mod tests {
|
||||
conversation_id,
|
||||
event_turn_id.clone(),
|
||||
turn_complete_event(&event_turn_id),
|
||||
/*analytics_events_client*/ None,
|
||||
&outgoing,
|
||||
&thread_state,
|
||||
)
|
||||
@@ -3908,6 +3936,7 @@ mod tests {
|
||||
conversation_id,
|
||||
event_turn_id.clone(),
|
||||
turn_aborted_event(&event_turn_id),
|
||||
/*analytics_events_client*/ None,
|
||||
&outgoing,
|
||||
&thread_state,
|
||||
)
|
||||
@@ -3955,6 +3984,7 @@ mod tests {
|
||||
conversation_id,
|
||||
event_turn_id.clone(),
|
||||
turn_complete_event(&event_turn_id),
|
||||
/*analytics_events_client*/ None,
|
||||
&outgoing,
|
||||
&thread_state,
|
||||
)
|
||||
@@ -4221,6 +4251,7 @@ mod tests {
|
||||
conversation_a,
|
||||
a_turn1.clone(),
|
||||
turn_complete_event(&a_turn1),
|
||||
/*analytics_events_client*/ None,
|
||||
&outgoing,
|
||||
&thread_state,
|
||||
)
|
||||
@@ -4242,6 +4273,7 @@ mod tests {
|
||||
conversation_b,
|
||||
b_turn1.clone(),
|
||||
turn_complete_event(&b_turn1),
|
||||
/*analytics_events_client*/ None,
|
||||
&outgoing,
|
||||
&thread_state,
|
||||
)
|
||||
@@ -4253,6 +4285,7 @@ mod tests {
|
||||
conversation_a,
|
||||
a_turn2.clone(),
|
||||
turn_complete_event(&a_turn2),
|
||||
/*analytics_events_client*/ None,
|
||||
&outgoing,
|
||||
&thread_state,
|
||||
)
|
||||
|
||||
@@ -22,6 +22,9 @@ use chrono::DateTime;
|
||||
use chrono::SecondsFormat;
|
||||
use chrono::Utc;
|
||||
use codex_analytics::AnalyticsEventsClient;
|
||||
use codex_analytics::AnalyticsJsonRpcError;
|
||||
use codex_analytics::InputError;
|
||||
use codex_analytics::TurnSteerRequestError;
|
||||
use codex_app_server_protocol::Account;
|
||||
use codex_app_server_protocol::AccountLoginCompletedNotification;
|
||||
use codex_app_server_protocol::AccountUpdatedNotification;
|
||||
@@ -36,7 +39,7 @@ use codex_app_server_protocol::CancelLoginAccountResponse;
|
||||
use codex_app_server_protocol::CancelLoginAccountStatus;
|
||||
use codex_app_server_protocol::ClientRequest;
|
||||
use codex_app_server_protocol::ClientResponse;
|
||||
use codex_app_server_protocol::CodexErrorInfo as AppServerCodexErrorInfo;
|
||||
use codex_app_server_protocol::CodexErrorInfo;
|
||||
use codex_app_server_protocol::CollaborationModeListParams;
|
||||
use codex_app_server_protocol::CollaborationModeListResponse;
|
||||
use codex_app_server_protocol::CommandExecParams;
|
||||
@@ -642,6 +645,22 @@ impl CodexMessageProcessor {
|
||||
}
|
||||
}
|
||||
|
||||
fn track_error_response(
|
||||
&self,
|
||||
request_id: &ConnectionRequestId,
|
||||
error: &JSONRPCErrorError,
|
||||
error_type: Option<AnalyticsJsonRpcError>,
|
||||
) {
|
||||
if self.config.features.enabled(Feature::GeneralAnalytics) {
|
||||
self.analytics_events_client.track_error_response(
|
||||
request_id.connection_id.0,
|
||||
request_id.request_id.clone(),
|
||||
error.clone(),
|
||||
error_type,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
async fn load_thread(
|
||||
&self,
|
||||
thread_id: &str,
|
||||
@@ -6920,12 +6939,18 @@ impl CodexMessageProcessor {
|
||||
app_server_client_version: Option<String>,
|
||||
) {
|
||||
if let Err(error) = Self::validate_v2_input_limit(¶ms.input) {
|
||||
self.track_error_response(
|
||||
&request_id,
|
||||
&error,
|
||||
Some(AnalyticsJsonRpcError::Input(InputError::TooLarge)),
|
||||
);
|
||||
self.outgoing.send_error(request_id, error).await;
|
||||
return;
|
||||
}
|
||||
let (_, thread) = match self.load_thread(¶ms.thread_id).await {
|
||||
Ok(v) => v,
|
||||
Err(error) => {
|
||||
self.track_error_response(&request_id, &error, /*error_type*/ None);
|
||||
self.outgoing.send_error(request_id, error).await;
|
||||
return;
|
||||
}
|
||||
@@ -6937,6 +6962,7 @@ impl CodexMessageProcessor {
|
||||
)
|
||||
.await
|
||||
{
|
||||
self.track_error_response(&request_id, &error, /*error_type*/ None);
|
||||
self.outgoing.send_error(request_id, error).await;
|
||||
return;
|
||||
}
|
||||
@@ -7020,6 +7046,15 @@ impl CodexMessageProcessor {
|
||||
};
|
||||
|
||||
let response = TurnStartResponse { turn };
|
||||
if self.config.features.enabled(Feature::GeneralAnalytics) {
|
||||
self.analytics_events_client.track_response(
|
||||
request_id.connection_id.0,
|
||||
ClientResponse::TurnStart {
|
||||
request_id: request_id.request_id.clone(),
|
||||
response: response.clone(),
|
||||
},
|
||||
);
|
||||
}
|
||||
self.outgoing.send_response(request_id, response).await;
|
||||
}
|
||||
Err(err) => {
|
||||
@@ -7028,6 +7063,7 @@ impl CodexMessageProcessor {
|
||||
message: format!("failed to start turn: {err}"),
|
||||
data: None,
|
||||
};
|
||||
self.track_error_response(&request_id, &error, /*error_type*/ None);
|
||||
self.outgoing.send_error(request_id, error).await;
|
||||
}
|
||||
}
|
||||
@@ -7101,6 +7137,7 @@ impl CodexMessageProcessor {
|
||||
let (_, thread) = match self.load_thread(¶ms.thread_id).await {
|
||||
Ok(v) => v,
|
||||
Err(error) => {
|
||||
self.track_error_response(&request_id, &error, /*error_type*/ None);
|
||||
self.outgoing.send_error(request_id, error).await;
|
||||
return;
|
||||
}
|
||||
@@ -7118,6 +7155,11 @@ impl CodexMessageProcessor {
|
||||
.record_request_turn_id(&request_id, ¶ms.expected_turn_id)
|
||||
.await;
|
||||
if let Err(error) = Self::validate_v2_input_limit(¶ms.input) {
|
||||
self.track_error_response(
|
||||
&request_id,
|
||||
&error,
|
||||
Some(AnalyticsJsonRpcError::Input(InputError::TooLarge)),
|
||||
);
|
||||
self.outgoing.send_error(request_id, error).await;
|
||||
return;
|
||||
}
|
||||
@@ -7138,36 +7180,51 @@ impl CodexMessageProcessor {
|
||||
{
|
||||
Ok(turn_id) => {
|
||||
let response = TurnSteerResponse { turn_id };
|
||||
if self.config.features.enabled(Feature::GeneralAnalytics) {
|
||||
self.analytics_events_client.track_response(
|
||||
request_id.connection_id.0,
|
||||
ClientResponse::TurnSteer {
|
||||
request_id: request_id.request_id.clone(),
|
||||
response: response.clone(),
|
||||
},
|
||||
);
|
||||
}
|
||||
self.outgoing.send_response(request_id, response).await;
|
||||
}
|
||||
Err(err) => {
|
||||
let (code, message, data) = match err {
|
||||
let (code, message, data, error_type) = match err {
|
||||
SteerInputError::NoActiveTurn(_) => (
|
||||
INVALID_REQUEST_ERROR_CODE,
|
||||
"no active turn to steer".to_string(),
|
||||
None,
|
||||
Some(AnalyticsJsonRpcError::TurnSteer(
|
||||
TurnSteerRequestError::NoActiveTurn,
|
||||
)),
|
||||
),
|
||||
SteerInputError::ExpectedTurnMismatch { expected, actual } => (
|
||||
INVALID_REQUEST_ERROR_CODE,
|
||||
format!("expected active turn id `{expected}` but found `{actual}`"),
|
||||
None,
|
||||
Some(AnalyticsJsonRpcError::TurnSteer(
|
||||
TurnSteerRequestError::ExpectedTurnMismatch,
|
||||
)),
|
||||
),
|
||||
SteerInputError::ActiveTurnNotSteerable { turn_kind } => {
|
||||
let message = match turn_kind {
|
||||
codex_protocol::protocol::NonSteerableTurnKind::Review => {
|
||||
"cannot steer a review turn".to_string()
|
||||
}
|
||||
codex_protocol::protocol::NonSteerableTurnKind::Compact => {
|
||||
"cannot steer a compact turn".to_string()
|
||||
}
|
||||
let (message, turn_steer_error) = match turn_kind {
|
||||
codex_protocol::protocol::NonSteerableTurnKind::Review => (
|
||||
"cannot steer a review turn".to_string(),
|
||||
TurnSteerRequestError::NonSteerableReview,
|
||||
),
|
||||
codex_protocol::protocol::NonSteerableTurnKind::Compact => (
|
||||
"cannot steer a compact turn".to_string(),
|
||||
TurnSteerRequestError::NonSteerableCompact,
|
||||
),
|
||||
};
|
||||
let error = TurnError {
|
||||
message: message.clone(),
|
||||
codex_error_info: Some(
|
||||
AppServerCodexErrorInfo::ActiveTurnNotSteerable {
|
||||
turn_kind: turn_kind.into(),
|
||||
},
|
||||
),
|
||||
codex_error_info: Some(CodexErrorInfo::ActiveTurnNotSteerable {
|
||||
turn_kind: turn_kind.into(),
|
||||
}),
|
||||
additional_details: None,
|
||||
};
|
||||
let data = match serde_json::to_value(error) {
|
||||
@@ -7180,12 +7237,18 @@ impl CodexMessageProcessor {
|
||||
None
|
||||
}
|
||||
};
|
||||
(INVALID_REQUEST_ERROR_CODE, message, data)
|
||||
(
|
||||
INVALID_REQUEST_ERROR_CODE,
|
||||
message,
|
||||
data,
|
||||
Some(AnalyticsJsonRpcError::TurnSteer(turn_steer_error)),
|
||||
)
|
||||
}
|
||||
SteerInputError::EmptyInput => (
|
||||
INVALID_REQUEST_ERROR_CODE,
|
||||
"input must not be empty".to_string(),
|
||||
None,
|
||||
Some(AnalyticsJsonRpcError::Input(InputError::Empty)),
|
||||
),
|
||||
};
|
||||
let error = JSONRPCErrorError {
|
||||
@@ -7193,6 +7256,7 @@ impl CodexMessageProcessor {
|
||||
message,
|
||||
data,
|
||||
};
|
||||
self.track_error_response(&request_id, &error, error_type);
|
||||
self.outgoing.send_error(request_id, error).await;
|
||||
}
|
||||
}
|
||||
@@ -7940,6 +8004,9 @@ impl CodexMessageProcessor {
|
||||
conversation_id,
|
||||
conversation.clone(),
|
||||
thread_manager.clone(),
|
||||
listener_task_context
|
||||
.general_analytics_enabled
|
||||
.then(|| listener_task_context.analytics_events_client.clone()),
|
||||
thread_outgoing,
|
||||
thread_state.clone(),
|
||||
thread_watch_manager.clone(),
|
||||
|
||||
@@ -12,6 +12,7 @@ use std::path::PathBuf;
|
||||
// Debug-only test hook: lets integration tests point the server at a temporary
|
||||
// managed config file without writing to /etc.
|
||||
const MANAGED_CONFIG_PATH_ENV_VAR: &str = "CODEX_APP_SERVER_MANAGED_CONFIG_PATH";
|
||||
const DISABLE_MANAGED_CONFIG_ENV_VAR: &str = "CODEX_APP_SERVER_DISABLE_MANAGED_CONFIG";
|
||||
|
||||
#[derive(Debug, Parser)]
|
||||
struct AppServerArgs {
|
||||
@@ -40,10 +41,13 @@ struct AppServerArgs {
|
||||
fn main() -> anyhow::Result<()> {
|
||||
arg0_dispatch_or_else(|arg0_paths: Arg0DispatchPaths| async move {
|
||||
let args = AppServerArgs::parse();
|
||||
let managed_config_path = managed_config_path_from_debug_env();
|
||||
let loader_overrides = LoaderOverrides {
|
||||
managed_config_path,
|
||||
..Default::default()
|
||||
let loader_overrides = if disable_managed_config_from_debug_env() {
|
||||
LoaderOverrides::without_managed_config_for_tests()
|
||||
} else {
|
||||
LoaderOverrides {
|
||||
managed_config_path: managed_config_path_from_debug_env(),
|
||||
..Default::default()
|
||||
}
|
||||
};
|
||||
let transport = args.listen;
|
||||
let session_source = args.session_source;
|
||||
@@ -63,6 +67,17 @@ fn main() -> anyhow::Result<()> {
|
||||
})
|
||||
}
|
||||
|
||||
fn disable_managed_config_from_debug_env() -> bool {
|
||||
#[cfg(debug_assertions)]
|
||||
{
|
||||
if let Ok(value) = std::env::var(DISABLE_MANAGED_CONFIG_ENV_VAR) {
|
||||
return matches!(value.as_str(), "1" | "true" | "TRUE" | "yes" | "YES");
|
||||
}
|
||||
}
|
||||
|
||||
false
|
||||
}
|
||||
|
||||
fn managed_config_path_from_debug_env() -> Option<PathBuf> {
|
||||
#[cfg(debug_assertions)]
|
||||
{
|
||||
|
||||
@@ -665,6 +665,16 @@ impl MessageProcessor {
|
||||
self.outgoing.send_error(connection_request_id, error).await;
|
||||
return;
|
||||
}
|
||||
if self.config.features.enabled(Feature::GeneralAnalytics)
|
||||
&& let ClientRequest::TurnStart { request_id, .. }
|
||||
| ClientRequest::TurnSteer { request_id, .. } = &codex_request
|
||||
{
|
||||
self.analytics_events_client.track_request(
|
||||
connection_id.0,
|
||||
request_id.clone(),
|
||||
codex_request.clone(),
|
||||
);
|
||||
}
|
||||
|
||||
match codex_request {
|
||||
ClientRequest::ConfigRead { request_id, params } => {
|
||||
|
||||
Reference in New Issue
Block a user