@@ -58,7 +58,8 @@ class BaseFFEWriter {
5858
5959 this . _buffer = [ ]
6060 this . _bufferLimit = 1000
61- this . _bufferSize = 0
61+ this . _bufferStart = 0
62+ this . _dropWarningLogged = false
6263
6364 this . _config = config
6465 this . _endpoint = endpoint
@@ -99,51 +100,87 @@ class BaseFFEWriter {
99100 const eventArray = Array . isArray ( events ) ? events : [ events ]
100101
101102 for ( const event of eventArray ) {
102- if ( this . _buffer . length >= this . _bufferLimit ) {
103- log . warn ( '%s event buffer full (limit is %d), dropping event' , this . constructor . name , this . _bufferLimit )
104- this . _droppedEvents ++
105- continue
106- }
107-
108- const eventSizeBytes = Buffer . byteLength ( JSON . stringify ( event ) )
109-
110- // Check individual event size limit if configured
111- if ( this . _eventSizeLimit && eventSizeBytes > this . _eventSizeLimit ) {
112- log . warn ( '%s event size %d bytes exceeds limit %d, dropping event' ,
113- this . constructor . name , eventSizeBytes , this . _eventSizeLimit )
103+ if ( this . _buffer . length < this . _bufferLimit ) {
104+ this . _buffer . push ( event )
105+ } else {
106+ this . _buffer [ this . _bufferStart ] = event
107+ this . _bufferStart = ( this . _bufferStart + 1 ) % this . _bufferLimit
114108 this . _droppedEvents ++
115- continue
116- }
117109
118- // Check if adding this event would exceed payload size limit if configured
119- if ( this . _payloadSizeLimit && this . _bufferSize + eventSizeBytes > this . _payloadSizeLimit ) {
120- log . debug ( '%s buffer size would exceed %d bytes, flushing first' , this . constructor . name , this . _payloadSizeLimit )
121- this . flush ( )
110+ if ( ! this . _dropWarningLogged ) {
111+ this . _dropWarningLogged = true
112+ log . warn (
113+ '%s dropped exposure event(s) at cap %d. This may invalidate experiment results.' ,
114+ this . constructor . name ,
115+ this . _bufferLimit
116+ )
117+ }
122118 }
123-
124- this . _bufferSize += eventSizeBytes
125- this . _buffer . push ( event )
126119 }
127120 }
128121
129122 /**
130- * Flushes all buffered events to the agent
123+ * Sizes, batches, and flushes all buffered events.
131124 */
132125 flush ( ) {
133126 if ( this . _buffer . length === 0 ) {
134127 return
135128 }
136- const events = this . _buffer
129+
130+ const events = this . _bufferStart === 0
131+ ? this . _buffer
132+ : [ ...this . _buffer . slice ( this . _bufferStart ) , ...this . _buffer . slice ( 0 , this . _bufferStart ) ]
137133 this . _buffer = [ ]
138- this . _bufferSize = 0
134+ this . _bufferStart = 0
139135
140- const payload = this . _encode ( this . makePayload ( events ) )
136+ let batch = [ ]
137+ let batchSize = 0
141138
142- // eslint-disable-next-line eslint-rules/eslint-log-printf-style
143- log . debug ( ( ) => `${ this . constructor . name } flushing payload: ${ safeJSONStringify ( payload ) } ` )
139+ for ( const event of events ) {
140+ let eventSize
141+ try {
142+ eventSize = Buffer . byteLength ( JSON . stringify ( event ) )
143+ } catch ( error ) {
144+ log . warn ( '%s could not serialize an event, dropping event: %s' , this . constructor . name , error . message )
145+ this . _droppedEvents ++
146+ continue
147+ }
144148
145- const route = this . #createActiveRoute( )
146- this . #sendRequest( payload , events . length , route , this . _fallbackRoute )
149+ if ( this . _eventSizeLimit && eventSize > this . _eventSizeLimit ) {
150+ log . warn (
151+ '%s event size %d bytes exceeds limit %d, dropping event' ,
152+ this . constructor . name ,
153+ eventSize ,
154+ this . _eventSizeLimit
155+ )
156+ this . _droppedEvents ++
157+ continue
158+ }
159+
160+ if ( this . _payloadSizeLimit && eventSize > this . _payloadSizeLimit ) {
161+ log . warn (
162+ '%s event size %d bytes exceeds payload limit %d, dropping event' ,
163+ this . constructor . name ,
164+ eventSize ,
165+ this . _payloadSizeLimit
166+ )
167+ this . _droppedEvents ++
168+ continue
169+ }
170+
171+ if ( batch . length > 0 && this . _payloadSizeLimit && batchSize + eventSize > this . _payloadSizeLimit ) {
172+ this . #send( batch )
173+ batch = [ ]
174+ batchSize = 0
175+ }
176+
177+ batch . push ( event )
178+ batchSize += eventSize
179+ }
180+
181+ if ( batch . length > 0 ) {
182+ this . #send( batch )
183+ }
147184 }
148185
149186 /**
@@ -194,6 +231,34 @@ class BaseFFEWriter {
194231 this . _fallbackRoute = fallbackRoute ? this . #createRoute( fallbackRoute ) : undefined
195232 }
196233
234+ /**
235+ * Sends one event batch.
236+ *
237+ * @param {Array<object> } events - Events in the batch
238+ * @returns {void }
239+ */
240+ #send ( events ) {
241+ let payload
242+ try {
243+ payload = this . _encode ( this . makePayload ( events ) )
244+ } catch ( error ) {
245+ log . warn (
246+ '%s could not encode %d event(s), dropping batch: %s' ,
247+ this . constructor . name ,
248+ events . length ,
249+ error . message
250+ )
251+ this . _droppedEvents += events . length
252+ return
253+ }
254+
255+ // eslint-disable-next-line eslint-rules/eslint-log-printf-style
256+ log . debug ( ( ) => `${ this . constructor . name } flushing payload: ${ safeJSONStringify ( payload ) } ` )
257+
258+ const route = this . #createActiveRoute( )
259+ this . #sendRequest( payload , events . length , route , this . _fallbackRoute )
260+ }
261+
197262 /**
198263 * Creates request state for a configured writer route.
199264 *
0 commit comments