diff --git a/packages/core/src/exports.ts b/packages/core/src/exports.ts index a5dc716d8124..e3f658c88f2a 100644 --- a/packages/core/src/exports.ts +++ b/packages/core/src/exports.ts @@ -40,8 +40,8 @@ export function captureMessage(message: string, captureContext?: CaptureContext // This is necessary to provide explicit scopes upgrade, without changing the original // arity of the `captureMessage(message, level)` method. const level = typeof captureContext === 'string' ? captureContext : undefined; - const context = typeof captureContext !== 'string' ? { captureContext } : undefined; - return getCurrentScope().captureMessage(message, level, context); + const hint = typeof captureContext !== 'string' ? { captureContext } : undefined; + return getCurrentScope().captureMessage(message, level, hint); } /** diff --git a/packages/core/src/integrations/captureconsole.ts b/packages/core/src/integrations/captureconsole.ts index d5d34bd554aa..4c28e2c74a54 100644 --- a/packages/core/src/integrations/captureconsole.ts +++ b/packages/core/src/integrations/captureconsole.ts @@ -1,5 +1,5 @@ import { getClient, withScope } from '../currentScopes'; -import { captureException, captureMessage } from '../exports'; +import { captureException } from '../exports'; import { addConsoleInstrumentationHandler } from '../instrument/console'; import { defineIntegration } from '../integration'; import type { CaptureContext } from '../scope'; @@ -52,6 +52,17 @@ const _captureConsoleIntegration = ((options: CaptureConsoleOptions = {}) => { export const captureConsoleIntegration = defineIntegration(_captureConsoleIntegration); function consoleHandler(args: unknown[], level: string, handled: boolean): void { + const severityLevel = severityLevelFromString(level); + + /* + We create this error here already to attach a stack trace to captured messages, + if users set `attachStackTrace` to `true` in Sentry.init. + We do this here already because we want to minimize the number of Sentry SDK stack frames + within the error. Technically, Client.captureMessage will also do it but this happens several + stack frames deeper. + */ + const syntheticException = new Error(); + const captureContext: CaptureContext = { level: severityLevelFromString(level), extra: { @@ -75,7 +86,7 @@ function consoleHandler(args: unknown[], level: string, handled: boolean): void if (!args[0]) { const message = `Assertion failed: ${safeJoin(args.slice(1), ' ') || 'console.assert'}`; scope.setExtra('arguments', args.slice(1)); - captureMessage(message, captureContext); + scope.captureMessage(message, severityLevel, { captureContext, syntheticException }); } return; } @@ -87,6 +98,6 @@ function consoleHandler(args: unknown[], level: string, handled: boolean): void } const message = safeJoin(args, ' '); - captureMessage(message, captureContext); + scope.captureMessage(message, severityLevel, { captureContext, syntheticException }); }); } diff --git a/packages/core/src/scope.ts b/packages/core/src/scope.ts index 8b1e21acfb4a..3287d8efbbbd 100644 --- a/packages/core/src/scope.ts +++ b/packages/core/src/scope.ts @@ -607,7 +607,7 @@ export class Scope { return eventId; } - const syntheticException = new Error(message); + const syntheticException = hint?.syntheticException ?? new Error(message); this._client.captureMessage( message, diff --git a/packages/core/test/lib/integrations/captureconsole.test.ts b/packages/core/test/lib/integrations/captureconsole.test.ts index 2ca059be4f86..a7e14f6536c3 100644 --- a/packages/core/test/lib/integrations/captureconsole.test.ts +++ b/packages/core/test/lib/integrations/captureconsole.test.ts @@ -29,13 +29,14 @@ describe('CaptureConsole setup', () => { let mockClient: Client; + const captureException = vi.fn(); + const mockScope = { setExtra: vi.fn(), addEventProcessor: vi.fn(), + captureMessage: vi.fn(), }; - const captureMessage = vi.fn(); - const captureException = vi.fn(); const withScope = vi.fn(callback => { return callback(mockScope); }); @@ -43,7 +44,6 @@ describe('CaptureConsole setup', () => { beforeEach(() => { mockClient = {} as Client; - vi.spyOn(SentryCore, 'captureMessage').mockImplementation(captureMessage); vi.spyOn(SentryCore, 'captureException').mockImplementation(captureException); vi.spyOn(CurrentScopes, 'getClient').mockImplementation(() => mockClient); vi.spyOn(CurrentScopes, 'withScope').mockImplementation(withScope); @@ -72,7 +72,7 @@ describe('CaptureConsole setup', () => { GLOBAL_OBJ.console.log('msg 2'); GLOBAL_OBJ.console.warn('msg 3'); - expect(captureMessage).toHaveBeenCalledTimes(2); + expect(mockScope.captureMessage).toHaveBeenCalledTimes(2); }); it('should fall back to default console levels if none are provided', () => { @@ -86,7 +86,7 @@ describe('CaptureConsole setup', () => { GLOBAL_OBJ.console.assert(false); - expect(captureMessage).toHaveBeenCalledTimes(7); + expect(mockScope.captureMessage).toHaveBeenCalledTimes(7); }); it('should not wrap any functions with an empty levels option', () => { @@ -97,7 +97,7 @@ describe('CaptureConsole setup', () => { GLOBAL_OBJ.console[key]('msg'); }); - expect(captureMessage).toHaveBeenCalledTimes(0); + expect(mockScope.captureMessage).toHaveBeenCalledTimes(0); }); }); @@ -121,8 +121,14 @@ describe('CaptureConsole setup', () => { GLOBAL_OBJ.console.log(); - expect(captureMessage).toHaveBeenCalledTimes(1); - expect(captureMessage).toHaveBeenCalledWith('', { extra: { arguments: [] }, level: 'log' }); + expect(mockScope.captureMessage).toHaveBeenCalledTimes(1); + expect(mockScope.captureMessage).toHaveBeenCalledWith('', 'log', { + captureContext: { + level: 'log', + extra: { arguments: [] }, + }, + syntheticException: expect.any(Error), + }); }); it('should add an event processor that sets the `debug` field of events', () => { @@ -148,10 +154,13 @@ describe('CaptureConsole setup', () => { GLOBAL_OBJ.console.assert(1 + 1 === 3); expect(mockScope.setExtra).toHaveBeenLastCalledWith('arguments', []); - expect(captureMessage).toHaveBeenCalledTimes(1); - expect(captureMessage).toHaveBeenCalledWith('Assertion failed: console.assert', { - extra: { arguments: [false] }, - level: 'log', + expect(mockScope.captureMessage).toHaveBeenCalledTimes(1); + expect(mockScope.captureMessage).toHaveBeenCalledWith('Assertion failed: console.assert', 'log', { + captureContext: { + level: 'log', + extra: { arguments: [false] }, + }, + syntheticException: expect.any(Error), }); }); @@ -162,10 +171,13 @@ describe('CaptureConsole setup', () => { GLOBAL_OBJ.console.assert(1 + 1 === 3, 'expression is false'); expect(mockScope.setExtra).toHaveBeenLastCalledWith('arguments', ['expression is false']); - expect(captureMessage).toHaveBeenCalledTimes(1); - expect(captureMessage).toHaveBeenCalledWith('Assertion failed: expression is false', { - extra: { arguments: [false, 'expression is false'] }, - level: 'log', + expect(mockScope.captureMessage).toHaveBeenCalledTimes(1); + expect(mockScope.captureMessage).toHaveBeenCalledWith('Assertion failed: expression is false', 'log', { + captureContext: { + level: 'log', + extra: { arguments: [false, 'expression is false'] }, + }, + syntheticException: expect.any(Error), }); }); @@ -175,7 +187,7 @@ describe('CaptureConsole setup', () => { GLOBAL_OBJ.console.assert(1 + 1 === 2); - expect(captureMessage).toHaveBeenCalledTimes(0); + expect(mockScope.captureMessage).toHaveBeenCalledTimes(0); }); it('should capture exception when console logs an error object with level set to "error"', () => { @@ -226,10 +238,13 @@ describe('CaptureConsole setup', () => { GLOBAL_OBJ.console.error('some message'); - expect(captureMessage).toHaveBeenCalledTimes(1); - expect(captureMessage).toHaveBeenCalledWith('some message', { - extra: { arguments: ['some message'] }, - level: 'error', + expect(mockScope.captureMessage).toHaveBeenCalledTimes(1); + expect(mockScope.captureMessage).toHaveBeenCalledWith('some message', 'error', { + captureContext: { + level: 'error', + extra: { arguments: ['some message'] }, + }, + syntheticException: expect.any(Error), }); }); @@ -239,10 +254,13 @@ describe('CaptureConsole setup', () => { GLOBAL_OBJ.console.error('some non-error message'); - expect(captureMessage).toHaveBeenCalledTimes(1); - expect(captureMessage).toHaveBeenCalledWith('some non-error message', { - extra: { arguments: ['some non-error message'] }, - level: 'error', + expect(mockScope.captureMessage).toHaveBeenCalledTimes(1); + expect(mockScope.captureMessage).toHaveBeenCalledWith('some non-error message', 'error', { + captureContext: { + level: 'error', + extra: { arguments: ['some non-error message'] }, + }, + syntheticException: expect.any(Error), }); expect(captureException).not.toHaveBeenCalled(); }); @@ -253,10 +271,13 @@ describe('CaptureConsole setup', () => { GLOBAL_OBJ.console.info('some message'); - expect(captureMessage).toHaveBeenCalledTimes(1); - expect(captureMessage).toHaveBeenCalledWith('some message', { - extra: { arguments: ['some message'] }, - level: 'info', + expect(mockScope.captureMessage).toHaveBeenCalledTimes(1); + expect(mockScope.captureMessage).toHaveBeenCalledWith('some message', 'info', { + captureContext: { + level: 'info', + extra: { arguments: ['some message'] }, + }, + syntheticException: expect.any(Error), }); }); @@ -293,7 +314,7 @@ describe('CaptureConsole setup', () => { // Should not capture messages GLOBAL_OBJ.console.log('some message'); - expect(captureMessage).not.toHaveBeenCalledWith(); + expect(mockScope.captureMessage).not.toHaveBeenCalledWith(); }); it("should not crash when the original console methods don't exist at time of invocation", () => {