Skip to content

Commit 380f483

Browse files
authored
fix(core): Suppress stack when SentryError isn't an error (#5562)
We use `SentryError`s in our event processing and sending pipeline both when something has gone legitimately wrong and when we just want to bail on a promise chain. In all cases, however, we log both the message and the stack, at a warning log level, which both clutters the logs and unnecessarily alarms anyone who has logging on. This fixes that by adding a `logLevel` property to the `SentryError` class, which can be read before we log to the console. The default behavior remains the same (log the full error, using `logger.warn`), but now we have the option of passing `'log'` as a second constructor parameter, in order to mark a given `SentryError` as something which should only be `logger.log`ged and whose stack should be suppressed.
1 parent 9339a73 commit 380f483

File tree

6 files changed

+34
-15
lines changed

6 files changed

+34
-15
lines changed

packages/core/src/baseclient.ts

+14-4
Original file line numberDiff line numberDiff line change
@@ -583,7 +583,16 @@ export abstract class BaseClient<O extends ClientOptions> implements Client<O> {
583583
return finalEvent.event_id;
584584
},
585585
reason => {
586-
__DEBUG_BUILD__ && logger.warn(reason);
586+
if (__DEBUG_BUILD__) {
587+
// If something's gone wrong, log the error as a warning. If it's just us having used a `SentryError` for
588+
// control flow, log just the message (no stack) as a log-level log.
589+
const sentryError = reason as SentryError;
590+
if (sentryError.logLevel === 'log') {
591+
logger.log(sentryError.message);
592+
} else {
593+
logger.warn(sentryError);
594+
}
595+
}
587596
return undefined;
588597
},
589598
);
@@ -606,7 +615,7 @@ export abstract class BaseClient<O extends ClientOptions> implements Client<O> {
606615
const { beforeSend, sampleRate } = this.getOptions();
607616

608617
if (!this._isEnabled()) {
609-
return rejectedSyncPromise(new SentryError('SDK not enabled, will not capture event.'));
618+
return rejectedSyncPromise(new SentryError('SDK not enabled, will not capture event.', 'log'));
610619
}
611620

612621
const isTransaction = event.type === 'transaction';
@@ -618,6 +627,7 @@ export abstract class BaseClient<O extends ClientOptions> implements Client<O> {
618627
return rejectedSyncPromise(
619628
new SentryError(
620629
`Discarding event because it's not included in the random sample (sampling rate = ${sampleRate})`,
630+
'log',
621631
),
622632
);
623633
}
@@ -626,7 +636,7 @@ export abstract class BaseClient<O extends ClientOptions> implements Client<O> {
626636
.then(prepared => {
627637
if (prepared === null) {
628638
this.recordDroppedEvent('event_processor', event.type || 'error');
629-
throw new SentryError('An event processor returned null, will not send event.');
639+
throw new SentryError('An event processor returned null, will not send event.', 'log');
630640
}
631641

632642
const isInternalException = hint.data && (hint.data as { __sentry__: boolean }).__sentry__ === true;
@@ -640,7 +650,7 @@ export abstract class BaseClient<O extends ClientOptions> implements Client<O> {
640650
.then(processedEvent => {
641651
if (processedEvent === null) {
642652
this.recordDroppedEvent('before_send', event.type || 'error');
643-
throw new SentryError('`beforeSend` returned `null`, will not send event.');
653+
throw new SentryError('`beforeSend` returned `null`, will not send event.', 'log');
644654
}
645655

646656
const session = scope && scope.getSession();

packages/core/test/lib/base.test.ts

+4-4
Original file line numberDiff line numberDiff line change
@@ -920,13 +920,13 @@ describe('BaseClient', () => {
920920
const options = getDefaultTestClientOptions({ dsn: PUBLIC_DSN, beforeSend });
921921
const client = new TestClient(options);
922922
const captureExceptionSpy = jest.spyOn(client, 'captureException');
923-
const loggerWarnSpy = jest.spyOn(logger, 'warn');
923+
const loggerWarnSpy = jest.spyOn(logger, 'log');
924924

925925
client.captureEvent({ message: 'hello' });
926926

927927
expect(TestClient.instance!.event).toBeUndefined();
928928
expect(captureExceptionSpy).not.toBeCalled();
929-
expect(loggerWarnSpy).toBeCalledWith(new SentryError('`beforeSend` returned `null`, will not send event.'));
929+
expect(loggerWarnSpy).toBeCalledWith('`beforeSend` returned `null`, will not send event.');
930930
});
931931

932932
test('calls beforeSend and log info about invalid return value', () => {
@@ -1065,15 +1065,15 @@ describe('BaseClient', () => {
10651065

10661066
const client = new TestClient(getDefaultTestClientOptions({ dsn: PUBLIC_DSN }));
10671067
const captureExceptionSpy = jest.spyOn(client, 'captureException');
1068-
const loggerWarnSpy = jest.spyOn(logger, 'warn');
1068+
const loggerLogSpy = jest.spyOn(logger, 'log');
10691069
const scope = new Scope();
10701070
scope.addEventProcessor(() => null);
10711071

10721072
client.captureEvent({ message: 'hello' }, {}, scope);
10731073

10741074
expect(TestClient.instance!.event).toBeUndefined();
10751075
expect(captureExceptionSpy).not.toBeCalled();
1076-
expect(loggerWarnSpy).toBeCalledWith(new SentryError('An event processor returned null, will not send event.'));
1076+
expect(loggerLogSpy).toBeCalledWith('An event processor returned null, will not send event.');
10771077
});
10781078

10791079
test('eventProcessor records dropped events', () => {

packages/nextjs/test/index.client.test.ts

+3-3
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ import { getCurrentHub } from '@sentry/hub';
33
import * as SentryReact from '@sentry/react';
44
import { Integrations as TracingIntegrations } from '@sentry/tracing';
55
import { Integration } from '@sentry/types';
6-
import { getGlobalObject, logger, SentryError } from '@sentry/utils';
6+
import { getGlobalObject, logger } from '@sentry/utils';
77

88
import { init, Integrations, nextRouterInstrumentation } from '../src/index.client';
99
import { NextjsOptions } from '../src/utils/nextjsOptions';
@@ -14,7 +14,7 @@ const global = getGlobalObject();
1414

1515
const reactInit = jest.spyOn(SentryReact, 'init');
1616
const captureEvent = jest.spyOn(BaseClient.prototype, 'captureEvent');
17-
const logWarn = jest.spyOn(logger, 'warn');
17+
const loggerLogSpy = jest.spyOn(logger, 'log');
1818

1919
describe('Client init()', () => {
2020
afterEach(() => {
@@ -75,7 +75,7 @@ describe('Client init()', () => {
7575

7676
expect(transportSend).not.toHaveBeenCalled();
7777
expect(captureEvent.mock.results[0].value).toBeUndefined();
78-
expect(logWarn).toHaveBeenCalledWith(new SentryError('An event processor returned null, will not send event.'));
78+
expect(loggerLogSpy).toHaveBeenCalledWith('An event processor returned null, will not send event.');
7979
});
8080

8181
describe('integrations', () => {

packages/nextjs/test/index.server.test.ts

+3-3
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import { RewriteFrames } from '@sentry/integrations';
22
import * as SentryNode from '@sentry/node';
33
import { getCurrentHub, NodeClient } from '@sentry/node';
44
import { Integration } from '@sentry/types';
5-
import { getGlobalObject, logger, SentryError } from '@sentry/utils';
5+
import { getGlobalObject, logger } from '@sentry/utils';
66
import * as domain from 'domain';
77

88
import { init } from '../src/index.server';
@@ -16,7 +16,7 @@ const global = getGlobalObject();
1616
(global as typeof global & { __rewriteFramesDistDir__: string }).__rewriteFramesDistDir__ = '.next';
1717

1818
const nodeInit = jest.spyOn(SentryNode, 'init');
19-
const logWarn = jest.spyOn(logger, 'warn');
19+
const loggerLogSpy = jest.spyOn(logger, 'log');
2020

2121
describe('Server init()', () => {
2222
afterEach(() => {
@@ -104,7 +104,7 @@ describe('Server init()', () => {
104104
await SentryNode.flush();
105105

106106
expect(transportSend).not.toHaveBeenCalled();
107-
expect(logWarn).toHaveBeenCalledWith(new SentryError('An event processor returned null, will not send event.'));
107+
expect(loggerLogSpy).toHaveBeenCalledWith('An event processor returned null, will not send event.');
108108
});
109109

110110
it("initializes both global hub and domain hub when there's an active domain", () => {

packages/utils/src/error.ts

+9-1
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,20 @@
1+
import type { ConsoleLevel } from './logger';
2+
13
/** An error emitted by Sentry SDKs and related utilities. */
24
export class SentryError extends Error {
35
/** Display name of this error instance. */
46
public name: string;
57

6-
public constructor(public message: string) {
8+
public logLevel: ConsoleLevel;
9+
10+
public constructor(public message: string, logLevel: ConsoleLevel = 'warn') {
711
super(message);
812

913
this.name = new.target.prototype.constructor.name;
14+
// This sets the prototype to be `Error`, not `SentryError`. It's unclear why we do this, but commenting this line
15+
// out causes various (seemingly totally unrelated) playwright tests consistently time out. FYI, this makes
16+
// instances of `SentryError` fail `obj instanceof SentryError` checks.
1017
Object.setPrototypeOf(this, new.target.prototype);
18+
this.logLevel = logLevel;
1119
}
1220
}

packages/utils/src/logger.ts

+1
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ const global = getGlobalObject<Window | NodeJS.Global>();
99
const PREFIX = 'Sentry Logger ';
1010

1111
export const CONSOLE_LEVELS = ['debug', 'info', 'warn', 'error', 'log', 'assert', 'trace'] as const;
12+
export type ConsoleLevel = typeof CONSOLE_LEVELS[number];
1213

1314
type LoggerMethod = (...args: unknown[]) => void;
1415
type LoggerConsoleMethods = Record<typeof CONSOLE_LEVELS[number], LoggerMethod>;

0 commit comments

Comments
 (0)