@@ -6,6 +6,40 @@ const STEP_KEYWORDS = ['given', 'when', 'then', 'and', 'but', '*'];
66let eventsQueue = [ ] ;
77let testRunStarted = false ;
88
9+ /*
10+ * Command args (command.attributes.args) and cy.log items are captured raw and can hold
11+ * circular Cypress runtime objects (e.g. a config-like object whose `renderOptions.host`
12+ * points back to itself). cy.task() JSON-serializes its payload to ship it from the browser
13+ * to the Node plugin process, so a circular arg makes Cypress throw
14+ * "Converting circular structure to JSON" and aborts the run. Decycle the payload before
15+ * handing it to cy.task so o11y instrumentation can never break the customer's tests. [SDK-6016]
16+ */
17+ const getCircularReplacer = ( ) => {
18+ const seen = new WeakSet ( ) ;
19+ return ( key , value ) => {
20+ if ( typeof value === 'object' && value !== null ) {
21+ if ( seen . has ( value ) ) return '[Circular]' ;
22+ seen . add ( value ) ;
23+ }
24+ return value ;
25+ } ;
26+ } ;
27+
28+ /*
29+ * Returns a decycled, JSON-safe plain object, or `null` if the payload still cannot be
30+ * serialized for a non-circular reason (BigInt, a throwing toJSON, a Proxy trap, etc.).
31+ * `null` is a "skip this event" sentinel — callers must NOT forward it to cy.task, because
32+ * the Node o11y handler expects a structured event payload, not an error stub. Skipping keeps
33+ * graceful degradation total: no crash, and no malformed event reaches the collector.
34+ */
35+ const sanitizeForTask = ( data ) => {
36+ try {
37+ return JSON . parse ( JSON . stringify ( data , getCircularReplacer ( ) ) ) ;
38+ } catch ( e ) {
39+ return null ;
40+ }
41+ } ;
42+
943const browserStackLog = ( message ) => {
1044
1145 if ( ! Cypress . env ( 'BROWSERSTACK_LOGS' ) ) return ;
@@ -208,7 +242,12 @@ Cypress.on('command:enqueued', (attrs) => {
208242 if ( args . includes ( 'test_observability_log' ) || args . includes ( 'test_observability_command' ) ) return ;
209243 const message = args . reduce ( ( result , logItem ) => {
210244 if ( typeof logItem === 'object' ) {
211- return [ result , JSON . stringify ( logItem ) ] . join ( ' ' ) ;
245+ /* Route through sanitizeForTask so a non-circular serialization failure can never
246+ * throw out of the command:enqueued handler (same graceful-degradation contract as
247+ * the flush sites). sanitizeForTask returns a decycled plain object (safe to stringify)
248+ * or null; on null, contribute nothing for this item rather than crash. */
249+ const safeLog = sanitizeForTask ( logItem ) ;
250+ return [ result , safeLog === null ? '' : JSON . stringify ( safeLog ) ] . join ( ' ' ) ;
212251 }
213252 return [ result , logItem ? logItem . toString ( ) : '' ] . join ( ' ' ) ;
214253 } , '' ) ;
@@ -309,7 +348,8 @@ beforeEach(() => {
309348
310349 if ( eventsQueue . length > 0 ) {
311350 eventsQueue . forEach ( event => {
312- cy . task ( event . task , event . data , event . options ) ;
351+ const payload = sanitizeForTask ( event . data ) ;
352+ if ( payload !== null ) cy . task ( event . task , payload , event . options ) ;
313353 } ) ;
314354 }
315355 eventsQueue = [ ] ;
@@ -324,7 +364,8 @@ afterEach(function() {
324364
325365 if ( eventsQueue . length > 0 ) {
326366 eventsQueue . forEach ( event => {
327- cy . task ( event . task , event . data , event . options ) ;
367+ const payload = sanitizeForTask ( event . data ) ;
368+ if ( payload !== null ) cy . task ( event . task , payload , event . options ) ;
328369 } ) ;
329370 }
330371
0 commit comments