diff --git a/packages/core/src/domain/error/error.spec.ts b/packages/core/src/domain/error/error.spec.ts index b0580ecc37..ce6c9f4a44 100644 --- a/packages/core/src/domain/error/error.spec.ts +++ b/packages/core/src/domain/error/error.spec.ts @@ -234,16 +234,6 @@ describe('flattenErrorCauses', () => { expect(errorCauses).toEqual(undefined) }) - it('should return undefined if cause is not of type Error', () => { - const error = new Error('foo') as ErrorWithCause - const nestedError = { biz: 'buz', cause: new Error('boo') } as unknown as Error - - error.cause = nestedError - - const errorCauses = flattenErrorCauses(error, ErrorSource.LOGGER) - expect(errorCauses?.length).toEqual(undefined) - }) - it('should use error to extract stack trace', () => { const error = new Error('foo') as ErrorWithCause @@ -259,6 +249,96 @@ describe('flattenErrorCauses', () => { const errorCauses = flattenErrorCauses(error, ErrorSource.LOGGER) expect(errorCauses?.length).toEqual(10) }) + + describe('with non-Error values', () => { + it('should handle string cause with consistent structure', () => { + const error = new Error('main') as ErrorWithCause + error.cause = 'string cause' + + const causes = flattenErrorCauses(error, ErrorSource.CUSTOM) + expect(causes?.length).toBe(1) + expect(causes?.[0]).toEqual({ + message: '"string cause"', // JSON stringified + source: ErrorSource.CUSTOM, + type: undefined, + stack: undefined, + }) + }) + + it('should handle object cause with consistent structure', () => { + const error = new Error('main') as ErrorWithCause + error.cause = { code: 'ERR_001', details: 'Invalid input' } + + const causes = flattenErrorCauses(error, ErrorSource.CUSTOM) + expect(causes?.length).toBe(1) + expect(causes?.[0]).toEqual({ + message: '{"code":"ERR_001","details":"Invalid input"}', + source: ErrorSource.CUSTOM, + type: undefined, + stack: undefined, + }) + }) + + it('should handle number cause with consistent structure', () => { + const error = new Error('main') as ErrorWithCause + error.cause = 42 + + const causes = flattenErrorCauses(error, ErrorSource.CUSTOM) + expect(causes?.length).toBe(1) + expect(causes?.[0]).toEqual({ + message: '42', + source: ErrorSource.CUSTOM, + type: undefined, + stack: undefined, + }) + }) + + it('should handle mixed Error and non-Error chain', () => { + const error1 = new Error('first') as ErrorWithCause + const error2 = new Error('second') as ErrorWithCause + error1.cause = error2 + error2.cause = { code: 'ERR_ROOT' } + + const causes = flattenErrorCauses(error1, ErrorSource.CUSTOM) + expect(causes?.length).toBe(2) + + // First cause: Error with full structure + expect(causes?.[0].message).toBe('second') + expect(causes?.[0].type).toBe('Error') + expect(causes?.[0].stack).toContain('Error') + + // Second cause: Object with normalized structure + expect(causes?.[1]).toEqual({ + message: '{"code":"ERR_ROOT"}', + source: ErrorSource.CUSTOM, + type: undefined, + stack: undefined, + }) + }) + + it('should stop chain after non-Error cause', () => { + const error = new Error('main') as ErrorWithCause + error.cause = { value: 'data', cause: new Error('ignored') } + + const causes = flattenErrorCauses(error, ErrorSource.CUSTOM) + expect(causes?.length).toBe(1) + // The entire object is captured, nested cause is sanitized + expect(causes?.[0].message).toContain('"value":"data"') + expect(causes?.[0].type).toBeUndefined() + }) + + it('should handle null cause', () => { + const error = new Error('main') as ErrorWithCause + error.cause = null + expect(flattenErrorCauses(error, ErrorSource.CUSTOM)).toBeUndefined() + }) + + it('should handle undefined cause', () => { + const error = new Error('main') as ErrorWithCause + error.cause = undefined + expect(flattenErrorCauses(error, ErrorSource.CUSTOM)).toBeUndefined() + }) + }) }) describe('isError', () => { diff --git a/packages/core/src/domain/error/error.ts b/packages/core/src/domain/error/error.ts index 52a42fb5a5..94a2da2145 100644 --- a/packages/core/src/domain/error/error.ts +++ b/packages/core/src/domain/error/error.ts @@ -21,6 +21,32 @@ interface RawErrorParams { handling: ErrorHandling } +function computeErrorBase({ + originalError, + stackTrace, + source, + useFallbackStack = true, + nonErrorPrefix, +}: { + originalError: unknown + stackTrace?: StackTrace + source: ErrorSource + useFallbackStack?: boolean + nonErrorPrefix?: NonErrorPrefix +}) { + const isErrorInstance = isError(originalError) + if (!stackTrace && isErrorInstance) { + stackTrace = computeStackTrace(originalError) + } + + return { + source, + type: stackTrace ? stackTrace.name : undefined, + message: computeMessage(stackTrace, isErrorInstance, nonErrorPrefix, originalError), + stack: stackTrace ? toStackTraceString(stackTrace) : useFallbackStack ? NO_ERROR_STACK_PRESENT_MESSAGE : undefined, + } +} + export function computeRawError({ stackTrace, originalError, @@ -32,22 +58,16 @@ export function computeRawError({ source, handling, }: RawErrorParams): RawError { - const isErrorInstance = isError(originalError) - if (!stackTrace && isErrorInstance) { - stackTrace = computeStackTrace(originalError) - } + const errorBase = computeErrorBase({ originalError, stackTrace, source, useFallbackStack, nonErrorPrefix }) return { startClocks, - source, handling, handlingStack, componentStack, originalError, - type: stackTrace ? stackTrace.name : undefined, - message: computeMessage(stackTrace, isErrorInstance, nonErrorPrefix, originalError), - stack: stackTrace ? toStackTraceString(stackTrace) : useFallbackStack ? NO_ERROR_STACK_PRESENT_MESSAGE : undefined, - causes: isErrorInstance ? flattenErrorCauses(originalError as ErrorWithCause, source) : undefined, + ...errorBase, + causes: isError(originalError) ? flattenErrorCauses(originalError as ErrorWithCause, source) : undefined, fingerprint: tryToGetFingerprint(originalError), context: tryToGetErrorContext(originalError), } @@ -56,7 +76,7 @@ export function computeRawError({ function computeMessage( stackTrace: StackTrace | undefined, isErrorInstance: boolean, - nonErrorPrefix: NonErrorPrefix, + nonErrorPrefix: NonErrorPrefix | undefined, originalError: unknown ) { // Favor stackTrace message only if tracekit has really been able to extract something meaningful (message + name) @@ -64,7 +84,9 @@ function computeMessage( return stackTrace?.message && stackTrace?.name ? stackTrace.message : !isErrorInstance - ? `${nonErrorPrefix} ${jsonStringify(sanitize(originalError))!}` + ? nonErrorPrefix + ? `${nonErrorPrefix} ${jsonStringify(sanitize(originalError))!}` + : jsonStringify(sanitize(originalError))! : 'Empty message' } @@ -87,17 +109,20 @@ export function isError(error: unknown): error is Error { } export function flattenErrorCauses(error: ErrorWithCause, parentSource: ErrorSource): RawErrorCause[] | undefined { - let currentError = error const causes: RawErrorCause[] = [] - while (isError(currentError?.cause) && causes.length < 10) { - const stackTrace = computeStackTrace(currentError.cause) - causes.push({ - message: currentError.cause.message, + let currentCause = error.cause + + while (currentCause !== undefined && currentCause !== null && causes.length < 10) { + const causeBase = computeErrorBase({ + originalError: currentCause, source: parentSource, - type: stackTrace?.name, - stack: stackTrace && toStackTraceString(stackTrace), + useFallbackStack: false, }) - currentError = currentError.cause + + causes.push(causeBase) + + currentCause = isError(currentCause) ? (currentCause as ErrorWithCause).cause : undefined } + return causes.length ? causes : undefined } diff --git a/packages/rum-core/src/domain/error/errorCollection.spec.ts b/packages/rum-core/src/domain/error/errorCollection.spec.ts index 2efeb35fda..2a3d203d44 100644 --- a/packages/rum-core/src/domain/error/errorCollection.spec.ts +++ b/packages/rum-core/src/domain/error/errorCollection.spec.ts @@ -126,6 +126,27 @@ describe('error collection', () => { expect(error?.causes?.[1].source).toEqual(ErrorSource.CUSTOM) }) + it('should extract non-Error causes with consistent structure', () => { + setupErrorCollection() + const error = new Error('RSA key generation failed') as ErrorWithCause + error.cause = { code: 'NonInteger', values: [3.14, 2.71] } + + addError({ + error, + handlingStack: 'Error: handling', + startClocks: { relative: 1234 as RelativeTime, timeStamp: 123456789 as TimeStamp }, + }) + + const { error: rumError } = rawRumEvents[0].rawRumEvent as RawRumErrorEvent + expect(rumError.causes?.length).toBe(1) + expect(rumError.causes?.[0]).toEqual({ + message: '{"code":"NonInteger","values":[3.14,2.71]}', + source: ErrorSource.CUSTOM, + type: undefined, + stack: undefined, + }) + }) + it('should extract fingerprint from error', () => { setupErrorCollection()