@@ -13,6 +13,7 @@ use super::Result;
1313use crate :: can_track:: can_track;
1414use crate :: collect:: { Collect , posthog} ;
1515use crate :: event:: Identity ;
16+ use crate :: rate_limit:: RateLimiter ;
1617use crate :: { Event , EventKind , client_id} ;
1718
1819const POSTHOG_API_SECRET : & str = match option_env ! ( "POSTHOG_API_SECRET" ) {
@@ -47,6 +48,13 @@ static CACHED_PATH: LazyLock<Option<String>> = LazyLock::new(|| {
4748} ) ;
4849static CACHED_ARGS : LazyLock < Vec < String > > = LazyLock :: new ( || std:: env:: args ( ) . skip ( 1 ) . collect ( ) ) ;
4950
51+ /// Maximum number of events that can be dispatched per minute.
52+ ///
53+ /// This acts as a rate limiter to prevent runaway loops (e.g. when
54+ /// stdout/stderr is closed and every write error triggers another error event)
55+ /// while allowing normal tracking to continue for long-running sessions.
56+ const MAX_EVENTS_PER_MINUTE : usize = 1_000 ;
57+
5058#[ derive( Clone ) ]
5159pub struct Tracker {
5260 collectors : Arc < Vec < Box < dyn Collect > > > ,
@@ -56,6 +64,7 @@ pub struct Tracker {
5664 model : Arc < Mutex < Option < String > > > ,
5765 conversation : Arc < Mutex < Option < Conversation > > > ,
5866 is_logged_in : Arc < AtomicBool > ,
67+ rate_limiter : Arc < RateLimiter > ,
5968}
6069
6170impl Default for Tracker {
@@ -71,6 +80,7 @@ impl Default for Tracker {
7180 model : Arc :: new ( Mutex :: new ( None ) ) ,
7281 conversation : Arc :: new ( Mutex :: new ( None ) ) ,
7382 is_logged_in : Arc :: new ( AtomicBool :: new ( false ) ) ,
83+ rate_limiter : Arc :: new ( RateLimiter :: new ( MAX_EVENTS_PER_MINUTE ) ) ,
7484 }
7585 }
7686}
@@ -93,35 +103,41 @@ impl Tracker {
93103 }
94104
95105 pub async fn dispatch ( & self , event_kind : EventKind ) -> Result < ( ) > {
96- if self . can_track {
97- // Create a new event
98- let email = self . email ( ) . await ;
99- let event = Event {
100- event_name : event_kind. name ( ) ,
101- event_value : event_kind. value ( ) ,
102- start_time : self . start_time ,
103- cores : cores ( ) ,
104- client_id : client_id ( ) ,
105- os_name : os_name ( ) ,
106- up_time : up_time ( self . start_time ) ,
107- args : args ( ) ,
108- path : path ( ) ,
109- cwd : cwd ( ) ,
110- user : user ( ) ,
111- version : version ( ) ,
112- email : email. clone ( ) ,
113- model : self . model . lock ( ) . await . clone ( ) ,
114- conversation : self . conversation ( ) . await ,
115- identity : match event_kind {
116- EventKind :: Login ( id) => Some ( id) ,
117- _ => None ,
118- } ,
119- } ;
120-
121- // Dispatch the event to all collectors
122- for collector in self . collectors . as_ref ( ) {
123- collector. collect ( event. clone ( ) ) . await ?;
124- }
106+ if !self . can_track {
107+ return Ok ( ( ) ) ;
108+ }
109+
110+ if !self . rate_limiter . check ( ) {
111+ return Ok ( ( ) ) ; // Drop event if rate limit exceeded
112+ }
113+
114+ // Create a new event
115+ let email = self . email ( ) . await ;
116+ let event = Event {
117+ event_name : event_kind. name ( ) ,
118+ event_value : event_kind. value ( ) ,
119+ start_time : self . start_time ,
120+ cores : cores ( ) ,
121+ client_id : client_id ( ) ,
122+ os_name : os_name ( ) ,
123+ up_time : up_time ( self . start_time ) ,
124+ args : args ( ) ,
125+ path : path ( ) ,
126+ cwd : cwd ( ) ,
127+ user : user ( ) ,
128+ version : version ( ) ,
129+ email : email. clone ( ) ,
130+ model : self . model . lock ( ) . await . clone ( ) ,
131+ conversation : self . conversation ( ) . await ,
132+ identity : match event_kind {
133+ EventKind :: Login ( id) => Some ( id) ,
134+ _ => None ,
135+ } ,
136+ } ;
137+
138+ // Dispatch the event to all collectors
139+ for collector in self . collectors . as_ref ( ) {
140+ collector. collect ( event. clone ( ) ) . await ?;
125141 }
126142 Ok ( ( ) )
127143 }
0 commit comments