Skip to content

Commit 0138d1c

Browse files
authored
ref: Avoid using global singleton for logger (#8880)
This tries to simplify the logger to avoid the global lookup. If you have more than one utils, having multiple loggers would be the least of your problems.
1 parent 723285f commit 0138d1c

File tree

7 files changed

+97
-35
lines changed

7 files changed

+97
-35
lines changed
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
import * as Sentry from '@sentry/browser';
2+
3+
window.Sentry = Sentry;
4+
5+
Sentry.init({
6+
dsn: 'https://public@dsn.ingest.sentry.io/1337',
7+
debug: true,
8+
});
Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
/* eslint-disable no-console */
2+
import type { ConsoleMessage } from '@playwright/test';
3+
import { expect } from '@playwright/test';
4+
5+
import { sentryTest } from '../../../utils/fixtures';
6+
7+
sentryTest('logs debug messages correctly', async ({ getLocalTestUrl, page }) => {
8+
const bundleKey = process.env.PW_BUNDLE || '';
9+
const hasDebug = !bundleKey.includes('_min');
10+
11+
const url = await getLocalTestUrl({ testDir: __dirname });
12+
13+
const consoleMessages: string[] = [];
14+
15+
page.on('console', (msg: ConsoleMessage) => {
16+
consoleMessages.push(msg.text());
17+
});
18+
19+
await page.goto(url);
20+
21+
await page.evaluate(() => console.log('test log'));
22+
23+
expect(consoleMessages).toEqual(
24+
hasDebug
25+
? [
26+
'Sentry Logger [log]: Integration installed: InboundFilters',
27+
'Sentry Logger [log]: Integration installed: FunctionToString',
28+
'Sentry Logger [log]: Integration installed: TryCatch',
29+
'Sentry Logger [log]: Integration installed: Breadcrumbs',
30+
'Sentry Logger [log]: Global Handler attached: onerror',
31+
'Sentry Logger [log]: Global Handler attached: onunhandledrejection',
32+
'Sentry Logger [log]: Integration installed: GlobalHandlers',
33+
'Sentry Logger [log]: Integration installed: LinkedErrors',
34+
'Sentry Logger [log]: Integration installed: Dedupe',
35+
'Sentry Logger [log]: Integration installed: HttpContext',
36+
'Sentry Logger [warn]: Discarded session because of missing or non-string release',
37+
'test log',
38+
]
39+
: ['[Sentry] Cannot initialize SDK with `debug` option using a non-debug bundle.', 'test log'],
40+
);
41+
});

packages/hub/test/scope.test.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ describe('Scope', () => {
99
afterEach(() => {
1010
jest.resetAllMocks();
1111
jest.useRealTimers();
12+
GLOBAL_OBJ.__SENTRY__ = GLOBAL_OBJ.__SENTRY__ || {};
1213
GLOBAL_OBJ.__SENTRY__.globalEventProcessors = undefined;
1314
});
1415

packages/integrations/test/captureconsole.test.ts

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,13 @@
11
/* eslint-disable @typescript-eslint/unbound-method */
22
import type { Event, Hub, Integration } from '@sentry/types';
33
import type { ConsoleLevel } from '@sentry/utils';
4-
import { addInstrumentationHandler, CONSOLE_LEVELS, GLOBAL_OBJ, originalConsoleMethods } from '@sentry/utils';
4+
import {
5+
addInstrumentationHandler,
6+
CONSOLE_LEVELS,
7+
GLOBAL_OBJ,
8+
originalConsoleMethods,
9+
resetInstrumentationHandlers,
10+
} from '@sentry/utils';
511

612
import { CaptureConsole } from '../src/captureconsole';
713

@@ -54,6 +60,8 @@ describe('CaptureConsole setup', () => {
5460
CONSOLE_LEVELS.forEach(key => {
5561
originalConsoleMethods[key] = _originalConsoleMethods[key];
5662
});
63+
64+
resetInstrumentationHandlers();
5765
});
5866

5967
describe('monkeypatching', () => {

packages/replay/test/mocks/resetSdkMock.ts

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,6 @@
1+
import type { EventProcessor } from '@sentry/types';
2+
import { getGlobalSingleton, resetInstrumentationHandlers } from '@sentry/utils';
3+
14
import type { Replay as ReplayIntegration } from '../../src';
25
import type { ReplayContainer } from '../../src/replay';
36
import type { RecordMock } from './../index';
@@ -17,9 +20,11 @@ export async function resetSdkMock({ replayOptions, sentryOptions, autoStart }:
1720
jest.setSystemTime(new Date(BASE_TIMESTAMP));
1821
jest.clearAllMocks();
1922
jest.resetModules();
20-
// NOTE: The listeners added to `addInstrumentationHandler` are leaking
21-
// @ts-ignore Don't know if there's a cleaner way to clean up old event processors
22-
globalThis.__SENTRY__.globalEventProcessors = [];
23+
24+
// Clear all handlers that have been registered
25+
resetInstrumentationHandlers();
26+
getGlobalSingleton<EventProcessor[]>('globalEventProcessors', () => []).length = 0;
27+
2328
const SentryUtils = await import('@sentry/utils');
2429
jest.spyOn(SentryUtils, 'addInstrumentationHandler').mockImplementation((type, handler: (args: any) => any) => {
2530
if (type === 'dom') {

packages/utils/src/instrument.ts

Lines changed: 11 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ import type {
1111

1212
import { isString } from './is';
1313
import type { ConsoleLevel } from './logger';
14-
import { CONSOLE_LEVELS, logger } from './logger';
14+
import { CONSOLE_LEVELS, logger, originalConsoleMethods } from './logger';
1515
import { fill } from './object';
1616
import { getFunctionName } from './stacktrace';
1717
import { supportsHistory, supportsNativeFetch } from './supports';
@@ -94,6 +94,16 @@ export function addInstrumentationHandler(type: InstrumentHandlerType, callback:
9494
instrument(type);
9595
}
9696

97+
/**
98+
* Reset all instrumentation handlers.
99+
* This can be used by tests to ensure we have a clean slate of instrumentation handlers.
100+
*/
101+
export function resetInstrumentationHandlers(): void {
102+
Object.keys(handlers).forEach(key => {
103+
handlers[key as InstrumentHandlerType] = undefined;
104+
});
105+
}
106+
97107
/** JSDoc */
98108
function triggerHandlers(type: InstrumentHandlerType, data: any): void {
99109
if (!type || !handlers[type]) {
@@ -113,11 +123,6 @@ function triggerHandlers(type: InstrumentHandlerType, data: any): void {
113123
}
114124
}
115125

116-
/** Only exported for testing & debugging. */
117-
export const originalConsoleMethods: {
118-
[key in ConsoleLevel]?: (...args: any[]) => void;
119-
} = {};
120-
121126
/** JSDoc */
122127
function instrumentConsole(): void {
123128
if (!('console' in GLOBAL_OBJ)) {

packages/utils/src/logger.ts

Lines changed: 19 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,4 @@
1-
import type { WrappedFunction } from '@sentry/types';
2-
3-
import { getGlobalSingleton, GLOBAL_OBJ } from './worldwide';
1+
import { GLOBAL_OBJ } from './worldwide';
42

53
/** Prefix for logging strings */
64
const PREFIX = 'Sentry Logger ';
@@ -9,7 +7,13 @@ export const CONSOLE_LEVELS = ['debug', 'info', 'warn', 'error', 'log', 'assert'
97
export type ConsoleLevel = (typeof CONSOLE_LEVELS)[number];
108

119
type LoggerMethod = (...args: unknown[]) => void;
12-
type LoggerConsoleMethods = Record<(typeof CONSOLE_LEVELS)[number], LoggerMethod>;
10+
type LoggerConsoleMethods = Record<ConsoleLevel, LoggerMethod>;
11+
12+
/** This may be mutated by the console instrumentation. */
13+
export const originalConsoleMethods: {
14+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
15+
[key in ConsoleLevel]?: (...args: any[]) => void;
16+
} = {};
1317

1418
/** JSDoc */
1519
interface Logger extends LoggerConsoleMethods {
@@ -28,26 +32,24 @@ export function consoleSandbox<T>(callback: () => T): T {
2832
return callback();
2933
}
3034

31-
const originalConsole = GLOBAL_OBJ.console as Console & Record<string, unknown>;
32-
const wrappedLevels: Partial<LoggerConsoleMethods> = {};
35+
const console = GLOBAL_OBJ.console as Console;
36+
const wrappedFuncs: Partial<LoggerConsoleMethods> = {};
37+
38+
const wrappedLevels = Object.keys(originalConsoleMethods) as ConsoleLevel[];
3339

3440
// Restore all wrapped console methods
35-
CONSOLE_LEVELS.forEach(level => {
36-
// TODO(v7): Remove this check as it's only needed for Node 6
37-
const originalWrappedFunc =
38-
originalConsole[level] && (originalConsole[level] as WrappedFunction).__sentry_original__;
39-
if (level in originalConsole && originalWrappedFunc) {
40-
wrappedLevels[level] = originalConsole[level] as LoggerConsoleMethods[typeof level];
41-
originalConsole[level] = originalWrappedFunc as Console[typeof level];
42-
}
41+
wrappedLevels.forEach(level => {
42+
const originalConsoleMethod = originalConsoleMethods[level] as LoggerMethod;
43+
wrappedFuncs[level] = console[level] as LoggerMethod | undefined;
44+
console[level] = originalConsoleMethod;
4345
});
4446

4547
try {
4648
return callback();
4749
} finally {
4850
// Revert restoration to wrapped state
49-
Object.keys(wrappedLevels).forEach(level => {
50-
originalConsole[level] = wrappedLevels[level as (typeof CONSOLE_LEVELS)[number]];
51+
wrappedLevels.forEach(level => {
52+
console[level] = wrappedFuncs[level] as LoggerMethod;
5153
});
5254
}
5355
}
@@ -83,12 +85,4 @@ function makeLogger(): Logger {
8385
return logger as Logger;
8486
}
8587

86-
// Ensure we only have a single logger instance, even if multiple versions of @sentry/utils are being used
87-
let logger: Logger;
88-
if (__DEBUG_BUILD__) {
89-
logger = getGlobalSingleton('logger', makeLogger);
90-
} else {
91-
logger = makeLogger();
92-
}
93-
94-
export { logger };
88+
export const logger = makeLogger();

0 commit comments

Comments
 (0)