Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions packages/core/src/exports.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}

/**
Expand Down
17 changes: 14 additions & 3 deletions packages/core/src/integrations/captureconsole.ts
Original file line number Diff line number Diff line change
@@ -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';
Expand Down Expand Up @@ -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: {
Expand All @@ -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;
}
Expand All @@ -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 });
});
}
2 changes: 1 addition & 1 deletion packages/core/src/scope.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
81 changes: 51 additions & 30 deletions packages/core/test/lib/integrations/captureconsole.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,21 +29,21 @@ 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);
});

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);
Expand Down Expand Up @@ -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', () => {
Expand All @@ -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', () => {
Expand All @@ -97,7 +97,7 @@ describe('CaptureConsole setup', () => {
GLOBAL_OBJ.console[key]('msg');
});

expect(captureMessage).toHaveBeenCalledTimes(0);
expect(mockScope.captureMessage).toHaveBeenCalledTimes(0);
});
});

Expand All @@ -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', () => {
Expand All @@ -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),
});
});

Expand All @@ -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),
});
});

Expand All @@ -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"', () => {
Expand Down Expand Up @@ -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),
});
});

Expand All @@ -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();
});
Expand All @@ -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),
});
});

Expand Down Expand Up @@ -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", () => {
Expand Down
Loading