-
Notifications
You must be signed in to change notification settings - Fork 44
fix(observability): prevent "Converting circular structure to JSON" crash from circular cy.task payloads [SDK-6016] #1118
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: master
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -6,6 +6,34 @@ const STEP_KEYWORDS = ['given', 'when', 'then', 'and', 'but', '*']; | |
| let eventsQueue = []; | ||
| let testRunStarted = false; | ||
|
|
||
| /* | ||
| * Command args (command.attributes.args) and cy.log items are captured raw and can hold | ||
| * circular Cypress runtime objects (e.g. a config-like object whose `renderOptions.host` | ||
| * points back to itself). cy.task() JSON-serializes its payload to ship it from the browser | ||
| * to the Node plugin process, so a circular arg makes Cypress throw | ||
| * "Converting circular structure to JSON" and aborts the run. Decycle the payload before | ||
| * handing it to cy.task so o11y instrumentation can never break the customer's tests. [SDK-6016] | ||
| */ | ||
| const getCircularReplacer = () => { | ||
| const seen = new WeakSet(); | ||
| return (key, value) => { | ||
| if (typeof value === 'object' && value !== null) { | ||
| if (seen.has(value)) return '[Circular]'; | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 💡 Suggestion — [OBSERVABILITY-IMPACT]
|
||
| seen.add(value); | ||
| } | ||
| return value; | ||
| }; | ||
| }; | ||
|
|
||
| const sanitizeForTask = (data) => { | ||
| try { | ||
| return JSON.parse(JSON.stringify(data, getCircularReplacer())); | ||
| } catch (e) { | ||
| /* Never let serialization of o11y data break the user's test run (graceful degradation). */ | ||
| return { serializationError: e && e.message ? e.message : String(e) }; | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
|
||
| } | ||
| }; | ||
|
|
||
| const browserStackLog = (message) => { | ||
|
|
||
| if (!Cypress.env('BROWSERSTACK_LOGS')) return; | ||
|
|
@@ -208,7 +236,7 @@ Cypress.on('command:enqueued', (attrs) => { | |
| if (args.includes('test_observability_log') || args.includes('test_observability_command')) return; | ||
| const message = args.reduce((result, logItem) => { | ||
| if (typeof logItem === 'object') { | ||
| return [result, JSON.stringify(logItem)].join(' '); | ||
| return [result, JSON.stringify(logItem, getCircularReplacer())].join(' '); | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
|
||
| } | ||
| return [result, logItem ? logItem.toString() : ''].join(' '); | ||
| }, ''); | ||
|
|
@@ -309,7 +337,7 @@ beforeEach(() => { | |
|
|
||
| if (eventsQueue.length > 0) { | ||
| eventsQueue.forEach(event => { | ||
| cy.task(event.task, event.data, event.options); | ||
| cy.task(event.task, sanitizeForTask(event.data), event.options); | ||
| }); | ||
| } | ||
| eventsQueue = []; | ||
|
|
@@ -324,7 +352,7 @@ afterEach(function() { | |
|
|
||
| if (eventsQueue.length > 0) { | ||
| eventsQueue.forEach(event => { | ||
| cy.task(event.task, event.data, event.options); | ||
| cy.task(event.task, sanitizeForTask(event.data), event.options); | ||
| }); | ||
| } | ||
|
|
||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
💡 Suggestion — [CODE-QUALITY]
getCircularReplacerallocates a new WeakSet on every callCreating a new WeakSet per
JSON.stringifyinvocation is correct for cycle detection — each call needs its own independent seen-set to track cycles within that specific value graph. There is no functional issue here.If
command:enqueuedis ever profiled as a hot path (high-frequency commands in large test suites), the per-call WeakSet allocation may be worth revisiting. No change required unless profiling shows it as a hotspot.