2026-04-23 23:20:16 +01:00
use crate ::events ::AppServerRpcTransport ;
use crate ::events ::GuardianReviewAnalyticsResult ;
use crate ::events ::GuardianReviewTrackContext ;
use crate ::events ::TrackEventRequest ;
use crate ::events ::TrackEventsRequest ;
use crate ::events ::current_runtime_metadata ;
use crate ::facts ::AnalyticsFact ;
use crate ::facts ::AnalyticsJsonRpcError ;
use crate ::facts ::CustomAnalyticsFact ;
use crate ::facts ::HookRunFact ;
use crate ::facts ::HookRunInput ;
use crate ::facts ::SubAgentThreadStartedInput ;
use crate ::facts ::TrackEventsContext ;
use crate ::facts ::TurnResolvedConfigFact ;
use crate ::facts ::TurnTokenUsageFact ;
use crate ::reducer ::AnalyticsReducer ;
use codex_app_server_protocol ::ClientRequest ;
use codex_app_server_protocol ::ClientResponse ;
use codex_app_server_protocol ::InitializeParams ;
use codex_app_server_protocol ::JSONRPCErrorError ;
use codex_app_server_protocol ::RequestId ;
use codex_app_server_protocol ::ServerNotification ;
use codex_login ::AuthManager ;
use codex_login ::default_client ::create_client ;
use std ::sync ::Arc ;
use std ::time ::Duration ;
use tokio ::sync ::mpsc ;
const ANALYTICS_EVENTS_QUEUE_SIZE : usize = 256 ;
const ANALYTICS_EVENTS_TIMEOUT : Duration = Duration ::from_secs ( 10 ) ;
#[ derive(Clone) ]
pub ( crate ) struct AnalyticsEventsQueue {
pub ( crate ) sender : mpsc ::Sender < AnalyticsFact > ,
}
#[ derive(Clone) ]
pub struct AnalyticsEventsClient {
queue : AnalyticsEventsQueue ,
analytics_enabled : Option < bool > ,
}
impl AnalyticsEventsQueue {
pub ( crate ) fn new ( auth_manager : Arc < AuthManager > , base_url : String ) -> Self {
let ( sender , mut receiver ) = mpsc ::channel ( ANALYTICS_EVENTS_QUEUE_SIZE ) ;
tokio ::spawn ( async move {
let mut reducer = AnalyticsReducer ::default ( ) ;
while let Some ( input ) = receiver . recv ( ) . await {
let mut events = Vec ::new ( ) ;
reducer . ingest ( input , & mut events ) . await ;
send_track_events ( & auth_manager , & base_url , events ) . await ;
}
} ) ;
2026-05-13 21:04:15 +01:00
Self { sender }
2026-04-23 23:20:16 +01:00
}
fn try_send ( & self , input : AnalyticsFact ) {
if self . sender . try_send ( input ) . is_err ( ) {
//TODO: add a metric for this
tracing ::warn! ( " dropping analytics events: queue is full " ) ;
}
}
}
impl AnalyticsEventsClient {
pub fn new (
auth_manager : Arc < AuthManager > ,
base_url : String ,
analytics_enabled : Option < bool > ,
) -> Self {
Self {
queue : AnalyticsEventsQueue ::new ( Arc ::clone ( & auth_manager ) , base_url ) ,
analytics_enabled ,
}
}
pub fn track_initialize (
& self ,
connection_id : u64 ,
params : InitializeParams ,
product_client_id : String ,
rpc_transport : AppServerRpcTransport ,
) {
self . record_fact ( AnalyticsFact ::Initialize {
connection_id ,
params ,
product_client_id ,
runtime : current_runtime_metadata ( ) ,
rpc_transport ,
} ) ;
}
pub fn track_subagent_thread_started ( & self , input : SubAgentThreadStartedInput ) {
self . record_fact ( AnalyticsFact ::Custom (
CustomAnalyticsFact ::SubAgentThreadStarted ( input ) ,
) ) ;
}
pub fn track_guardian_review (
& self ,
tracking : & GuardianReviewTrackContext ,
result : GuardianReviewAnalyticsResult ,
) {
self . record_fact ( AnalyticsFact ::Custom ( CustomAnalyticsFact ::GuardianReview (
Box ::new ( tracking . event_params ( result ) ) ,
) ) ) ;
}
pub fn track_request ( & self , connection_id : u64 , request_id : RequestId , request : ClientRequest ) {
self . record_fact ( AnalyticsFact ::Request {
connection_id ,
request_id ,
request : Box ::new ( request ) ,
} ) ;
}
pub fn track_hook_run ( & self , tracking : TrackEventsContext , hook : HookRunFact ) {
self . record_fact ( AnalyticsFact ::Custom ( CustomAnalyticsFact ::HookRun (
HookRunInput { tracking , hook } ,
) ) ) ;
}
pub fn track_compaction ( & self , event : crate ::facts ::CodexCompactionEvent ) {
self . record_fact ( AnalyticsFact ::Custom ( CustomAnalyticsFact ::Compaction (
Box ::new ( event ) ,
) ) ) ;
}
pub fn track_turn_resolved_config ( & self , fact : TurnResolvedConfigFact ) {
self . record_fact ( AnalyticsFact ::Custom (
CustomAnalyticsFact ::TurnResolvedConfig ( Box ::new ( fact ) ) ,
) ) ;
}
pub fn track_turn_token_usage ( & self , fact : TurnTokenUsageFact ) {
self . record_fact ( AnalyticsFact ::Custom ( CustomAnalyticsFact ::TurnTokenUsage (
Box ::new ( fact ) ,
) ) ) ;
}
pub ( crate ) fn record_fact ( & self , input : AnalyticsFact ) {
if self . analytics_enabled = = Some ( false ) {
return ;
}
self . queue . try_send ( input ) ;
}
pub fn track_response ( & self , connection_id : u64 , response : ClientResponse ) {
self . record_fact ( AnalyticsFact ::Response {
connection_id ,
response : Box ::new ( response ) ,
} ) ;
}
pub fn track_error_response (
& self ,
connection_id : u64 ,
request_id : RequestId ,
error : JSONRPCErrorError ,
error_type : Option < AnalyticsJsonRpcError > ,
) {
self . record_fact ( AnalyticsFact ::ErrorResponse {
connection_id ,
request_id ,
error ,
error_type ,
} ) ;
}
pub fn track_notification ( & self , notification : ServerNotification ) {
self . record_fact ( AnalyticsFact ::Notification ( Box ::new ( notification ) ) ) ;
}
}
async fn send_track_events (
auth_manager : & AuthManager ,
base_url : & str ,
events : Vec < TrackEventRequest > ,
) {
if events . is_empty ( ) {
return ;
}
let Some ( auth ) = auth_manager . auth ( ) . await else {
return ;
} ;
if ! auth . is_chatgpt_auth ( ) {
return ;
}
let access_token = match auth . get_token ( ) {
Ok ( token ) = > token ,
Err ( _ ) = > return ,
} ;
let Some ( account_id ) = auth . get_account_id ( ) else {
return ;
} ;
let base_url = base_url . trim_end_matches ( '/' ) ;
let url = format! ( " {base_url} /codex/analytics-events/events " ) ;
let payload = TrackEventsRequest { events } ;
let response = create_client ( )
. post ( & url )
. timeout ( ANALYTICS_EVENTS_TIMEOUT )
. bearer_auth ( & access_token )
. header ( " chatgpt-account-id " , & account_id )
. header ( " Content-Type " , " application/json " )
. json ( & payload )
. send ( )
. await ;
match response {
Ok ( response ) if response . status ( ) . is_success ( ) = > { }
Ok ( response ) = > {
let status = response . status ( ) ;
let body = response . text ( ) . await . unwrap_or_default ( ) ;
tracing ::warn! ( " events failed with status {status}: {body} " ) ;
}
Err ( err ) = > {
tracing ::warn! ( " failed to send events request: {err} " ) ;
}
}
}