From 8264a2e039baae43f4bca0a2d53e529acfd60169 Mon Sep 17 00:00:00 2001 From: Jennifer Shehane Date: Tue, 7 Oct 2025 10:25:46 -0400 Subject: [PATCH 1/9] chore: update errors to be stringified when object --- .../cloud/api/studio/report_studio_error.ts | 5 +- packages/server/lib/cloud/studio/studio.ts | 10 ++- .../api/studio/report_studio_error_spec.ts | 39 +++++++++++ .../test/unit/cloud/studio/studio_spec.ts | 66 +++++++++++++++++++ 4 files changed, 117 insertions(+), 3 deletions(-) diff --git a/packages/server/lib/cloud/api/studio/report_studio_error.ts b/packages/server/lib/cloud/api/studio/report_studio_error.ts index 5b8e24047c6..138d66c2880 100644 --- a/packages/server/lib/cloud/api/studio/report_studio_error.ts +++ b/packages/server/lib/cloud/api/studio/report_studio_error.ts @@ -57,7 +57,10 @@ export function reportStudioError ({ let errorObject: Error if (!(error instanceof Error)) { - errorObject = new Error(String(error)) + // For strings, use them directly. For objects, serialize them. + const message = typeof error === 'string' ? error : JSON.stringify(error) + + errorObject = new Error(message) } else { errorObject = error } diff --git a/packages/server/lib/cloud/studio/studio.ts b/packages/server/lib/cloud/studio/studio.ts index 903ee02e153..141e7dfdbb9 100644 --- a/packages/server/lib/cloud/studio/studio.ts +++ b/packages/server/lib/cloud/studio/studio.ts @@ -124,7 +124,10 @@ export class StudioManager implements StudioManagerShape { let actualError: Error if (!(error instanceof Error)) { - actualError = new Error(String(error)) + // For strings, use them directly. For objects, serialize them. + const message = typeof error === 'string' ? error : JSON.stringify(error) + + actualError = new Error(message) } else { actualError = error } @@ -154,7 +157,10 @@ export class StudioManager implements StudioManagerShape { let actualError: Error if (!(error instanceof Error)) { - actualError = new Error(String(error)) + // For strings, use them directly. For objects, serialize them. + const message = typeof error === 'string' ? error : JSON.stringify(error) + + actualError = new Error(message) } else { actualError = error } diff --git a/packages/server/test/unit/cloud/api/studio/report_studio_error_spec.ts b/packages/server/test/unit/cloud/api/studio/report_studio_error_spec.ts index e31fd3fda85..ed337ebbf78 100644 --- a/packages/server/test/unit/cloud/api/studio/report_studio_error_spec.ts +++ b/packages/server/test/unit/cloud/api/studio/report_studio_error_spec.ts @@ -149,6 +149,45 @@ describe('lib/cloud/api/studio/report_studio_error', () => { ) }) + it('serializes object errors properly instead of showing [object Object]', () => { + const objectError = { + additionalData: { type: 'studio:panel:opened' }, + message: 'Something went wrong', + code: 'TELEMETRY_ERROR', + } + + reportStudioError({ + cloudApi, + studioHash: 'abc123', + projectSlug: 'test-project', + error: objectError, + studioMethod: 'telemetryService', + }) + + expect(cloudRequestStub).to.be.calledWithMatch( + 'http://localhost:1234/studio/errors', + { + studioHash: 'abc123', + projectSlug: 'test-project', + errors: [{ + name: 'Error', + message: JSON.stringify(objectError), + stack: sinon.match((stack) => stack.includes('report_studio_error_spec.ts')), + code: undefined, + errno: undefined, + studioMethod: 'telemetryService', + studioMethodArgs: undefined, + }], + }, + { + headers: { + 'Content-Type': 'application/json', + 'x-cypress-version': '1.2.3', + }, + }, + ) + }) + it('handles Error objects correctly', () => { const error = new Error('test error') diff --git a/packages/server/test/unit/cloud/studio/studio_spec.ts b/packages/server/test/unit/cloud/studio/studio_spec.ts index 7f578846bbe..aa651e1d225 100644 --- a/packages/server/test/unit/cloud/studio/studio_spec.ts +++ b/packages/server/test/unit/cloud/studio/studio_spec.ts @@ -77,6 +77,28 @@ describe('lib/cloud/studio', () => { expect(studioManager.status).to.eq('IN_ERROR') expect(studio.reportError).to.be.calledWithMatch(error, 'initializeRoutes', {}) }) + + it('serializes object errors properly instead of showing [object Object]', () => { + const objectError = { + additionalData: { type: 'studio:panel:opened' }, + message: 'Something went wrong', + } + + sinon.stub(studio, 'initializeRoutes').throws(objectError) + sinon.stub(studio, 'reportError') + + studioManager.initializeRoutes({} as any) + + expect(studioManager.status).to.eq('IN_ERROR') + expect(studio.reportError).to.be.calledWithMatch( + sinon.match((error) => { + return error instanceof Error && + error.message === JSON.stringify(objectError) + }), + 'initializeRoutes', + {}, + ) + }) }) describe('asynchronous method invocation', () => { @@ -92,6 +114,28 @@ describe('lib/cloud/studio', () => { expect(studio.reportError).to.be.calledWithMatch(error, 'initializeStudioAI', {}) }) + it('serializes object errors properly in async methods instead of showing [object Object]', async () => { + const objectError = { + additionalData: { type: 'studio:panel:opened' }, + message: 'Async error occurred', + } + + sinon.stub(studio, 'initializeStudioAI').throws(objectError) + sinon.stub(studio, 'reportError') + + await studioManager.initializeStudioAI({} as any) + + expect(studioManager.status).to.eq('IN_ERROR') + expect(studio.reportError).to.be.calledWithMatch( + sinon.match((error) => { + return error instanceof Error && + error.message === JSON.stringify(objectError) + }), + 'initializeStudioAI', + {}, + ) + }) + it('does not set state IN_ERROR when a non-essential async method fails', async () => { const error = new Error('foo') @@ -101,6 +145,28 @@ describe('lib/cloud/studio', () => { expect(studioManager.status).to.eq('ENABLED') }) + + it('serializes object errors properly in non-essential async methods', async () => { + const objectError = { + additionalData: { type: 'studio:panel:opened' }, + message: 'Non-essential error occurred', + } + + sinon.stub(studio, 'captureStudioEvent').throws(objectError) + sinon.stub(studio, 'reportError') + + await studioManager.captureStudioEvent({} as any) + + expect(studioManager.status).to.eq('ENABLED') + expect(studio.reportError).to.be.calledWithMatch( + sinon.match((error) => { + return error instanceof Error && + error.message === JSON.stringify(objectError) + }), + 'captureStudioEvent', + {}, + ) + }) }) describe('initializeRoutes', () => { From 3038d10f7f633289394b3929756c430cc549f260 Mon Sep 17 00:00:00 2001 From: Jennifer Shehane Date: Tue, 7 Oct 2025 11:38:10 -0400 Subject: [PATCH 2/9] handle circular dependencies --- .../cloud/api/studio/report_studio_error.ts | 5 +- packages/server/lib/cloud/studio/studio.ts | 9 +- packages/server/lib/cloud/studio/utils.ts | 41 +++++++++ .../api/studio/report_studio_error_spec.ts | 44 ++++++++++ .../test/unit/cloud/studio/studio_spec.ts | 84 +++++++++++++++++++ 5 files changed, 177 insertions(+), 6 deletions(-) create mode 100644 packages/server/lib/cloud/studio/utils.ts diff --git a/packages/server/lib/cloud/api/studio/report_studio_error.ts b/packages/server/lib/cloud/api/studio/report_studio_error.ts index 138d66c2880..7e5daa2a8af 100644 --- a/packages/server/lib/cloud/api/studio/report_studio_error.ts +++ b/packages/server/lib/cloud/api/studio/report_studio_error.ts @@ -3,6 +3,7 @@ import Debug from 'debug' import { stripPath } from '../../strip_path' const debug = Debug('cypress:server:cloud:api:studio:report_studio_errors') import { logError } from '@packages/stderr-filtering' +import { safeErrorSerialize } from '../../studio/utils' export interface ReportStudioErrorOptions { cloudApi: StudioCloudApi @@ -57,8 +58,8 @@ export function reportStudioError ({ let errorObject: Error if (!(error instanceof Error)) { - // For strings, use them directly. For objects, serialize them. - const message = typeof error === 'string' ? error : JSON.stringify(error) + // Use safe serialization that handles circular references and other edge cases + const message = safeErrorSerialize(error) errorObject = new Error(message) } else { diff --git a/packages/server/lib/cloud/studio/studio.ts b/packages/server/lib/cloud/studio/studio.ts index 141e7dfdbb9..68e50530f83 100644 --- a/packages/server/lib/cloud/studio/studio.ts +++ b/packages/server/lib/cloud/studio/studio.ts @@ -6,6 +6,7 @@ import path from 'path' import { reportStudioError, ReportStudioErrorOptions } from '../api/studio/report_studio_error' import crypto, { BinaryLike } from 'crypto' import { StudioElectron } from './StudioElectron' +import { safeErrorSerialize } from './utils' interface StudioServer { default: StudioServerDefaultShape } @@ -124,8 +125,8 @@ export class StudioManager implements StudioManagerShape { let actualError: Error if (!(error instanceof Error)) { - // For strings, use them directly. For objects, serialize them. - const message = typeof error === 'string' ? error : JSON.stringify(error) + // Use safe serialization that handles circular references and other edge cases + const message = safeErrorSerialize(error) actualError = new Error(message) } else { @@ -157,8 +158,8 @@ export class StudioManager implements StudioManagerShape { let actualError: Error if (!(error instanceof Error)) { - // For strings, use them directly. For objects, serialize them. - const message = typeof error === 'string' ? error : JSON.stringify(error) + // Use safe serialization that handles circular references and other edge cases + const message = safeErrorSerialize(error) actualError = new Error(message) } else { diff --git a/packages/server/lib/cloud/studio/utils.ts b/packages/server/lib/cloud/studio/utils.ts new file mode 100644 index 00000000000..21244b32ecf --- /dev/null +++ b/packages/server/lib/cloud/studio/utils.ts @@ -0,0 +1,41 @@ +/** + * Safely serializes an error object to a string, handling circular references + * and other non-serializable values that would cause JSON.stringify to throw. + */ +export function safeErrorSerialize (error: unknown): string { + if (typeof error === 'string') { + return error + } + + if (typeof error === 'object' && error !== null) { + try { + // Try JSON.stringify first, but catch any errors + return JSON.stringify(error) + } catch (e) { + // If JSON.stringify fails (e.g., circular reference), fall back to a safer approach + try { + // Try to extract meaningful properties + const errorObj = error as Record + const safeObj: Record = {} + + // Common error properties + const commonProps = ['name', 'message', 'code', 'errno', 'stack'] + + for (const prop of commonProps) { + if (prop in errorObj && typeof errorObj[prop] === 'string') { + safeObj[prop] = errorObj[prop] + } + } + + // Try to stringify the safe object + return JSON.stringify(safeObj) + } catch (e2) { + // If even that fails, use a generic fallback + return `[Non-serializable object: ${error.constructor?.name || 'Object'}]` + } + } + } + + // For primitives and other types, use String() as fallback + return String(error) +} diff --git a/packages/server/test/unit/cloud/api/studio/report_studio_error_spec.ts b/packages/server/test/unit/cloud/api/studio/report_studio_error_spec.ts index ed337ebbf78..0c6392f0465 100644 --- a/packages/server/test/unit/cloud/api/studio/report_studio_error_spec.ts +++ b/packages/server/test/unit/cloud/api/studio/report_studio_error_spec.ts @@ -188,6 +188,50 @@ describe('lib/cloud/api/studio/report_studio_error', () => { ) }) + it('handles circular reference objects safely without throwing', () => { + // Create an object with circular reference + const circularError: any = { + message: 'Circular reference error', + code: 'CIRCULAR_ERROR', + } + + circularError.self = circularError // Create circular reference + + reportStudioError({ + cloudApi, + studioHash: 'abc123', + projectSlug: 'test-project', + error: circularError, + studioMethod: 'circularTest', + }) + + expect(cloudRequestStub).to.be.calledWithMatch( + 'http://localhost:1234/studio/errors', + { + studioHash: 'abc123', + projectSlug: 'test-project', + errors: [{ + name: 'Error', + message: JSON.stringify({ + message: 'Circular reference error', + code: 'CIRCULAR_ERROR', + }), + stack: sinon.match((stack) => stack.includes('report_studio_error_spec.ts')), + code: undefined, + errno: undefined, + studioMethod: 'circularTest', + studioMethodArgs: undefined, + }], + }, + { + headers: { + 'Content-Type': 'application/json', + 'x-cypress-version': '1.2.3', + }, + }, + ) + }) + it('handles Error objects correctly', () => { const error = new Error('test error') diff --git a/packages/server/test/unit/cloud/studio/studio_spec.ts b/packages/server/test/unit/cloud/studio/studio_spec.ts index aa651e1d225..33a41bf4122 100644 --- a/packages/server/test/unit/cloud/studio/studio_spec.ts +++ b/packages/server/test/unit/cloud/studio/studio_spec.ts @@ -99,6 +99,34 @@ describe('lib/cloud/studio', () => { {}, ) }) + + it('handles circular reference objects safely in sync methods without throwing', () => { + // Create an object with circular reference + const circularError: any = { + message: 'Circular reference error', + code: 'CIRCULAR_ERROR', + } + + circularError.self = circularError // Create circular reference + + sinon.stub(studio, 'initializeRoutes').throws(circularError) + sinon.stub(studio, 'reportError') + + studioManager.initializeRoutes({} as any) + + expect(studioManager.status).to.eq('IN_ERROR') + expect(studio.reportError).to.be.calledWithMatch( + sinon.match((error) => { + return error instanceof Error && + error.message === JSON.stringify({ + message: 'Circular reference error', + code: 'CIRCULAR_ERROR', + }) + }), + 'initializeRoutes', + {}, + ) + }) }) describe('asynchronous method invocation', () => { @@ -136,6 +164,34 @@ describe('lib/cloud/studio', () => { ) }) + it('handles circular reference objects safely in async methods without throwing', async () => { + // Create an object with circular reference + const circularError: any = { + message: 'Async circular reference error', + code: 'ASYNC_CIRCULAR_ERROR', + } + + circularError.self = circularError // Create circular reference + + sinon.stub(studio, 'initializeStudioAI').throws(circularError) + sinon.stub(studio, 'reportError') + + await studioManager.initializeStudioAI({} as any) + + expect(studioManager.status).to.eq('IN_ERROR') + expect(studio.reportError).to.be.calledWithMatch( + sinon.match((error) => { + return error instanceof Error && + error.message === JSON.stringify({ + message: 'Async circular reference error', + code: 'ASYNC_CIRCULAR_ERROR', + }) + }), + 'initializeStudioAI', + {}, + ) + }) + it('does not set state IN_ERROR when a non-essential async method fails', async () => { const error = new Error('foo') @@ -167,6 +223,34 @@ describe('lib/cloud/studio', () => { {}, ) }) + + it('handles circular reference objects safely in non-essential async methods without throwing', async () => { + // Create an object with circular reference + const circularError: any = { + message: 'Non-essential circular reference error', + code: 'NON_ESSENTIAL_CIRCULAR_ERROR', + } + + circularError.self = circularError // Create circular reference + + sinon.stub(studio, 'captureStudioEvent').throws(circularError) + sinon.stub(studio, 'reportError') + + await studioManager.captureStudioEvent({} as any) + + expect(studioManager.status).to.eq('ENABLED') + expect(studio.reportError).to.be.calledWithMatch( + sinon.match((error) => { + return error instanceof Error && + error.message === JSON.stringify({ + message: 'Non-essential circular reference error', + code: 'NON_ESSENTIAL_CIRCULAR_ERROR', + }) + }), + 'captureStudioEvent', + {}, + ) + }) }) describe('initializeRoutes', () => { From cce7a78d975883bbab63920cc81a2af9df9fa0b0 Mon Sep 17 00:00:00 2001 From: Jennifer Shehane Date: Tue, 7 Oct 2025 12:48:31 -0400 Subject: [PATCH 3/9] add serialize-error npm package --- packages/server/lib/cloud/studio/utils.ts | 38 +++++-------------- packages/server/package.json | 1 + .../api/studio/report_studio_error_spec.ts | 1 + .../test/unit/cloud/studio/studio_spec.ts | 3 ++ yarn.lock | 9 ++++- 5 files changed, 22 insertions(+), 30 deletions(-) diff --git a/packages/server/lib/cloud/studio/utils.ts b/packages/server/lib/cloud/studio/utils.ts index 21244b32ecf..f96a74f5a40 100644 --- a/packages/server/lib/cloud/studio/utils.ts +++ b/packages/server/lib/cloud/studio/utils.ts @@ -1,3 +1,5 @@ +import { serializeError } from 'serialize-error' + /** * Safely serializes an error object to a string, handling circular references * and other non-serializable values that would cause JSON.stringify to throw. @@ -7,35 +9,13 @@ export function safeErrorSerialize (error: unknown): string { return error } - if (typeof error === 'object' && error !== null) { - try { - // Try JSON.stringify first, but catch any errors - return JSON.stringify(error) - } catch (e) { - // If JSON.stringify fails (e.g., circular reference), fall back to a safer approach - try { - // Try to extract meaningful properties - const errorObj = error as Record - const safeObj: Record = {} - - // Common error properties - const commonProps = ['name', 'message', 'code', 'errno', 'stack'] + try { + // Use serialize-error package to handle complex error objects safely + const serialized = serializeError(error) - for (const prop of commonProps) { - if (prop in errorObj && typeof errorObj[prop] === 'string') { - safeObj[prop] = errorObj[prop] - } - } - - // Try to stringify the safe object - return JSON.stringify(safeObj) - } catch (e2) { - // If even that fails, use a generic fallback - return `[Non-serializable object: ${error.constructor?.name || 'Object'}]` - } - } + return JSON.stringify(serialized) + } catch (e) { + // If even serialize-error fails, use a generic fallback + return `[Non-serializable object: ${error?.constructor?.name || 'Object'}]` } - - // For primitives and other types, use String() as fallback - return String(error) } diff --git a/packages/server/package.json b/packages/server/package.json index 7dfadcb0f2a..8415e16d31a 100644 --- a/packages/server/package.json +++ b/packages/server/package.json @@ -200,6 +200,7 @@ "proxyquire": "2.1.3", "repl.history": "0.1.4", "request-promise": "4.2.6", + "serialize-error": "12.0.0", "sinon": "17.0.1", "snap-shot-it": "7.9.10", "socket.io": "4.0.1", diff --git a/packages/server/test/unit/cloud/api/studio/report_studio_error_spec.ts b/packages/server/test/unit/cloud/api/studio/report_studio_error_spec.ts index 0c6392f0465..0b58ee93176 100644 --- a/packages/server/test/unit/cloud/api/studio/report_studio_error_spec.ts +++ b/packages/server/test/unit/cloud/api/studio/report_studio_error_spec.ts @@ -215,6 +215,7 @@ describe('lib/cloud/api/studio/report_studio_error', () => { message: JSON.stringify({ message: 'Circular reference error', code: 'CIRCULAR_ERROR', + self: '[Circular]', }), stack: sinon.match((stack) => stack.includes('report_studio_error_spec.ts')), code: undefined, diff --git a/packages/server/test/unit/cloud/studio/studio_spec.ts b/packages/server/test/unit/cloud/studio/studio_spec.ts index 33a41bf4122..609c3e7c2f6 100644 --- a/packages/server/test/unit/cloud/studio/studio_spec.ts +++ b/packages/server/test/unit/cloud/studio/studio_spec.ts @@ -121,6 +121,7 @@ describe('lib/cloud/studio', () => { error.message === JSON.stringify({ message: 'Circular reference error', code: 'CIRCULAR_ERROR', + self: '[Circular]', }) }), 'initializeRoutes', @@ -185,6 +186,7 @@ describe('lib/cloud/studio', () => { error.message === JSON.stringify({ message: 'Async circular reference error', code: 'ASYNC_CIRCULAR_ERROR', + self: '[Circular]', }) }), 'initializeStudioAI', @@ -245,6 +247,7 @@ describe('lib/cloud/studio', () => { error.message === JSON.stringify({ message: 'Non-essential circular reference error', code: 'NON_ESSENTIAL_CIRCULAR_ERROR', + self: '[Circular]', }) }), 'captureStudioEvent', diff --git a/yarn.lock b/yarn.lock index 6ac4e67f682..ffb0fe18dfb 100644 --- a/yarn.lock +++ b/yarn.lock @@ -29188,6 +29188,13 @@ sentence-case@^3.0.4: tslib "^2.0.3" upper-case-first "^2.0.2" +serialize-error@12.0.0: + version "12.0.0" + resolved "https://registry.yarnpkg.com/serialize-error/-/serialize-error-12.0.0.tgz#aed3d5abff192c855707513929bf8bf48d712194" + integrity sha512-ZYkZLAvKTKQXWuh5XpBw7CdbSzagarX39WyZ2H07CDLC5/KfsRGlIXV8d4+tfqX1M7916mRqR1QfNHSij+c9Pw== + dependencies: + type-fest "^4.31.0" + serialize-error@^7.0.1: version "7.0.1" resolved "https://registry.yarnpkg.com/serialize-error/-/serialize-error-7.0.1.tgz#f1360b0447f61ffb483ec4157c737fab7d778e18" @@ -32065,7 +32072,7 @@ type-fest@^2.12.2, type-fest@^2.13.0, type-fest@^2.3.4: resolved "https://registry.npmjs.org/type-fest/-/type-fest-2.19.0.tgz#88068015bb33036a598b952e55e9311a60fd3a9b" integrity sha512-RAH822pAdBgcNMAfWnCBU3CFZcfZ/i1eZjwFU/dsLKumyuuP3niueg2UAukXYF0E2AAoc82ZSSf9J0WQBinzHA== -type-fest@^4.26.1, type-fest@^4.41.0, type-fest@^4.6.0, type-fest@^4.7.1: +type-fest@^4.26.1, type-fest@^4.31.0, type-fest@^4.41.0, type-fest@^4.6.0, type-fest@^4.7.1: version "4.41.0" resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-4.41.0.tgz#6ae1c8e5731273c2bf1f58ad39cbae2c91a46c58" integrity sha512-TeTSQ6H5YHvpqVwBRcnLDCBnDOHWYu7IvGbHT6N8AOymcr9PJGjc1GTtiWZTYg0NCgYwvnYWEkVChQAr9bjfwA== From dd86c868443cd5f8c0b7e9b26429bf6054dabcdd Mon Sep 17 00:00:00 2001 From: Jennifer Shehane Date: Wed, 8 Oct 2025 10:17:04 -0400 Subject: [PATCH 4/9] downgrade serialize-error to cjs compatible version --- packages/server/lib/cloud/studio/utils.ts | 2 +- packages/server/package.json | 2 +- yarn.lock | 9 +-------- 3 files changed, 3 insertions(+), 10 deletions(-) diff --git a/packages/server/lib/cloud/studio/utils.ts b/packages/server/lib/cloud/studio/utils.ts index f96a74f5a40..be581c26689 100644 --- a/packages/server/lib/cloud/studio/utils.ts +++ b/packages/server/lib/cloud/studio/utils.ts @@ -1,4 +1,4 @@ -import { serializeError } from 'serialize-error' +const { serializeError } = require('serialize-error') /** * Safely serializes an error object to a string, handling circular references diff --git a/packages/server/package.json b/packages/server/package.json index 8415e16d31a..034157656a8 100644 --- a/packages/server/package.json +++ b/packages/server/package.json @@ -200,7 +200,7 @@ "proxyquire": "2.1.3", "repl.history": "0.1.4", "request-promise": "4.2.6", - "serialize-error": "12.0.0", + "serialize-error": "^7.0.1", "sinon": "17.0.1", "snap-shot-it": "7.9.10", "socket.io": "4.0.1", diff --git a/yarn.lock b/yarn.lock index b804b9d0425..a68c30e151c 100644 --- a/yarn.lock +++ b/yarn.lock @@ -29188,13 +29188,6 @@ sentence-case@^3.0.4: tslib "^2.0.3" upper-case-first "^2.0.2" -serialize-error@12.0.0: - version "12.0.0" - resolved "https://registry.yarnpkg.com/serialize-error/-/serialize-error-12.0.0.tgz#aed3d5abff192c855707513929bf8bf48d712194" - integrity sha512-ZYkZLAvKTKQXWuh5XpBw7CdbSzagarX39WyZ2H07CDLC5/KfsRGlIXV8d4+tfqX1M7916mRqR1QfNHSij+c9Pw== - dependencies: - type-fest "^4.31.0" - serialize-error@^7.0.1: version "7.0.1" resolved "https://registry.yarnpkg.com/serialize-error/-/serialize-error-7.0.1.tgz#f1360b0447f61ffb483ec4157c737fab7d778e18" @@ -32072,7 +32065,7 @@ type-fest@^2.12.2, type-fest@^2.13.0, type-fest@^2.3.4: resolved "https://registry.npmjs.org/type-fest/-/type-fest-2.19.0.tgz#88068015bb33036a598b952e55e9311a60fd3a9b" integrity sha512-RAH822pAdBgcNMAfWnCBU3CFZcfZ/i1eZjwFU/dsLKumyuuP3niueg2UAukXYF0E2AAoc82ZSSf9J0WQBinzHA== -type-fest@^4.26.1, type-fest@^4.31.0, type-fest@^4.41.0, type-fest@^4.6.0, type-fest@^4.7.1: +type-fest@^4.26.1, type-fest@^4.41.0, type-fest@^4.6.0, type-fest@^4.7.1: version "4.41.0" resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-4.41.0.tgz#6ae1c8e5731273c2bf1f58ad39cbae2c91a46c58" integrity sha512-TeTSQ6H5YHvpqVwBRcnLDCBnDOHWYu7IvGbHT6N8AOymcr9PJGjc1GTtiWZTYg0NCgYwvnYWEkVChQAr9bjfwA== From 2d35253396c79a07b117a992ccca3455b6c40946 Mon Sep 17 00:00:00 2001 From: Jennifer Shehane Date: Wed, 8 Oct 2025 10:20:50 -0400 Subject: [PATCH 5/9] move to prod dep --- packages/server/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/server/package.json b/packages/server/package.json index 034157656a8..2e15dc178f8 100644 --- a/packages/server/package.json +++ b/packages/server/package.json @@ -118,6 +118,7 @@ "sanitize-filename": "1.6.3", "semver": "^7.7.1", "send": "0.19.0", + "serialize-error": "^7.0.1", "shell-env": "3.0.1", "signal-exit": "3.0.7", "squirrelly": "7.9.2", @@ -200,7 +201,6 @@ "proxyquire": "2.1.3", "repl.history": "0.1.4", "request-promise": "4.2.6", - "serialize-error": "^7.0.1", "sinon": "17.0.1", "snap-shot-it": "7.9.10", "socket.io": "4.0.1", From c9c54f70ab19f4d6ded3d2c6a3903730d727f3e9 Mon Sep 17 00:00:00 2001 From: Jennifer Shehane Date: Wed, 8 Oct 2025 10:27:32 -0400 Subject: [PATCH 6/9] move safeErrorSerialize further out into Cloud dir --- .../cloud/api/studio/report_studio_error.ts | 4 +- packages/server/lib/cloud/exception.ts | 22 ++++ packages/server/lib/cloud/studio/studio.ts | 6 +- packages/server/lib/cloud/studio/utils.ts | 21 ---- .../server/test/unit/cloud/exceptions_spec.js | 112 ++++++++++++++++++ 5 files changed, 139 insertions(+), 26 deletions(-) delete mode 100644 packages/server/lib/cloud/studio/utils.ts diff --git a/packages/server/lib/cloud/api/studio/report_studio_error.ts b/packages/server/lib/cloud/api/studio/report_studio_error.ts index 7e5daa2a8af..4ea08976570 100644 --- a/packages/server/lib/cloud/api/studio/report_studio_error.ts +++ b/packages/server/lib/cloud/api/studio/report_studio_error.ts @@ -3,7 +3,7 @@ import Debug from 'debug' import { stripPath } from '../../strip_path' const debug = Debug('cypress:server:cloud:api:studio:report_studio_errors') import { logError } from '@packages/stderr-filtering' -import { safeErrorSerialize } from '../../studio/utils' +import exception from '../../exception' export interface ReportStudioErrorOptions { cloudApi: StudioCloudApi @@ -59,7 +59,7 @@ export function reportStudioError ({ if (!(error instanceof Error)) { // Use safe serialization that handles circular references and other edge cases - const message = safeErrorSerialize(error) + const message = exception.safeErrorSerialize(error) errorObject = new Error(message) } else { diff --git a/packages/server/lib/cloud/exception.ts b/packages/server/lib/cloud/exception.ts index 815591cbb3c..6683e788496 100644 --- a/packages/server/lib/cloud/exception.ts +++ b/packages/server/lib/cloud/exception.ts @@ -6,7 +6,29 @@ import user from './user' import system from '../util/system' import { stripPath } from './strip_path' +const { serializeError } = require('serialize-error') + export = { + /** + * Safely serializes an error object to a string, handling circular references + * and other non-serializable values that would cause JSON.stringify to throw. + */ + safeErrorSerialize (error: unknown): string { + if (typeof error === 'string') { + return error + } + + try { + // Use serialize-error package to handle complex error objects safely + const serialized = serializeError(error) + + return JSON.stringify(serialized) + } catch (e) { + // If even serialize-error fails, use a generic fallback + return `[Non-serializable object: ${error?.constructor?.name || 'Object'}]` + } + }, + getErr (err: Error) { return { name: stripPath(err.name), diff --git a/packages/server/lib/cloud/studio/studio.ts b/packages/server/lib/cloud/studio/studio.ts index 68e50530f83..9869fe94bff 100644 --- a/packages/server/lib/cloud/studio/studio.ts +++ b/packages/server/lib/cloud/studio/studio.ts @@ -6,7 +6,7 @@ import path from 'path' import { reportStudioError, ReportStudioErrorOptions } from '../api/studio/report_studio_error' import crypto, { BinaryLike } from 'crypto' import { StudioElectron } from './StudioElectron' -import { safeErrorSerialize } from './utils' +import exception from '../exception' interface StudioServer { default: StudioServerDefaultShape } @@ -126,7 +126,7 @@ export class StudioManager implements StudioManagerShape { if (!(error instanceof Error)) { // Use safe serialization that handles circular references and other edge cases - const message = safeErrorSerialize(error) + const message = exception.safeErrorSerialize(error) actualError = new Error(message) } else { @@ -159,7 +159,7 @@ export class StudioManager implements StudioManagerShape { if (!(error instanceof Error)) { // Use safe serialization that handles circular references and other edge cases - const message = safeErrorSerialize(error) + const message = exception.safeErrorSerialize(error) actualError = new Error(message) } else { diff --git a/packages/server/lib/cloud/studio/utils.ts b/packages/server/lib/cloud/studio/utils.ts deleted file mode 100644 index be581c26689..00000000000 --- a/packages/server/lib/cloud/studio/utils.ts +++ /dev/null @@ -1,21 +0,0 @@ -const { serializeError } = require('serialize-error') - -/** - * Safely serializes an error object to a string, handling circular references - * and other non-serializable values that would cause JSON.stringify to throw. - */ -export function safeErrorSerialize (error: unknown): string { - if (typeof error === 'string') { - return error - } - - try { - // Use serialize-error package to handle complex error objects safely - const serialized = serializeError(error) - - return JSON.stringify(serialized) - } catch (e) { - // If even serialize-error fails, use a generic fallback - return `[Non-serializable object: ${error?.constructor?.name || 'Object'}]` - } -} diff --git a/packages/server/test/unit/cloud/exceptions_spec.js b/packages/server/test/unit/cloud/exceptions_spec.js index 20bbe786f79..4c800287d5f 100644 --- a/packages/server/test/unit/cloud/exceptions_spec.js +++ b/packages/server/test/unit/cloud/exceptions_spec.js @@ -228,4 +228,116 @@ at bar bar.js:92\ }) }) }) + + context('.safeErrorSerialize', () => { + it('returns string as-is when error is already a string', () => { + const stringError = 'Simple string error' + + expect(exception.safeErrorSerialize(stringError)).to.eq('Simple string error') + }) + + it('serializes plain objects properly', () => { + const objectError = { + additionalData: { type: 'studio:panel:opened' }, + message: 'Something went wrong', + code: 'TELEMETRY_ERROR', + } + + const result = exception.safeErrorSerialize(objectError) + + expect(result).to.eq(JSON.stringify(objectError)) + }) + + it('handles circular reference objects safely without throwing', () => { + // Create an object with circular reference + const circularError = { + message: 'Circular reference error', + code: 'CIRCULAR_ERROR', + } + + circularError.self = circularError // Create circular reference + + const result = exception.safeErrorSerialize(circularError) + + expect(result).to.eq(JSON.stringify({ + message: 'Circular reference error', + code: 'CIRCULAR_ERROR', + self: '[Circular]', + })) + }) + + it('handles Error objects correctly', () => { + const error = new Error('test error') + + error.code = 'TEST_CODE' + error.errno = 123 + + const result = exception.safeErrorSerialize(error) + + // serializeError should preserve Error properties + const parsed = JSON.parse(result) + + expect(parsed.message).to.eq('test error') + expect(parsed.name).to.eq('Error') + expect(parsed.code).to.eq('TEST_CODE') + expect(parsed.errno).to.eq(123) + }) + + it('handles null and undefined gracefully', () => { + expect(exception.safeErrorSerialize(null)).to.eq('null') + expect(exception.safeErrorSerialize(undefined)).to.be.undefined + }) + + it('handles primitive types', () => { + expect(exception.safeErrorSerialize(42)).to.eq('42') + expect(exception.safeErrorSerialize(true)).to.eq('true') + expect(exception.safeErrorSerialize(false)).to.eq('false') + }) + + it('provides fallback for non-serializable objects', () => { + // Create an object that might cause issues + const problematicObject = { + get value () { + throw new Error('Cannot access value') + }, + } + + const result = exception.safeErrorSerialize(problematicObject) + + expect(result).to.match(/^\[Non-serializable object:/) + }) + + it('handles deeply nested objects', () => { + const deepObject = { + level1: { + level2: { + level3: { + level4: { + message: 'Deep error', + data: [1, 2, 3, { nested: true }], + }, + }, + }, + }, + } + + const result = exception.safeErrorSerialize(deepObject) + + expect(result).to.eq(JSON.stringify(deepObject)) + }) + + it('handles arrays with mixed content', () => { + const arrayError = [ + 'string', + 42, + { message: 'object in array' }, + null, + undefined, + ] + + const result = exception.safeErrorSerialize(arrayError) + + expect(result).to.eq(JSON.stringify(arrayError)) + }) + }) }) From fc7fafc7ebf223f8d9fa2a9795138db57add3b36 Mon Sep 17 00:00:00 2001 From: Jennifer Shehane Date: Wed, 8 Oct 2025 10:31:23 -0400 Subject: [PATCH 7/9] remove redundancies in studio tests --- .../api/studio/report_studio_error_spec.ts | 49 +-------- .../test/unit/cloud/studio/studio_spec.ts | 102 ++---------------- 2 files changed, 8 insertions(+), 143 deletions(-) diff --git a/packages/server/test/unit/cloud/api/studio/report_studio_error_spec.ts b/packages/server/test/unit/cloud/api/studio/report_studio_error_spec.ts index 0b58ee93176..b99f44a12ac 100644 --- a/packages/server/test/unit/cloud/api/studio/report_studio_error_spec.ts +++ b/packages/server/test/unit/cloud/api/studio/report_studio_error_spec.ts @@ -149,7 +149,7 @@ describe('lib/cloud/api/studio/report_studio_error', () => { ) }) - it('serializes object errors properly instead of showing [object Object]', () => { + it('converts non-Error objects to Error instances', () => { const objectError = { additionalData: { type: 'studio:panel:opened' }, message: 'Something went wrong', @@ -171,7 +171,7 @@ describe('lib/cloud/api/studio/report_studio_error', () => { projectSlug: 'test-project', errors: [{ name: 'Error', - message: JSON.stringify(objectError), + message: sinon.match.string, stack: sinon.match((stack) => stack.includes('report_studio_error_spec.ts')), code: undefined, errno: undefined, @@ -188,51 +188,6 @@ describe('lib/cloud/api/studio/report_studio_error', () => { ) }) - it('handles circular reference objects safely without throwing', () => { - // Create an object with circular reference - const circularError: any = { - message: 'Circular reference error', - code: 'CIRCULAR_ERROR', - } - - circularError.self = circularError // Create circular reference - - reportStudioError({ - cloudApi, - studioHash: 'abc123', - projectSlug: 'test-project', - error: circularError, - studioMethod: 'circularTest', - }) - - expect(cloudRequestStub).to.be.calledWithMatch( - 'http://localhost:1234/studio/errors', - { - studioHash: 'abc123', - projectSlug: 'test-project', - errors: [{ - name: 'Error', - message: JSON.stringify({ - message: 'Circular reference error', - code: 'CIRCULAR_ERROR', - self: '[Circular]', - }), - stack: sinon.match((stack) => stack.includes('report_studio_error_spec.ts')), - code: undefined, - errno: undefined, - studioMethod: 'circularTest', - studioMethodArgs: undefined, - }], - }, - { - headers: { - 'Content-Type': 'application/json', - 'x-cypress-version': '1.2.3', - }, - }, - ) - }) - it('handles Error objects correctly', () => { const error = new Error('test error') diff --git a/packages/server/test/unit/cloud/studio/studio_spec.ts b/packages/server/test/unit/cloud/studio/studio_spec.ts index 609c3e7c2f6..31b21b0c32c 100644 --- a/packages/server/test/unit/cloud/studio/studio_spec.ts +++ b/packages/server/test/unit/cloud/studio/studio_spec.ts @@ -78,7 +78,7 @@ describe('lib/cloud/studio', () => { expect(studio.reportError).to.be.calledWithMatch(error, 'initializeRoutes', {}) }) - it('serializes object errors properly instead of showing [object Object]', () => { + it('handles non-Error objects by converting them to Error instances', () => { const objectError = { additionalData: { type: 'studio:panel:opened' }, message: 'Something went wrong', @@ -92,37 +92,7 @@ describe('lib/cloud/studio', () => { expect(studioManager.status).to.eq('IN_ERROR') expect(studio.reportError).to.be.calledWithMatch( sinon.match((error) => { - return error instanceof Error && - error.message === JSON.stringify(objectError) - }), - 'initializeRoutes', - {}, - ) - }) - - it('handles circular reference objects safely in sync methods without throwing', () => { - // Create an object with circular reference - const circularError: any = { - message: 'Circular reference error', - code: 'CIRCULAR_ERROR', - } - - circularError.self = circularError // Create circular reference - - sinon.stub(studio, 'initializeRoutes').throws(circularError) - sinon.stub(studio, 'reportError') - - studioManager.initializeRoutes({} as any) - - expect(studioManager.status).to.eq('IN_ERROR') - expect(studio.reportError).to.be.calledWithMatch( - sinon.match((error) => { - return error instanceof Error && - error.message === JSON.stringify({ - message: 'Circular reference error', - code: 'CIRCULAR_ERROR', - self: '[Circular]', - }) + return error instanceof Error }), 'initializeRoutes', {}, @@ -143,7 +113,7 @@ describe('lib/cloud/studio', () => { expect(studio.reportError).to.be.calledWithMatch(error, 'initializeStudioAI', {}) }) - it('serializes object errors properly in async methods instead of showing [object Object]', async () => { + it('handles non-Error objects in async methods by converting them to Error instances', async () => { const objectError = { additionalData: { type: 'studio:panel:opened' }, message: 'Async error occurred', @@ -157,37 +127,7 @@ describe('lib/cloud/studio', () => { expect(studioManager.status).to.eq('IN_ERROR') expect(studio.reportError).to.be.calledWithMatch( sinon.match((error) => { - return error instanceof Error && - error.message === JSON.stringify(objectError) - }), - 'initializeStudioAI', - {}, - ) - }) - - it('handles circular reference objects safely in async methods without throwing', async () => { - // Create an object with circular reference - const circularError: any = { - message: 'Async circular reference error', - code: 'ASYNC_CIRCULAR_ERROR', - } - - circularError.self = circularError // Create circular reference - - sinon.stub(studio, 'initializeStudioAI').throws(circularError) - sinon.stub(studio, 'reportError') - - await studioManager.initializeStudioAI({} as any) - - expect(studioManager.status).to.eq('IN_ERROR') - expect(studio.reportError).to.be.calledWithMatch( - sinon.match((error) => { - return error instanceof Error && - error.message === JSON.stringify({ - message: 'Async circular reference error', - code: 'ASYNC_CIRCULAR_ERROR', - self: '[Circular]', - }) + return error instanceof Error }), 'initializeStudioAI', {}, @@ -204,7 +144,7 @@ describe('lib/cloud/studio', () => { expect(studioManager.status).to.eq('ENABLED') }) - it('serializes object errors properly in non-essential async methods', async () => { + it('handles non-Error objects in non-essential async methods without changing status', async () => { const objectError = { additionalData: { type: 'studio:panel:opened' }, message: 'Non-essential error occurred', @@ -218,37 +158,7 @@ describe('lib/cloud/studio', () => { expect(studioManager.status).to.eq('ENABLED') expect(studio.reportError).to.be.calledWithMatch( sinon.match((error) => { - return error instanceof Error && - error.message === JSON.stringify(objectError) - }), - 'captureStudioEvent', - {}, - ) - }) - - it('handles circular reference objects safely in non-essential async methods without throwing', async () => { - // Create an object with circular reference - const circularError: any = { - message: 'Non-essential circular reference error', - code: 'NON_ESSENTIAL_CIRCULAR_ERROR', - } - - circularError.self = circularError // Create circular reference - - sinon.stub(studio, 'captureStudioEvent').throws(circularError) - sinon.stub(studio, 'reportError') - - await studioManager.captureStudioEvent({} as any) - - expect(studioManager.status).to.eq('ENABLED') - expect(studio.reportError).to.be.calledWithMatch( - sinon.match((error) => { - return error instanceof Error && - error.message === JSON.stringify({ - message: 'Non-essential circular reference error', - code: 'NON_ESSENTIAL_CIRCULAR_ERROR', - self: '[Circular]', - }) + return error instanceof Error }), 'captureStudioEvent', {}, From d7b191192d9dbd62d450d302f50ad6b699c13462 Mon Sep 17 00:00:00 2001 From: Jennifer Shehane Date: Wed, 8 Oct 2025 10:35:04 -0400 Subject: [PATCH 8/9] handle undefined --- packages/server/lib/cloud/exception.ts | 9 ++++++++- packages/server/test/unit/cloud/exceptions_spec.js | 2 +- 2 files changed, 9 insertions(+), 2 deletions(-) diff --git a/packages/server/lib/cloud/exception.ts b/packages/server/lib/cloud/exception.ts index 6683e788496..ad4c15b676f 100644 --- a/packages/server/lib/cloud/exception.ts +++ b/packages/server/lib/cloud/exception.ts @@ -22,7 +22,14 @@ export = { // Use serialize-error package to handle complex error objects safely const serialized = serializeError(error) - return JSON.stringify(serialized) + const result = JSON.stringify(serialized) + + // JSON.stringify returns undefined for undefined input, but we need to return a string + if (result === undefined) { + return 'undefined' + } + + return result } catch (e) { // If even serialize-error fails, use a generic fallback return `[Non-serializable object: ${error?.constructor?.name || 'Object'}]` diff --git a/packages/server/test/unit/cloud/exceptions_spec.js b/packages/server/test/unit/cloud/exceptions_spec.js index 4c800287d5f..f73cdeac7f4 100644 --- a/packages/server/test/unit/cloud/exceptions_spec.js +++ b/packages/server/test/unit/cloud/exceptions_spec.js @@ -285,7 +285,7 @@ at bar bar.js:92\ it('handles null and undefined gracefully', () => { expect(exception.safeErrorSerialize(null)).to.eq('null') - expect(exception.safeErrorSerialize(undefined)).to.be.undefined + expect(exception.safeErrorSerialize(undefined)).to.eq('undefined') }) it('handles primitive types', () => { From 0be6bef23462ae5795914a99027c272c1fbc9a3c Mon Sep 17 00:00:00 2001 From: Jennifer Shehane Date: Wed, 8 Oct 2025 15:37:50 -0400 Subject: [PATCH 9/9] fix undefined --- packages/server/lib/cloud/exception.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/server/lib/cloud/exception.ts b/packages/server/lib/cloud/exception.ts index ad4c15b676f..5b4b25bd20c 100644 --- a/packages/server/lib/cloud/exception.ts +++ b/packages/server/lib/cloud/exception.ts @@ -25,7 +25,7 @@ export = { const result = JSON.stringify(serialized) // JSON.stringify returns undefined for undefined input, but we need to return a string - if (result === undefined) { + if (typeof result === 'undefined') { return 'undefined' }