diff --git a/packages/core/src/utils/eventbuilder.ts b/packages/core/src/utils/eventbuilder.ts index 9b300c59c593..2e8445ed5878 100644 --- a/packages/core/src/utils/eventbuilder.ts +++ b/packages/core/src/utils/eventbuilder.ts @@ -7,10 +7,11 @@ import type { ParameterizedString } from '../types/parameterize'; import type { SeverityLevel } from '../types/severity'; import type { StackFrame } from '../types/stackframe'; import type { StackParser } from '../types/stacktrace'; -import { isError, isErrorEvent, isParameterizedString, isPlainObject } from './is'; +import { isError, isErrorEvent, isParameterizedString, isPlainObject, isPrimitive } from './is'; import { addExceptionMechanism, addExceptionTypeValue } from './misc'; -import { normalizeToSize } from './normalize'; +import { normalizeToSize, stringifyValue } from './normalize'; import { extractExceptionKeysForMessage } from './object'; +import { stringify } from './string'; /** * Extracts stack frames from the error.stack string @@ -32,13 +33,40 @@ function hasSentryFetchUrlHost(error: unknown): error is Error & { __sentry_fetc * @hidden */ export function _enhanceErrorWithSentryInfo(error: T): string { + const message = errorMessageToString(error.message); + // If the error has a __sentry_fetch_url_host__ property (added by fetch instrumentation), // enhance the error message with the hostname. if (hasSentryFetchUrlHost(error)) { - return `${error.message} (${error.__sentry_fetch_url_host__})`; + return `${message} (${error.__sentry_fetch_url_host__})`; + } + + return message; +} + +// `message` is typed as a string, but user code can assign anything to it, and event processors +// call string methods on `exception.value`. +function errorMessageToString(message: unknown): string { + if (typeof message === 'string') { + return message; + } + + if (message == null) { + return ''; + } + + if (isPrimitive(message)) { + return String(message); + } + + if (isPlainObject(message) || Array.isArray(message)) { + const json = stringify(message, ''); + if (json) { + return json; + } } - return error.message; + return stringifyValue(undefined, message); } /** diff --git a/packages/core/test/lib/utils/eventbuilder.test.ts b/packages/core/test/lib/utils/eventbuilder.test.ts index 2a08f073117e..5a2fcc003419 100644 --- a/packages/core/test/lib/utils/eventbuilder.test.ts +++ b/packages/core/test/lib/utils/eventbuilder.test.ts @@ -308,5 +308,46 @@ describe('eventFromMessage', () => { const exception = exceptionFromError(stackParser, error); expect(exception.value).toBe('Failed to fetch (localhost:8080)'); }); + + it('should enhance a non-string message', () => { + const error = new TypeError('placeholder'); + (error as { message: unknown }).message = { code: 42 }; + addNonEnumerableProperty(error, '__sentry_fetch_url_host__', 'example.com'); + + const exception = exceptionFromError(stackParser, error); + expect(exception.value).toBe('{"code":42} (example.com)'); + }); + }); +}); + +describe('exceptionFromError', () => { + const circular: Record = { code: 42 }; + circular.self = circular; + + it.each([ + ['a number', 42, '42'], + ['a bigint', BigInt(42), '42'], + ['a boolean', false, 'false'], + ['undefined', undefined, ''], + ['null', null, ''], + ['an object', { code: 42 }, '{"code":42}'], + ['an array', [1, 'two'], '[1,"two"]'], + ['a circular object', circular, '[object Object]'], + ['a function', function namedFn() {}, '[Function: namedFn]'], + ['an error', new Error('inner'), '[object Error]'], + ['a map', new Map([['code', 42]]), '[object Map]'], + [ + 'a class instance', + new (class CustomMessage { + code = 42; + })(), + '{"code":42}', + ], + ])('turns %s into a string', (_, message, expected) => { + const error = new Error('placeholder'); + (error as { message: unknown }).message = message; + + const exception = exceptionFromError(stackParser, error); + expect(exception.value).toBe(expected); }); });