Skip to content

Commit 9d99c99

Browse files
authored
fix(core): Decrease number of Sentry stack frames for messages from captureConsoleIntegration (#18096)
This patch creates a synthetic exception already within the captureConsole handler, so that we minimize the number of Sentry stack frames in the stack trace. It also adjusts the `Client::captureMessage` method to favor an already provided `syntheticException` over the one it would create by itself.
1 parent 8267561 commit 9d99c99

File tree

4 files changed

+68
-36
lines changed

4 files changed

+68
-36
lines changed

packages/core/src/exports.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -40,8 +40,8 @@ export function captureMessage(message: string, captureContext?: CaptureContext
4040
// This is necessary to provide explicit scopes upgrade, without changing the original
4141
// arity of the `captureMessage(message, level)` method.
4242
const level = typeof captureContext === 'string' ? captureContext : undefined;
43-
const context = typeof captureContext !== 'string' ? { captureContext } : undefined;
44-
return getCurrentScope().captureMessage(message, level, context);
43+
const hint = typeof captureContext !== 'string' ? { captureContext } : undefined;
44+
return getCurrentScope().captureMessage(message, level, hint);
4545
}
4646

4747
/**

packages/core/src/integrations/captureconsole.ts

Lines changed: 14 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { getClient, withScope } from '../currentScopes';
2-
import { captureException, captureMessage } from '../exports';
2+
import { captureException } from '../exports';
33
import { addConsoleInstrumentationHandler } from '../instrument/console';
44
import { defineIntegration } from '../integration';
55
import type { CaptureContext } from '../scope';
@@ -52,6 +52,17 @@ const _captureConsoleIntegration = ((options: CaptureConsoleOptions = {}) => {
5252
export const captureConsoleIntegration = defineIntegration(_captureConsoleIntegration);
5353

5454
function consoleHandler(args: unknown[], level: string, handled: boolean): void {
55+
const severityLevel = severityLevelFromString(level);
56+
57+
/*
58+
We create this error here already to attach a stack trace to captured messages,
59+
if users set `attachStackTrace` to `true` in Sentry.init.
60+
We do this here already because we want to minimize the number of Sentry SDK stack frames
61+
within the error. Technically, Client.captureMessage will also do it but this happens several
62+
stack frames deeper.
63+
*/
64+
const syntheticException = new Error();
65+
5566
const captureContext: CaptureContext = {
5667
level: severityLevelFromString(level),
5768
extra: {
@@ -75,7 +86,7 @@ function consoleHandler(args: unknown[], level: string, handled: boolean): void
7586
if (!args[0]) {
7687
const message = `Assertion failed: ${safeJoin(args.slice(1), ' ') || 'console.assert'}`;
7788
scope.setExtra('arguments', args.slice(1));
78-
captureMessage(message, captureContext);
89+
scope.captureMessage(message, severityLevel, { captureContext, syntheticException });
7990
}
8091
return;
8192
}
@@ -87,6 +98,6 @@ function consoleHandler(args: unknown[], level: string, handled: boolean): void
8798
}
8899

89100
const message = safeJoin(args, ' ');
90-
captureMessage(message, captureContext);
101+
scope.captureMessage(message, severityLevel, { captureContext, syntheticException });
91102
});
92103
}

packages/core/src/scope.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -607,7 +607,7 @@ export class Scope {
607607
return eventId;
608608
}
609609

610-
const syntheticException = new Error(message);
610+
const syntheticException = hint?.syntheticException ?? new Error(message);
611611

612612
this._client.captureMessage(
613613
message,

packages/core/test/lib/integrations/captureconsole.test.ts

Lines changed: 51 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -29,21 +29,21 @@ describe('CaptureConsole setup', () => {
2929

3030
let mockClient: Client;
3131

32+
const captureException = vi.fn();
33+
3234
const mockScope = {
3335
setExtra: vi.fn(),
3436
addEventProcessor: vi.fn(),
37+
captureMessage: vi.fn(),
3538
};
3639

37-
const captureMessage = vi.fn();
38-
const captureException = vi.fn();
3940
const withScope = vi.fn(callback => {
4041
return callback(mockScope);
4142
});
4243

4344
beforeEach(() => {
4445
mockClient = {} as Client;
4546

46-
vi.spyOn(SentryCore, 'captureMessage').mockImplementation(captureMessage);
4747
vi.spyOn(SentryCore, 'captureException').mockImplementation(captureException);
4848
vi.spyOn(CurrentScopes, 'getClient').mockImplementation(() => mockClient);
4949
vi.spyOn(CurrentScopes, 'withScope').mockImplementation(withScope);
@@ -72,7 +72,7 @@ describe('CaptureConsole setup', () => {
7272
GLOBAL_OBJ.console.log('msg 2');
7373
GLOBAL_OBJ.console.warn('msg 3');
7474

75-
expect(captureMessage).toHaveBeenCalledTimes(2);
75+
expect(mockScope.captureMessage).toHaveBeenCalledTimes(2);
7676
});
7777

7878
it('should fall back to default console levels if none are provided', () => {
@@ -86,7 +86,7 @@ describe('CaptureConsole setup', () => {
8686

8787
GLOBAL_OBJ.console.assert(false);
8888

89-
expect(captureMessage).toHaveBeenCalledTimes(7);
89+
expect(mockScope.captureMessage).toHaveBeenCalledTimes(7);
9090
});
9191

9292
it('should not wrap any functions with an empty levels option', () => {
@@ -97,7 +97,7 @@ describe('CaptureConsole setup', () => {
9797
GLOBAL_OBJ.console[key]('msg');
9898
});
9999

100-
expect(captureMessage).toHaveBeenCalledTimes(0);
100+
expect(mockScope.captureMessage).toHaveBeenCalledTimes(0);
101101
});
102102
});
103103

@@ -121,8 +121,14 @@ describe('CaptureConsole setup', () => {
121121

122122
GLOBAL_OBJ.console.log();
123123

124-
expect(captureMessage).toHaveBeenCalledTimes(1);
125-
expect(captureMessage).toHaveBeenCalledWith('', { extra: { arguments: [] }, level: 'log' });
124+
expect(mockScope.captureMessage).toHaveBeenCalledTimes(1);
125+
expect(mockScope.captureMessage).toHaveBeenCalledWith('', 'log', {
126+
captureContext: {
127+
level: 'log',
128+
extra: { arguments: [] },
129+
},
130+
syntheticException: expect.any(Error),
131+
});
126132
});
127133

128134
it('should add an event processor that sets the `debug` field of events', () => {
@@ -148,10 +154,13 @@ describe('CaptureConsole setup', () => {
148154
GLOBAL_OBJ.console.assert(1 + 1 === 3);
149155

150156
expect(mockScope.setExtra).toHaveBeenLastCalledWith('arguments', []);
151-
expect(captureMessage).toHaveBeenCalledTimes(1);
152-
expect(captureMessage).toHaveBeenCalledWith('Assertion failed: console.assert', {
153-
extra: { arguments: [false] },
154-
level: 'log',
157+
expect(mockScope.captureMessage).toHaveBeenCalledTimes(1);
158+
expect(mockScope.captureMessage).toHaveBeenCalledWith('Assertion failed: console.assert', 'log', {
159+
captureContext: {
160+
level: 'log',
161+
extra: { arguments: [false] },
162+
},
163+
syntheticException: expect.any(Error),
155164
});
156165
});
157166

@@ -162,10 +171,13 @@ describe('CaptureConsole setup', () => {
162171
GLOBAL_OBJ.console.assert(1 + 1 === 3, 'expression is false');
163172

164173
expect(mockScope.setExtra).toHaveBeenLastCalledWith('arguments', ['expression is false']);
165-
expect(captureMessage).toHaveBeenCalledTimes(1);
166-
expect(captureMessage).toHaveBeenCalledWith('Assertion failed: expression is false', {
167-
extra: { arguments: [false, 'expression is false'] },
168-
level: 'log',
174+
expect(mockScope.captureMessage).toHaveBeenCalledTimes(1);
175+
expect(mockScope.captureMessage).toHaveBeenCalledWith('Assertion failed: expression is false', 'log', {
176+
captureContext: {
177+
level: 'log',
178+
extra: { arguments: [false, 'expression is false'] },
179+
},
180+
syntheticException: expect.any(Error),
169181
});
170182
});
171183

@@ -175,7 +187,7 @@ describe('CaptureConsole setup', () => {
175187

176188
GLOBAL_OBJ.console.assert(1 + 1 === 2);
177189

178-
expect(captureMessage).toHaveBeenCalledTimes(0);
190+
expect(mockScope.captureMessage).toHaveBeenCalledTimes(0);
179191
});
180192

181193
it('should capture exception when console logs an error object with level set to "error"', () => {
@@ -226,10 +238,13 @@ describe('CaptureConsole setup', () => {
226238

227239
GLOBAL_OBJ.console.error('some message');
228240

229-
expect(captureMessage).toHaveBeenCalledTimes(1);
230-
expect(captureMessage).toHaveBeenCalledWith('some message', {
231-
extra: { arguments: ['some message'] },
232-
level: 'error',
241+
expect(mockScope.captureMessage).toHaveBeenCalledTimes(1);
242+
expect(mockScope.captureMessage).toHaveBeenCalledWith('some message', 'error', {
243+
captureContext: {
244+
level: 'error',
245+
extra: { arguments: ['some message'] },
246+
},
247+
syntheticException: expect.any(Error),
233248
});
234249
});
235250

@@ -239,10 +254,13 @@ describe('CaptureConsole setup', () => {
239254

240255
GLOBAL_OBJ.console.error('some non-error message');
241256

242-
expect(captureMessage).toHaveBeenCalledTimes(1);
243-
expect(captureMessage).toHaveBeenCalledWith('some non-error message', {
244-
extra: { arguments: ['some non-error message'] },
245-
level: 'error',
257+
expect(mockScope.captureMessage).toHaveBeenCalledTimes(1);
258+
expect(mockScope.captureMessage).toHaveBeenCalledWith('some non-error message', 'error', {
259+
captureContext: {
260+
level: 'error',
261+
extra: { arguments: ['some non-error message'] },
262+
},
263+
syntheticException: expect.any(Error),
246264
});
247265
expect(captureException).not.toHaveBeenCalled();
248266
});
@@ -253,10 +271,13 @@ describe('CaptureConsole setup', () => {
253271

254272
GLOBAL_OBJ.console.info('some message');
255273

256-
expect(captureMessage).toHaveBeenCalledTimes(1);
257-
expect(captureMessage).toHaveBeenCalledWith('some message', {
258-
extra: { arguments: ['some message'] },
259-
level: 'info',
274+
expect(mockScope.captureMessage).toHaveBeenCalledTimes(1);
275+
expect(mockScope.captureMessage).toHaveBeenCalledWith('some message', 'info', {
276+
captureContext: {
277+
level: 'info',
278+
extra: { arguments: ['some message'] },
279+
},
280+
syntheticException: expect.any(Error),
260281
});
261282
});
262283

@@ -293,7 +314,7 @@ describe('CaptureConsole setup', () => {
293314

294315
// Should not capture messages
295316
GLOBAL_OBJ.console.log('some message');
296-
expect(captureMessage).not.toHaveBeenCalledWith();
317+
expect(mockScope.captureMessage).not.toHaveBeenCalledWith();
297318
});
298319

299320
it("should not crash when the original console methods don't exist at time of invocation", () => {

0 commit comments

Comments
 (0)