@@ -50,7 +50,8 @@ const shouldSkipCommand = (command) => {
5050 if ( ! Cypress . env ( 'BROWSERSTACK_O11Y_LOGS' ) ) {
5151 return true ;
5252 }
53- return command . attributes . name == 'log' || ( command . attributes . name == 'task' && ( [ 'test_observability_platform_details' , 'test_observability_step' , 'test_observability_command' , 'browserstack_log' , 'test_observability_log' ] . some ( event => command . attributes . args . includes ( event ) ) ) ) ;
53+ /* the batch task is filtered too, else each dispatch refills the queue */
54+ return command . attributes . name == 'log' || ( command . attributes . name == 'task' && ( [ 'test_observability_platform_details' , 'test_observability_step' , 'test_observability_command' , 'test_observability_batch' , 'browserstack_log' , 'test_observability_log' ] . some ( event => command . attributes . args . includes ( event ) ) ) ) ;
5455}
5556
5657Cypress . on ( 'log:changed' , ( attrs ) => {
@@ -339,20 +340,91 @@ Cypress.Commands.add('fatal', (message, file) => {
339340 } ) ;
340341} ) ;
341342
343+ /* console.warn, not cy.task — a diagnostic must not use the mechanism it reports on */
344+ const warnFlushFailure = ( stage , err ) => {
345+ try {
346+ console . warn ( `BrowserStack Test Observability: suppressed ${ stage } error, event(s) dropped: ${ err && err . message ? err . message : err } ` ) ;
347+ } catch ( e ) { /* logging must never throw either */ }
348+ } ;
349+
350+ /*
351+ * [SDK-7399] One cy.task per drain, not per event. Each round-trip costs ~0.8s on a
352+ * remote terminal, so a command-heavy spec spent minutes in afterEach, exceeded
353+ * spec_timeout and was killed — unrun tests then reported as skipped, with nothing
354+ * thrown. 600 events: 581s as 600 calls, 109s as one.
355+ * Stays on cy.task: cy.now('task') throws on Cypress 14, so it would "fix" this by
356+ * delivering nothing. The try/catch is only a backstop for synchronous throws — a
357+ * cy.task failure surfaces later, while the command queue drains.
358+ */
359+
360+ /* Remote terminal: a single cy.task payload of 768KB succeeds, 1MB fails.
361+ * Split well under that; drop only what cannot be sent at all. The two must stay
362+ * distinct — an event between them still sends on its own. */
363+ const MAX_BATCH_CHARS = 512 * 1024 ;
364+ const MAX_EVENT_CHARS = 768 * 1024 ;
365+
366+ const flushEventsQueue = ( ) => {
367+ try {
368+ const queued = eventsQueue ;
369+ eventsQueue = [ ] ; /* cleared before dispatch so a throw cannot replay these events */
370+ if ( queued . length === 0 ) return ;
371+
372+ let batch = [ ] ;
373+ let batchChars = 0 ;
374+
375+ const sendBatch = ( ) => {
376+ if ( batch . length === 0 ) return ;
377+ const toSend = batch ;
378+ batch = [ ] ;
379+ batchChars = 0 ;
380+ try {
381+ /* every push site uses { log: false }, so per-event options are not forwarded */
382+ cy . task ( 'test_observability_batch' , toSend , { log : false } ) ;
383+ } catch ( e ) {
384+ warnFlushFailure ( `batch dispatch of ${ toSend . length } event(s)` , e ) ;
385+ }
386+ } ;
387+
388+ queued . forEach ( event => {
389+ try {
390+ const payload = sanitizeForTask ( event . data ) ;
391+ if ( payload === null ) {
392+ warnFlushFailure ( `unserializable payload for '${ event . task } '` ,
393+ new Error ( 'event skipped' ) ) ;
394+ return ;
395+ }
396+ const size = JSON . stringify ( payload ) . length ;
397+ if ( size > MAX_EVENT_CHARS ) {
398+ /* past the largest size measured to send; 768KB-1MB is untested, so skip */
399+ warnFlushFailure ( `event too large to send for '${ event . task } ' (${ size } chars)` ,
400+ new Error ( 'event skipped' ) ) ;
401+ return ;
402+ }
403+ /* oversized-but-sendable: let it travel alone */
404+ if ( batch . length > 0 && batchChars + size > MAX_BATCH_CHARS ) sendBatch ( ) ;
405+ batch . push ( { task : event . task , data : payload } ) ;
406+ batchChars += size ;
407+ if ( batchChars >= MAX_BATCH_CHARS ) sendBatch ( ) ;
408+ } catch ( e ) {
409+ warnFlushFailure ( `preparing '${ event . task } '` , e ) ; /* skip one event, not the rest */
410+ }
411+ } ) ;
412+
413+ sendBatch ( ) ;
414+ } catch ( e ) {
415+ warnFlushFailure ( 'queue flush' , e ) ;
416+ eventsQueue = [ ] ;
417+ }
418+ } ;
419+
342420beforeEach ( ( ) => {
343421 /* browserstack internal helper hook */
344422
345423 if ( ! Cypress . env ( 'BROWSERSTACK_O11Y_LOGS' ) ) {
346424 return ;
347425 }
348426
349- if ( eventsQueue . length > 0 ) {
350- eventsQueue . forEach ( event => {
351- const payload = sanitizeForTask ( event . data ) ;
352- if ( payload !== null ) cy . task ( event . task , payload , event . options ) ;
353- } ) ;
354- }
355- eventsQueue = [ ] ;
427+ flushEventsQueue ( ) ;
356428 testRunStarted = true ;
357429} ) ;
358430
@@ -362,13 +434,6 @@ afterEach(function() {
362434 return ;
363435 }
364436
365- if ( eventsQueue . length > 0 ) {
366- eventsQueue . forEach ( event => {
367- const payload = sanitizeForTask ( event . data ) ;
368- if ( payload !== null ) cy . task ( event . task , payload , event . options ) ;
369- } ) ;
370- }
371-
372- eventsQueue = [ ] ;
437+ flushEventsQueue ( ) ;
373438 testRunStarted = false ;
374439} ) ;
0 commit comments