Skip to content

Commit c82b4e8

Browse files
committed
ref(core): Rewrite logger to avoid side effects
1 parent 7164cd6 commit c82b4e8

File tree

4 files changed

+165
-46
lines changed

4 files changed

+165
-46
lines changed

packages/core/src/carrier.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,9 @@ export interface SentryCarrier {
2424
globalScope?: Scope;
2525
defaultIsolationScope?: Scope;
2626
defaultCurrentScope?: Scope;
27+
/** @deprecated Logger is no longer set. Instead, we keep enabled state in loggerSettings. */
2728
logger?: Logger;
29+
loggerSettings?: { enabled: boolean };
2830

2931
/** Overwrites TextEncoder used in `@sentry/core`, need for `[email protected]` and older */
3032
encodePolyfill?: (input: string) => Uint8Array;

packages/core/src/utils/logger.ts

Lines changed: 106 additions & 46 deletions
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,19 @@ import { DEBUG_BUILD } from '../debug-build';
33
import type { ConsoleLevel } from '../types-hoist/instrument';
44
import { GLOBAL_OBJ } from './worldwide';
55

6-
/** Prefix for logging strings */
7-
const PREFIX = 'Sentry Logger ';
6+
/** A Sentry Logger instance. */
7+
export interface Logger {
8+
disable(): void;
9+
enable(): void;
10+
isEnabled(): boolean;
11+
log(...args: Parameters<typeof console.log>): void;
12+
info(...args: Parameters<typeof console.info>): void;
13+
warn(...args: Parameters<typeof console.warn>): void;
14+
error(...args: Parameters<typeof console.error>): void;
15+
debug(...args: Parameters<typeof console.debug>): void;
16+
assert(...args: Parameters<typeof console.assert>): void;
17+
trace(...args: Parameters<typeof console.trace>): void;
18+
}
819

920
export const CONSOLE_LEVELS: readonly ConsoleLevel[] = [
1021
'debug',
@@ -16,20 +27,19 @@ export const CONSOLE_LEVELS: readonly ConsoleLevel[] = [
1627
'trace',
1728
] as const;
1829

19-
type LoggerMethod = (...args: unknown[]) => void;
20-
type LoggerConsoleMethods = Record<ConsoleLevel, LoggerMethod>;
30+
/** Prefix for logging strings */
31+
const PREFIX = 'Sentry Logger ';
2132

2233
/** This may be mutated by the console instrumentation. */
23-
export const originalConsoleMethods: {
24-
[key in ConsoleLevel]?: (...args: unknown[]) => void;
25-
} = {};
26-
27-
/** A Sentry Logger instance. */
28-
export interface Logger extends LoggerConsoleMethods {
29-
disable(): void;
30-
enable(): void;
31-
isEnabled(): boolean;
32-
}
34+
export const originalConsoleMethods: Partial<{
35+
log(...args: Parameters<typeof console.log>): void;
36+
info(...args: Parameters<typeof console.info>): void;
37+
warn(...args: Parameters<typeof console.warn>): void;
38+
error(...args: Parameters<typeof console.error>): void;
39+
debug(...args: Parameters<typeof console.debug>): void;
40+
assert(...args: Parameters<typeof console.assert>): void;
41+
trace(...args: Parameters<typeof console.trace>): void;
42+
}> = {};
3343

3444
/**
3545
* Temporarily disable sentry console instrumentations.
@@ -43,60 +53,110 @@ export function consoleSandbox<T>(callback: () => T): T {
4353
}
4454

4555
const console = GLOBAL_OBJ.console as Console;
46-
const wrappedFuncs: Partial<LoggerConsoleMethods> = {};
56+
const wrappedFuncs: Partial<Record<ConsoleLevel, (...args: unknown[]) => void>> = {};
4757

4858
const wrappedLevels = Object.keys(originalConsoleMethods) as ConsoleLevel[];
4959

5060
// Restore all wrapped console methods
5161
wrappedLevels.forEach(level => {
52-
const originalConsoleMethod = originalConsoleMethods[level] as LoggerMethod;
53-
wrappedFuncs[level] = console[level] as LoggerMethod | undefined;
54-
console[level] = originalConsoleMethod;
62+
const originalConsoleMethod = originalConsoleMethods[level];
63+
wrappedFuncs[level] = console[level] as (...args: unknown[]) => void;
64+
console[level] = originalConsoleMethod as (...args: unknown[]) => void;
5565
});
5666

5767
try {
5868
return callback();
5969
} finally {
6070
// Revert restoration to wrapped state
6171
wrappedLevels.forEach(level => {
62-
console[level] = wrappedFuncs[level] as LoggerMethod;
72+
console[level] = wrappedFuncs[level] as (...args: unknown[]) => void;
6373
});
6474
}
6575
}
6676

67-
function makeLogger(): Logger {
68-
let enabled = false;
69-
const logger: Partial<Logger> = {
70-
enable: () => {
71-
enabled = true;
72-
},
73-
disable: () => {
74-
enabled = false;
75-
},
76-
isEnabled: () => enabled,
77-
};
78-
79-
if (DEBUG_BUILD) {
80-
CONSOLE_LEVELS.forEach(name => {
81-
logger[name] = (...args: Parameters<(typeof GLOBAL_OBJ.console)[typeof name]>) => {
82-
if (enabled) {
83-
consoleSandbox(() => {
84-
GLOBAL_OBJ.console[name](`${PREFIX}[${name}]:`, ...args);
85-
});
86-
}
87-
};
88-
});
89-
} else {
90-
CONSOLE_LEVELS.forEach(name => {
91-
logger[name] = () => undefined;
77+
function enable(): void {
78+
_getLoggerSettings().enabled = true;
79+
}
80+
81+
function disable(): void {
82+
_getLoggerSettings().enabled = false;
83+
}
84+
85+
function isEnabled(): boolean {
86+
return _getLoggerSettings().enabled;
87+
}
88+
89+
function log(...args: Parameters<typeof console.log>): void {
90+
_maybeLog('log', ...args);
91+
}
92+
93+
function info(...args: Parameters<typeof console.info>): void {
94+
_maybeLog('info', ...args);
95+
}
96+
97+
function warn(...args: Parameters<typeof console.warn>): void {
98+
_maybeLog('warn', ...args);
99+
}
100+
101+
function error(...args: Parameters<typeof console.error>): void {
102+
_maybeLog('error', ...args);
103+
}
104+
105+
function debug(...args: Parameters<typeof console.debug>): void {
106+
_maybeLog('debug', ...args);
107+
}
108+
109+
function assert(...args: Parameters<typeof console.assert>): void {
110+
_maybeLog('assert', ...args);
111+
}
112+
113+
function trace(...args: Parameters<typeof console.trace>): void {
114+
_maybeLog('trace', ...args);
115+
}
116+
117+
function _maybeLog(level: ConsoleLevel, ...args: Parameters<(typeof console)[typeof level]>): void {
118+
if (!DEBUG_BUILD) {
119+
return;
120+
}
121+
122+
if (isEnabled()) {
123+
consoleSandbox(() => {
124+
GLOBAL_OBJ.console[level](`${PREFIX}[${level}]:`, ...args);
92125
});
93126
}
127+
}
128+
129+
function _getLoggerSettings(): { enabled: boolean } {
130+
if (!DEBUG_BUILD) {
131+
return { enabled: false };
132+
}
94133

95-
return logger as Logger;
134+
return getGlobalSingleton('loggerSettings', () => ({ enabled: false }));
96135
}
97136

98137
/**
99138
* This is a logger singleton which either logs things or no-ops if logging is not enabled.
100139
* The logger is a singleton on the carrier, to ensure that a consistent logger is used throughout the SDK.
101140
*/
102-
export const logger = getGlobalSingleton('logger', makeLogger);
141+
export const logger = {
142+
/** Enable logging. */
143+
enable,
144+
/** Disable logging. */
145+
disable,
146+
/** Check if logging is enabled. */
147+
isEnabled,
148+
/** Log a message. */
149+
log,
150+
/** Log level info */
151+
info,
152+
/** Log a warning. */
153+
warn,
154+
/** Log an error. */
155+
error,
156+
/** Log a debug message. */
157+
debug,
158+
/** Log an assertion. */
159+
assert,
160+
/** Log a trace. */
161+
trace,
162+
} satisfies Logger;

packages/core/test/clear-global-scope.ts

Whitespace-only changes.
Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
import { beforeEach, describe, expect, it, vi } from 'vitest';
2+
import { logger } from '../../../src';
3+
import { getMainCarrier, getSentryCarrier } from '../../../src/carrier';
4+
5+
describe('logger', () => {
6+
beforeEach(() => {
7+
vi.clearAllMocks();
8+
getSentryCarrier(getMainCarrier()).loggerSettings = undefined;
9+
});
10+
11+
it('works with defaults', () => {
12+
const consoleLogSpy = vi.spyOn(console, 'log').mockImplementation(() => {});
13+
logger.log('test');
14+
expect(consoleLogSpy).not.toHaveBeenCalled();
15+
expect(logger.isEnabled()).toBe(false);
16+
});
17+
18+
it('allows to enable and disable logging', () => {
19+
const consoleLogSpy = vi.spyOn(console, 'log').mockImplementation(() => {});
20+
21+
logger.log('test');
22+
expect(logger.isEnabled()).toBe(false);
23+
expect(consoleLogSpy).not.toHaveBeenCalled();
24+
25+
logger.enable();
26+
logger.log('test');
27+
expect(logger.isEnabled()).toBe(true);
28+
expect(consoleLogSpy).toHaveBeenCalledTimes(1);
29+
30+
logger.log('test2');
31+
expect(consoleLogSpy).toHaveBeenCalledTimes(2);
32+
33+
logger.disable();
34+
35+
logger.log('test3');
36+
expect(logger.isEnabled()).toBe(false);
37+
expect(consoleLogSpy).toHaveBeenCalledTimes(2);
38+
});
39+
40+
it('picks up enabled logger settings from carrier', () => {
41+
getSentryCarrier(getMainCarrier()).loggerSettings = { enabled: true };
42+
43+
const consoleLogSpy = vi.spyOn(console, 'log').mockImplementation(() => {});
44+
logger.log('test');
45+
expect(consoleLogSpy).toHaveBeenCalledTimes(1);
46+
expect(logger.isEnabled()).toBe(true);
47+
});
48+
49+
it('picks up disabled logger settings from carrier', () => {
50+
getSentryCarrier(getMainCarrier()).loggerSettings = { enabled: false };
51+
52+
const consoleLogSpy = vi.spyOn(console, 'log').mockImplementation(() => {});
53+
logger.log('test');
54+
expect(consoleLogSpy).toHaveBeenCalledTimes(0);
55+
expect(logger.isEnabled()).toBe(false);
56+
});
57+
});

0 commit comments

Comments
 (0)