From ff0da3e3f3317792e068e3d9f3820cabb2c32df8 Mon Sep 17 00:00:00 2001 From: Francesco Novy Date: Tue, 29 Aug 2023 10:12:35 +0200 Subject: [PATCH] ref: Use consistent console instrumentation (#8879) While looking into logger issues, I noticed that we fill console.xxx multiple times. This PR changes that so that we use the console instrumentation from utils in all cases. --- packages/integrations/src/captureconsole.ts | 77 +++--- .../integrations/test/captureconsole.test.ts | 260 ++++++++++-------- packages/node/src/integrations/console.ts | 49 ++-- packages/utils/src/instrument.ts | 28 +- 4 files changed, 218 insertions(+), 196 deletions(-) diff --git a/packages/integrations/src/captureconsole.ts b/packages/integrations/src/captureconsole.ts index d72da5f2b5c4..993fb9414052 100644 --- a/packages/integrations/src/captureconsole.ts +++ b/packages/integrations/src/captureconsole.ts @@ -1,5 +1,11 @@ import type { EventProcessor, Hub, Integration } from '@sentry/types'; -import { CONSOLE_LEVELS, fill, GLOBAL_OBJ, safeJoin, severityLevelFromString } from '@sentry/utils'; +import { + addInstrumentationHandler, + CONSOLE_LEVELS, + GLOBAL_OBJ, + safeJoin, + severityLevelFromString, +} from '@sentry/utils'; /** Send Console API calls as Sentry Events */ export class CaptureConsole implements Integration { @@ -34,46 +40,45 @@ export class CaptureConsole implements Integration { return; } - this._levels.forEach((level: string) => { - // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access, @typescript-eslint/no-explicit-any - if (!(level in (GLOBAL_OBJ as any).console)) { + const levels = this._levels; + + addInstrumentationHandler('console', ({ args, level }: { args: unknown[]; level: string }) => { + if (!levels.includes(level)) { return; } - // eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/no-unsafe-member-access - fill((GLOBAL_OBJ as any).console, level, (originalConsoleMethod: () => any) => (...args: any[]): void => { - const hub = getCurrentHub(); - - if (hub.getIntegration(CaptureConsole)) { - hub.withScope(scope => { - scope.setLevel(severityLevelFromString(level)); - scope.setExtra('arguments', args); - scope.addEventProcessor(event => { - event.logger = 'console'; - return event; - }); + const hub = getCurrentHub(); - let message = safeJoin(args, ' '); - const error = args.find(arg => arg instanceof Error); - if (level === 'assert') { - if (args[0] === false) { - message = `Assertion failed: ${safeJoin(args.slice(1), ' ') || 'console.assert'}`; - scope.setExtra('arguments', args.slice(1)); - hub.captureMessage(message); - } - } else if (level === 'error' && error) { - hub.captureException(error); - } else { - hub.captureMessage(message); - } - }); - } + if (!hub.getIntegration(CaptureConsole)) { + return; + } - // this fails for some browsers. :( - if (originalConsoleMethod) { - originalConsoleMethod.apply(GLOBAL_OBJ.console, args); - } - }); + consoleHandler(hub, args, level); }); } } + +function consoleHandler(hub: Hub, args: unknown[], level: string): void { + hub.withScope(scope => { + scope.setLevel(severityLevelFromString(level)); + scope.setExtra('arguments', args); + scope.addEventProcessor(event => { + event.logger = 'console'; + return event; + }); + + let message = safeJoin(args, ' '); + const error = args.find(arg => arg instanceof Error); + if (level === 'assert') { + if (args[0] === false) { + message = `Assertion failed: ${safeJoin(args.slice(1), ' ') || 'console.assert'}`; + scope.setExtra('arguments', args.slice(1)); + hub.captureMessage(message); + } + } else if (level === 'error' && error) { + hub.captureException(error); + } else { + hub.captureMessage(message); + } + }); +} diff --git a/packages/integrations/test/captureconsole.test.ts b/packages/integrations/test/captureconsole.test.ts index 00e9f23c4564..0b851c493062 100644 --- a/packages/integrations/test/captureconsole.test.ts +++ b/packages/integrations/test/captureconsole.test.ts @@ -1,176 +1,194 @@ /* eslint-disable @typescript-eslint/unbound-method */ import type { Event, Hub, Integration } from '@sentry/types'; +import type { ConsoleLevel } from '@sentry/utils'; +import { addInstrumentationHandler, CONSOLE_LEVELS, GLOBAL_OBJ, originalConsoleMethods } from '@sentry/utils'; import { CaptureConsole } from '../src/captureconsole'; -const mockScope = { - setLevel: jest.fn(), - setExtra: jest.fn(), - addEventProcessor: jest.fn(), -}; - -const mockHub = { - withScope: jest.fn(callback => { - callback(mockScope); - }), - captureMessage: jest.fn(), - captureException: jest.fn(), -}; - -const mockConsole = { +const mockConsole: { [key in ConsoleLevel]: jest.Mock } = { debug: jest.fn(), log: jest.fn(), warn: jest.fn(), error: jest.fn(), assert: jest.fn(), info: jest.fn(), + trace: jest.fn(), }; -const getMockHubWithIntegration = (integration: Integration) => - ({ +function getMockHub(integration: Integration): Hub { + const mockScope = { + setLevel: jest.fn(), + setExtra: jest.fn(), + addEventProcessor: jest.fn(), + }; + + const mockHub = { + withScope: jest.fn(callback => { + callback(mockScope); + }), + captureMessage: jest.fn(), + captureException: jest.fn(), + getScope: jest.fn(() => mockScope), + }; + + return { ...mockHub, getIntegration: jest.fn(() => integration), - } as unknown as Hub); - -// We're using this to un-monkey patch the console after each test. -const originalConsole = Object.assign({}, global.console); + } as unknown as Hub; +} describe('CaptureConsole setup', () => { + // Ensure we've initialized the instrumentation so we can get the original one + addInstrumentationHandler('console', () => {}); + const _originalConsoleMethods = Object.assign({}, originalConsoleMethods); + beforeEach(() => { - // this suppresses output to the terminal running the tests, but doesn't interfere with our wrapping - Object.assign(global.console, mockConsole); + CONSOLE_LEVELS.forEach(key => { + originalConsoleMethods[key] = mockConsole[key]; + }); }); afterEach(() => { jest.clearAllMocks(); - // Un-monkey-patch the console functions - Object.assign(global.console, originalConsole); + CONSOLE_LEVELS.forEach(key => { + originalConsoleMethods[key] = _originalConsoleMethods[key]; + }); }); describe('monkeypatching', () => { - beforeEach(() => { - // for these tests only, we don't want to use the mock console, because we're testing for equality to methods from - // the original, so undo the global `beforeEach()` - Object.assign(global.console, originalConsole); - }); - it('should patch user-configured console levels', () => { const captureConsoleIntegration = new CaptureConsole({ levels: ['log', 'warn'] }); + const mockHub = getMockHub(captureConsoleIntegration); captureConsoleIntegration.setupOnce( () => undefined, - () => getMockHubWithIntegration(captureConsoleIntegration), + () => mockHub, ); - expect(global.console.error).toBe(originalConsole.error); // not monkey patched - expect(global.console.log).not.toBe(originalConsole.log); // monkey patched - expect(global.console.warn).not.toBe(originalConsole.warn); // monkey patched + GLOBAL_OBJ.console.error('msg 1'); + GLOBAL_OBJ.console.log('msg 2'); + GLOBAL_OBJ.console.warn('msg 3'); + + expect(mockHub.captureMessage).toHaveBeenCalledTimes(2); }); it('should fall back to default console levels if none are provided', () => { const captureConsoleIntegration = new CaptureConsole(); + const mockHub = getMockHub(captureConsoleIntegration); captureConsoleIntegration.setupOnce( () => undefined, - () => getMockHubWithIntegration(captureConsoleIntegration), + () => mockHub, ); - // expect a set of defined console levels to have been monkey patched - expect(global.console.debug).not.toBe(originalConsole.debug); - expect(global.console.info).not.toBe(originalConsole.info); - expect(global.console.warn).not.toBe(originalConsole.warn); - expect(global.console.error).not.toBe(originalConsole.error); - expect(global.console.log).not.toBe(originalConsole.log); - expect(global.console.assert).not.toBe(originalConsole.assert); - expect(global.console.trace).not.toBe(originalConsole.trace); - - // any other fields should not have been patched - expect(global.console.table).toBe(originalConsole.table); + // Assert has a special handling + (['debug', 'info', 'warn', 'error', 'log', 'trace'] as const).forEach(key => { + GLOBAL_OBJ.console[key]('msg'); + }); + + GLOBAL_OBJ.console.assert(false); + + expect(mockHub.captureMessage).toHaveBeenCalledTimes(7); }); it('should not wrap any functions with an empty levels option', () => { const captureConsoleIntegration = new CaptureConsole({ levels: [] }); + const mockHub = getMockHub(captureConsoleIntegration); captureConsoleIntegration.setupOnce( () => undefined, - () => getMockHubWithIntegration(captureConsoleIntegration), + () => mockHub, ); - // expect the default set of console levels not to have been monkey patched - expect(global.console.debug).toBe(originalConsole.debug); - expect(global.console.info).toBe(originalConsole.info); - expect(global.console.warn).toBe(originalConsole.warn); - expect(global.console.error).toBe(originalConsole.error); - expect(global.console.log).toBe(originalConsole.log); - expect(global.console.assert).toBe(originalConsole.assert); + CONSOLE_LEVELS.forEach(key => { + GLOBAL_OBJ.console[key]('msg'); + }); - // suppress output from the logging we're about to do - global.console.log = global.console.info = jest.fn(); - - // expect no message to be captured with console.log - global.console.log('some message'); - expect(mockHub.captureMessage).not.toHaveBeenCalled(); + expect(mockHub.captureMessage).toHaveBeenCalledTimes(0); }); }); it('setup should fail gracefully when console is not available', () => { - const consoleRef = global.console; + const consoleRef = GLOBAL_OBJ.console; // @ts-ignore remove console - delete global.console; + delete GLOBAL_OBJ.console; + const captureConsoleIntegration = new CaptureConsole(); + const mockHub = getMockHub(captureConsoleIntegration); expect(() => { - const captureConsoleIntegration = new CaptureConsole(); captureConsoleIntegration.setupOnce( () => undefined, - () => getMockHubWithIntegration(captureConsoleIntegration), + () => mockHub, ); }).not.toThrow(); // reinstate initial console - global.console = consoleRef; + GLOBAL_OBJ.console = consoleRef; }); it('should set a level in the scope when console function is called', () => { const captureConsoleIntegration = new CaptureConsole({ levels: ['error'] }); + const mockHub = getMockHub(captureConsoleIntegration); captureConsoleIntegration.setupOnce( () => undefined, - () => getMockHubWithIntegration(captureConsoleIntegration), + () => mockHub, ); + const mockScope = mockHub.getScope(); + // call a wrapped function - global.console.error('some logging message'); + GLOBAL_OBJ.console.error('some logging message'); expect(mockScope.setLevel).toHaveBeenCalledTimes(1); expect(mockScope.setLevel).toHaveBeenCalledWith('error'); }); - it('should send arguments as extra data on failed assertion', () => { + it('should send arguments as extra data', () => { const captureConsoleIntegration = new CaptureConsole({ levels: ['log'] }); + const mockHub = getMockHub(captureConsoleIntegration); captureConsoleIntegration.setupOnce( () => undefined, - () => getMockHubWithIntegration(captureConsoleIntegration), + () => mockHub, ); - // call a wrapped function - global.console.log('some arg 1', 'some arg 2'); - global.console.log(); + const mockScope = mockHub.getScope(); + + GLOBAL_OBJ.console.log('some arg 1', 'some arg 2'); - expect(mockScope.setExtra).toHaveBeenCalledTimes(2); + expect(mockScope.setExtra).toHaveBeenCalledTimes(1); expect(mockScope.setExtra).toHaveBeenCalledWith('arguments', ['some arg 1', 'some arg 2']); + }); + + it('should send empty arguments as extra data', () => { + const captureConsoleIntegration = new CaptureConsole({ levels: ['log'] }); + const mockHub = getMockHub(captureConsoleIntegration); + captureConsoleIntegration.setupOnce( + () => undefined, + () => mockHub, + ); + + const mockScope = mockHub.getScope(); + + GLOBAL_OBJ.console.log(); + + expect(mockScope.setExtra).toHaveBeenCalledTimes(1); expect(mockScope.setExtra).toHaveBeenCalledWith('arguments', []); }); it('should add an event processor that sets the `logger` field of events', () => { const captureConsoleIntegration = new CaptureConsole({ levels: ['log'] }); + const mockHub = getMockHub(captureConsoleIntegration); captureConsoleIntegration.setupOnce( () => undefined, - () => getMockHubWithIntegration(captureConsoleIntegration), + () => mockHub, ); + const mockScope = mockHub.getScope(); + // call a wrapped function - global.console.log('some message'); + GLOBAL_OBJ.console.log('some message'); expect(mockScope.addEventProcessor).toHaveBeenCalledTimes(1); - const addedEventProcessor = mockScope.addEventProcessor.mock.calls[0][0]; + const addedEventProcessor = (mockScope.addEventProcessor as jest.Mock).mock.calls[0][0]; const someEvent: Event = {}; addedEventProcessor(someEvent); @@ -179,12 +197,15 @@ describe('CaptureConsole setup', () => { it('should capture message on a failed assertion', () => { const captureConsoleIntegration = new CaptureConsole({ levels: ['assert'] }); + const mockHub = getMockHub(captureConsoleIntegration); captureConsoleIntegration.setupOnce( () => undefined, - () => getMockHubWithIntegration(captureConsoleIntegration), + () => mockHub, ); - global.console.assert(1 + 1 === 3); + const mockScope = mockHub.getScope(); + + GLOBAL_OBJ.console.assert(1 + 1 === 3); expect(mockScope.setExtra).toHaveBeenLastCalledWith('arguments', []); expect(mockHub.captureMessage).toHaveBeenCalledTimes(1); @@ -193,12 +214,15 @@ describe('CaptureConsole setup', () => { it('should capture correct message on a failed assertion with message', () => { const captureConsoleIntegration = new CaptureConsole({ levels: ['assert'] }); + const mockHub = getMockHub(captureConsoleIntegration); captureConsoleIntegration.setupOnce( () => undefined, - () => getMockHubWithIntegration(captureConsoleIntegration), + () => mockHub, ); - global.console.assert(1 + 1 === 3, 'expression is false'); + const mockScope = mockHub.getScope(); + + GLOBAL_OBJ.console.assert(1 + 1 === 3, 'expression is false'); expect(mockScope.setExtra).toHaveBeenLastCalledWith('arguments', ['expression is false']); expect(mockHub.captureMessage).toHaveBeenCalledTimes(1); @@ -207,23 +231,25 @@ describe('CaptureConsole setup', () => { it('should not capture message on a successful assertion', () => { const captureConsoleIntegration = new CaptureConsole({ levels: ['assert'] }); + const mockHub = getMockHub(captureConsoleIntegration); captureConsoleIntegration.setupOnce( () => undefined, - () => getMockHubWithIntegration(captureConsoleIntegration), + () => mockHub, ); - global.console.assert(1 + 1 === 2); + GLOBAL_OBJ.console.assert(1 + 1 === 2); }); it('should capture exception when console logs an error object with level set to "error"', () => { const captureConsoleIntegration = new CaptureConsole({ levels: ['error'] }); + const mockHub = getMockHub(captureConsoleIntegration); captureConsoleIntegration.setupOnce( () => undefined, - () => getMockHubWithIntegration(captureConsoleIntegration), + () => mockHub, ); const someError = new Error('some error'); - global.console.error(someError); + GLOBAL_OBJ.console.error(someError); expect(mockHub.captureException).toHaveBeenCalledTimes(1); expect(mockHub.captureException).toHaveBeenCalledWith(someError); @@ -231,13 +257,14 @@ describe('CaptureConsole setup', () => { it('should capture exception on `console.error` when no levels are provided in constructor', () => { const captureConsoleIntegration = new CaptureConsole(); + const mockHub = getMockHub(captureConsoleIntegration); captureConsoleIntegration.setupOnce( () => undefined, - () => getMockHubWithIntegration(captureConsoleIntegration), + () => mockHub, ); const someError = new Error('some error'); - global.console.error(someError); + GLOBAL_OBJ.console.error(someError); expect(mockHub.captureException).toHaveBeenCalledTimes(1); expect(mockHub.captureException).toHaveBeenCalledWith(someError); @@ -245,13 +272,14 @@ describe('CaptureConsole setup', () => { it('should capture exception when console logs an error object in any of the args when level set to "error"', () => { const captureConsoleIntegration = new CaptureConsole({ levels: ['error'] }); + const mockHub = getMockHub(captureConsoleIntegration); captureConsoleIntegration.setupOnce( () => undefined, - () => getMockHubWithIntegration(captureConsoleIntegration), + () => mockHub, ); const someError = new Error('some error'); - global.console.error('Something went wrong', someError); + GLOBAL_OBJ.console.error('Something went wrong', someError); expect(mockHub.captureException).toHaveBeenCalledTimes(1); expect(mockHub.captureException).toHaveBeenCalledWith(someError); @@ -259,12 +287,13 @@ describe('CaptureConsole setup', () => { it('should capture message on `console.log` when no levels are provided in constructor', () => { const captureConsoleIntegration = new CaptureConsole(); + const mockHub = getMockHub(captureConsoleIntegration); captureConsoleIntegration.setupOnce( () => undefined, - () => getMockHubWithIntegration(captureConsoleIntegration), + () => mockHub, ); - global.console.error('some message'); + GLOBAL_OBJ.console.error('some message'); expect(mockHub.captureMessage).toHaveBeenCalledTimes(1); expect(mockHub.captureMessage).toHaveBeenCalledWith('some message'); @@ -272,12 +301,13 @@ describe('CaptureConsole setup', () => { it('should capture message when console logs a non-error object with level set to "error"', () => { const captureConsoleIntegration = new CaptureConsole({ levels: ['error'] }); + const mockHub = getMockHub(captureConsoleIntegration); captureConsoleIntegration.setupOnce( () => undefined, - () => getMockHubWithIntegration(captureConsoleIntegration), + () => mockHub, ); - global.console.error('some non-error message'); + GLOBAL_OBJ.console.error('some non-error message'); expect(mockHub.captureMessage).toHaveBeenCalledTimes(1); expect(mockHub.captureMessage).toHaveBeenCalledWith('some non-error message'); @@ -286,12 +316,13 @@ describe('CaptureConsole setup', () => { it('should capture a message for non-error log levels', () => { const captureConsoleIntegration = new CaptureConsole({ levels: ['info'] }); + const mockHub = getMockHub(captureConsoleIntegration); captureConsoleIntegration.setupOnce( () => undefined, - () => getMockHubWithIntegration(captureConsoleIntegration), + () => mockHub, ); - global.console.info('some message'); + GLOBAL_OBJ.console.info('some message'); expect(mockHub.captureMessage).toHaveBeenCalledTimes(1); expect(mockHub.captureMessage).toHaveBeenCalledWith('some message'); @@ -299,70 +330,63 @@ describe('CaptureConsole setup', () => { it('should call the original console function when console members are called', () => { // Mock console log to test if it was called - const originalConsoleLog = global.console.log; + const originalConsoleLog = GLOBAL_OBJ.console.log; const mockConsoleLog = jest.fn(); - global.console.log = mockConsoleLog; + GLOBAL_OBJ.console.log = mockConsoleLog; const captureConsoleIntegration = new CaptureConsole({ levels: ['log'] }); + const mockHub = getMockHub(captureConsoleIntegration); captureConsoleIntegration.setupOnce( () => undefined, - () => getMockHubWithIntegration(captureConsoleIntegration), + () => mockHub, ); - global.console.log('some message 1', 'some message 2'); + GLOBAL_OBJ.console.log('some message 1', 'some message 2'); expect(mockConsoleLog).toHaveBeenCalledTimes(1); expect(mockConsoleLog).toHaveBeenCalledWith('some message 1', 'some message 2'); // Reset console log - global.console.log = originalConsoleLog; + GLOBAL_OBJ.console.log = originalConsoleLog; }); it('should not wrap any levels that are not members of console', () => { const captureConsoleIntegration = new CaptureConsole({ levels: ['log', 'someNonExistingLevel', 'error'] }); + const mockHub = getMockHub(captureConsoleIntegration); captureConsoleIntegration.setupOnce( () => undefined, - () => getMockHubWithIntegration(captureConsoleIntegration), + () => mockHub, ); // The provided level should not be created - expect((global.console as any)['someNonExistingLevel']).toBeUndefined(); - - // Ohter levels should be wrapped as expected - expect(global.console.log).not.toBe(originalConsole.log); - expect(global.console.error).not.toBe(originalConsole.error); + expect((GLOBAL_OBJ.console as any)['someNonExistingLevel']).toBeUndefined(); }); it('should wrap the console when the client does not have a registered captureconsole integration, but not capture any messages', () => { const captureConsoleIntegration = new CaptureConsole({ levels: ['log', 'error'] }); + const mockHub = getMockHub(null as any); // simulate not having the integration registered captureConsoleIntegration.setupOnce( () => undefined, - () => getMockHubWithIntegration(null as any), // simulate not having the integration registered + () => mockHub, ); - // Console should be wrapped - expect(global.console.log).not.toBe(originalConsole.log); - expect(global.console.error).not.toBe(originalConsole.error); - // Should not capture messages - global.console.log('some message'); + GLOBAL_OBJ.console.log('some message'); expect(mockHub.captureMessage).not.toHaveBeenCalledWith(); }); it("should not crash when the original console methods don't exist at time of invocation", () => { - const originalConsoleLog = global.console.log; - global.console.log = undefined as any; // don't `delete` here, otherwise `fill` won't wrap the function + originalConsoleMethods.log = undefined; const captureConsoleIntegration = new CaptureConsole({ levels: ['log'] }); + const mockHub = getMockHub(captureConsoleIntegration); captureConsoleIntegration.setupOnce( () => undefined, - () => getMockHubWithIntegration(captureConsoleIntegration), + () => mockHub, ); expect(() => { - global.console.log('some message'); + GLOBAL_OBJ.console.log('some message'); }).not.toThrow(); - - global.console.log = originalConsoleLog; }); }); diff --git a/packages/node/src/integrations/console.ts b/packages/node/src/integrations/console.ts index da0b684c4992..eb8a38980a64 100644 --- a/packages/node/src/integrations/console.ts +++ b/packages/node/src/integrations/console.ts @@ -1,6 +1,6 @@ import { getCurrentHub } from '@sentry/core'; import type { Integration } from '@sentry/types'; -import { fill, severityLevelFromString } from '@sentry/utils'; +import { addInstrumentationHandler, severityLevelFromString } from '@sentry/utils'; import * as util from 'util'; /** Console module integration */ @@ -19,37 +19,24 @@ export class Console implements Integration { * @inheritDoc */ public setupOnce(): void { - for (const level of ['debug', 'info', 'warn', 'error', 'log']) { - fill(console, level, createConsoleWrapper(level)); - } - } -} + addInstrumentationHandler('console', ({ args, level }: { args: unknown[]; level: string }) => { + const hub = getCurrentHub(); -/** - * Wrapper function that'll be used for every console level - */ -function createConsoleWrapper(level: string): (originalConsoleMethod: () => void) => void { - return function consoleWrapper(originalConsoleMethod: () => void): () => void { - const sentryLevel = severityLevelFromString(level); - - /* eslint-disable prefer-rest-params */ - return function (this: typeof console): void { - if (getCurrentHub().getIntegration(Console)) { - getCurrentHub().addBreadcrumb( - { - category: 'console', - level: sentryLevel, - message: util.format.apply(undefined, arguments), - }, - { - input: [...arguments], - level, - }, - ); + if (!hub.getIntegration(Console)) { + return; } - originalConsoleMethod.apply(this, arguments); - }; - /* eslint-enable prefer-rest-params */ - }; + hub.addBreadcrumb( + { + category: 'console', + level: severityLevelFromString(level), + message: util.format.apply(undefined, args), + }, + { + input: [...args], + level, + }, + ); + }); + } } diff --git a/packages/utils/src/instrument.ts b/packages/utils/src/instrument.ts index be0f8f49cedc..94812f47b252 100644 --- a/packages/utils/src/instrument.ts +++ b/packages/utils/src/instrument.ts @@ -10,11 +10,12 @@ import type { } from '@sentry/types'; import { isString } from './is'; +import type { ConsoleLevel } from './logger'; import { CONSOLE_LEVELS, logger } from './logger'; import { fill } from './object'; import { getFunctionName } from './stacktrace'; import { supportsHistory, supportsNativeFetch } from './supports'; -import { getGlobalObject } from './worldwide'; +import { getGlobalObject, GLOBAL_OBJ } from './worldwide'; // eslint-disable-next-line deprecation/deprecation const WINDOW = getGlobalObject(); @@ -112,25 +113,30 @@ function triggerHandlers(type: InstrumentHandlerType, data: any): void { } } +/** Only exported for testing & debugging. */ +export const originalConsoleMethods: { + [key in ConsoleLevel]?: (...args: any[]) => void; +} = {}; + /** JSDoc */ function instrumentConsole(): void { - if (!('console' in WINDOW)) { + if (!('console' in GLOBAL_OBJ)) { return; } - CONSOLE_LEVELS.forEach(function (level: string): void { - if (!(level in WINDOW.console)) { + CONSOLE_LEVELS.forEach(function (level: ConsoleLevel): void { + if (!(level in GLOBAL_OBJ.console)) { return; } - fill(WINDOW.console, level, function (originalConsoleMethod: () => any): Function { + fill(GLOBAL_OBJ.console, level, function (originalConsoleMethod: () => any): Function { + originalConsoleMethods[level] = originalConsoleMethod; + return function (...args: any[]): void { triggerHandlers('console', { args, level }); - // this fails for some browsers. :( - if (originalConsoleMethod) { - originalConsoleMethod.apply(WINDOW.console, args); - } + const log = originalConsoleMethods[level]; + log && log.apply(GLOBAL_OBJ.console, args); }; }); }); @@ -142,7 +148,7 @@ function instrumentFetch(): void { return; } - fill(WINDOW, 'fetch', function (originalFetch: () => void): () => void { + fill(GLOBAL_OBJ, 'fetch', function (originalFetch: () => void): () => void { return function (...args: any[]): void { const { method, url } = parseFetchArgs(args); @@ -160,7 +166,7 @@ function instrumentFetch(): void { }); // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access - return originalFetch.apply(WINDOW, args).then( + return originalFetch.apply(GLOBAL_OBJ, args).then( (response: Response) => { triggerHandlers('fetch', { ...handlerData,