From b5e5cadaa4cde7bfca1ab942ee9bf0a77edc7147 Mon Sep 17 00:00:00 2001 From: Francesco Gringl-Novy Date: Mon, 11 May 2026 11:10:49 +0200 Subject: [PATCH 1/9] feat(core): Add `addConsoleInstrumentationFilter` utility We want to leverage this for Node 26 to filter deprecation messages from IITM, but this could also be used by users if they want to silence certain things. --- packages/core/src/instrument/console.ts | 43 ++++++++- packages/core/src/integrations/console.ts | 5 + packages/core/src/shared-exports.ts | 2 +- .../core/test/lib/instrument/console.test.ts | 92 ++++++++++++++++++- .../node-core/src/integrations/console.ts | 11 +++ 5 files changed, 147 insertions(+), 6 deletions(-) diff --git a/packages/core/src/instrument/console.ts b/packages/core/src/instrument/console.ts index ef7e9c804943..a777c3eda04f 100644 --- a/packages/core/src/instrument/console.ts +++ b/packages/core/src/instrument/console.ts @@ -1,10 +1,23 @@ /* eslint-disable @typescript-eslint/no-explicit-any */ /* eslint-disable @typescript-eslint/ban-types */ +import { DEBUG_BUILD } from '../debug-build'; import type { ConsoleLevel, HandlerDataConsole } from '../types-hoist/instrument'; import { CONSOLE_LEVELS, originalConsoleMethods } from '../utils/debug-logger'; import { fill } from '../utils/object'; +import { stringMatchesSomePattern } from '../utils/string'; import { GLOBAL_OBJ } from '../utils/worldwide'; import { addHandler, maybeInstrument, triggerHandlers } from './handlers'; +import { debug } from '../utils/debug-logger'; + +interface ConsoleInstrumentationOptions { + /** + * Filter out console messages that match the given strings or regular expressions. + * These will neither be passed to the handler, and they will also not be logged to the user, unless they have debug enabled. + */ + filter?: (string | RegExp)[]; +} + +let _options: ConsoleInstrumentationOptions = {}; /** * Add an instrumentation handler for when a console.xxx method is called. @@ -20,6 +33,18 @@ export function addConsoleInstrumentationHandler(handler: (data: HandlerDataCons return removeHandler; } +export function addConsoleInstrumentationFilter(filter: (string | RegExp)[]): void { + _options = { + ..._options, + filter: [...(_options.filter || []), ...filter], + }; +} + +/** Only exported for tests. */ +export function _INTERNAL_resetConsoleInstrumentationOptions(): void { + _options = {}; +} + function instrumentConsole(): void { if (!('console' in GLOBAL_OBJ)) { return; @@ -34,10 +59,22 @@ function instrumentConsole(): void { originalConsoleMethods[level] = originalConsoleMethod; return function (...args: any[]): void { - triggerHandlers('console', { args, level } as HandlerDataConsole); - + const firstArg = args[0]; const log = originalConsoleMethods[level]; - log?.apply(GLOBAL_OBJ.console, args); + const filter = _options?.filter; + + const isFiltered = filter && typeof firstArg === 'string' && stringMatchesSomePattern(firstArg, filter); + + // Only trigger handlers for non-filtered messages + if (!isFiltered) { + triggerHandlers('console', { args, level } as HandlerDataConsole); + } + + // Only log filtered messages in debug mode + if (!isFiltered || (DEBUG_BUILD && debug.isEnabled())) { + // Call original console method + log?.apply(GLOBAL_OBJ.console, args); + } }; }); }); diff --git a/packages/core/src/integrations/console.ts b/packages/core/src/integrations/console.ts index e39fd5ddcf0d..e7baa781e9c7 100644 --- a/packages/core/src/integrations/console.ts +++ b/packages/core/src/integrations/console.ts @@ -10,6 +10,11 @@ import { GLOBAL_OBJ } from '../utils/worldwide'; interface ConsoleIntegrationOptions { levels: ConsoleLevel[]; + /** + * Filter out console messages that match the given strings or regular expressions. + * These will neither be passed to the handler, and they will also not be logged to the user, unless they have debug enabled. + */ + filter?: (string | RegExp)[]; } type GlobalObjectWithUtil = typeof GLOBAL_OBJ & { diff --git a/packages/core/src/shared-exports.ts b/packages/core/src/shared-exports.ts index ad84415a750f..dcc8c268f509 100644 --- a/packages/core/src/shared-exports.ts +++ b/packages/core/src/shared-exports.ts @@ -208,7 +208,7 @@ export { dsnFromString, dsnToString, makeDsn } from './utils/dsn'; export { SentryError } from './utils/error'; export { GLOBAL_OBJ } from './utils/worldwide'; export type { InternalGlobal } from './utils/worldwide'; -export { addConsoleInstrumentationHandler } from './instrument/console'; +export { addConsoleInstrumentationHandler, addConsoleInstrumentationFilter } from './instrument/console'; export { addFetchEndInstrumentationHandler, addFetchInstrumentationHandler } from './instrument/fetch'; export { addGlobalErrorInstrumentationHandler } from './instrument/globalError'; export { addGlobalUnhandledRejectionInstrumentationHandler } from './instrument/globalUnhandledRejection'; diff --git a/packages/core/test/lib/instrument/console.test.ts b/packages/core/test/lib/instrument/console.test.ts index 2499a231712d..f67f7da1a1a6 100644 --- a/packages/core/test/lib/instrument/console.test.ts +++ b/packages/core/test/lib/instrument/console.test.ts @@ -1,22 +1,110 @@ -import { describe, expect, it, vi } from 'vitest'; -import { addConsoleInstrumentationHandler } from '../../../src/instrument/console'; +import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; +import { + _INTERNAL_resetConsoleInstrumentationOptions, + addConsoleInstrumentationFilter, + addConsoleInstrumentationHandler, +} from '../../../src/instrument/console'; import { GLOBAL_OBJ } from '../../../src/utils/worldwide'; +import { debug, originalConsoleMethods } from '../../../src/utils/debug-logger'; +import { resetInstrumentationHandlers } from '../../../src/instrument/handlers'; describe('addConsoleInstrumentationHandler', () => { + let _originalConsoleMethods: typeof originalConsoleMethods = {}; + + afterEach(() => { + Object.assign(originalConsoleMethods, _originalConsoleMethods); + resetInstrumentationHandlers(); + vi.restoreAllMocks(); + }); + + // This cannot be done in beforeEach, as the first invocation of `addConsoleInstrumentationHandler` will overwrite the original console methods. + // Due to `fill` being called + // So instead, we need to call this each time after calling `addConsoleInstrumentationHandler` + function mockConsoleMethods() { + // Re-store this with the current implementation + Object.assign(_originalConsoleMethods, originalConsoleMethods); + + // Overwrite with mock console methods + Object.assign(originalConsoleMethods, { + log: vi.fn(), + warn: vi.fn(), + error: vi.fn(), + debug: vi.fn(), + info: vi.fn(), + }); + } + it.each(['log', 'warn', 'error', 'debug', 'info'] as const)( 'calls registered handler when console.%s is called', level => { const handler = vi.fn(); addConsoleInstrumentationHandler(handler); + mockConsoleMethods(); GLOBAL_OBJ.console[level]('test message'); expect(handler).toHaveBeenCalledWith(expect.objectContaining({ args: ['test message'], level })); + expect(originalConsoleMethods[level]).toHaveBeenCalledWith('test message'); }, ); it('calls through to the underlying console method without throwing', () => { addConsoleInstrumentationHandler(vi.fn()); + mockConsoleMethods(); expect(() => GLOBAL_OBJ.console.log('hello')).not.toThrow(); }); + + describe('filter', () => { + afterEach(() => { + _INTERNAL_resetConsoleInstrumentationOptions(); + }); + + describe('when debug is disabled', () => { + beforeEach(() => { + vi.spyOn(debug, 'isEnabled').mockImplementation(() => false); + }); + + it('filters out messages that match the filter', () => { + const handler = vi.fn(); + addConsoleInstrumentationHandler(handler); + addConsoleInstrumentationFilter(['test message']); + mockConsoleMethods(); + + GLOBAL_OBJ.console.log('test message'); + + expect(originalConsoleMethods.log).not.toHaveBeenCalledWith('test message'); + expect(handler).not.toHaveBeenCalled(); + }); + + it('does not filter out messages that do not match the filter', () => { + const handler = vi.fn(); + addConsoleInstrumentationHandler(handler); + addConsoleInstrumentationFilter(['test message']); + mockConsoleMethods(); + + GLOBAL_OBJ.console.log('other message'); + + expect(handler).toHaveBeenCalled(); + expect(originalConsoleMethods.log).toHaveBeenCalledWith('other message'); + }); + }); + + describe('when debug is enabled', () => { + beforeEach(() => { + vi.spyOn(debug, 'isEnabled').mockImplementation(() => true); + }); + + it('logs filtered messages but does not call the handler for them', () => { + const handler = vi.fn(); + addConsoleInstrumentationHandler(handler); + addConsoleInstrumentationFilter(['test message']); + mockConsoleMethods(); + + GLOBAL_OBJ.console.log('test message'); + + expect(handler).not.toHaveBeenCalled(); + expect(originalConsoleMethods.log).toHaveBeenCalledWith('test message'); + }); + }); + }); }); diff --git a/packages/node-core/src/integrations/console.ts b/packages/node-core/src/integrations/console.ts index d958e00bdf12..dedab25da3a1 100644 --- a/packages/node-core/src/integrations/console.ts +++ b/packages/node-core/src/integrations/console.ts @@ -10,10 +10,16 @@ import { maybeInstrument, originalConsoleMethods, triggerHandlers, + addConsoleInstrumentationFilter, } from '@sentry/core'; interface ConsoleIntegrationOptions { levels: ConsoleLevel[]; + /** + * Filter out console messages that match the given strings or regular expressions. + * These will neither be passed to the handler, and they will also not be logged to the user, unless they have debug enabled. + */ + filter?: (string | RegExp)[]; } /** @@ -35,6 +41,11 @@ export const consoleIntegration = defineIntegration((options: Partial Date: Mon, 11 May 2026 11:19:41 +0200 Subject: [PATCH 2/9] fix it for core --- packages/core/src/integrations/console.ts | 6 +++++- packages/node-core/src/integrations/console.ts | 5 ----- 2 files changed, 5 insertions(+), 6 deletions(-) diff --git a/packages/core/src/integrations/console.ts b/packages/core/src/integrations/console.ts index e7baa781e9c7..9f672e2ba2c3 100644 --- a/packages/core/src/integrations/console.ts +++ b/packages/core/src/integrations/console.ts @@ -1,6 +1,6 @@ import { addBreadcrumb } from '../breadcrumbs'; import { getClient } from '../currentScopes'; -import { addConsoleInstrumentationHandler } from '../instrument/console'; +import { addConsoleInstrumentationFilter, addConsoleInstrumentationHandler } from '../instrument/console'; import { defineIntegration } from '../integration'; import type { ConsoleLevel } from '../types-hoist/instrument'; import { CONSOLE_LEVELS } from '../utils/debug-logger'; @@ -54,6 +54,10 @@ export const consoleIntegration = defineIntegration((options: Partial Date: Mon, 11 May 2026 11:30:31 +0200 Subject: [PATCH 3/9] add test --- .../console/filter/instrument.mjs | 10 ++++++ .../integrations/console/filter/scenario.mjs | 9 +++++ .../integrations/console/filter/test.ts | 35 +++++++++++++++++++ .../node-core/src/integrations/console.ts | 1 - 4 files changed, 54 insertions(+), 1 deletion(-) create mode 100644 dev-packages/node-integration-tests/suites/integrations/console/filter/instrument.mjs create mode 100644 dev-packages/node-integration-tests/suites/integrations/console/filter/scenario.mjs create mode 100644 dev-packages/node-integration-tests/suites/integrations/console/filter/test.ts diff --git a/dev-packages/node-integration-tests/suites/integrations/console/filter/instrument.mjs b/dev-packages/node-integration-tests/suites/integrations/console/filter/instrument.mjs new file mode 100644 index 000000000000..16c05ae32ed0 --- /dev/null +++ b/dev-packages/node-integration-tests/suites/integrations/console/filter/instrument.mjs @@ -0,0 +1,10 @@ +import * as Sentry from '@sentry/node'; +import { loggingTransport } from '@sentry-internal/node-integration-tests'; + +Sentry.init({ + dsn: 'https://public@dsn.ingest.sentry.io/1337', + release: '1.0', + transport: loggingTransport, + defaultIntegrations: false, + integrations: [Sentry.consoleIntegration({ filter: ['foo'] })], +}); diff --git a/dev-packages/node-integration-tests/suites/integrations/console/filter/scenario.mjs b/dev-packages/node-integration-tests/suites/integrations/console/filter/scenario.mjs new file mode 100644 index 000000000000..5d1e93582c0b --- /dev/null +++ b/dev-packages/node-integration-tests/suites/integrations/console/filter/scenario.mjs @@ -0,0 +1,9 @@ +/* eslint-disable no-console */ +import * as Sentry from '@sentry/node'; + +console.log('hello'); +console.log('foo'); +console.log('foo2'); +console.log('baz'); + +Sentry.captureException(new Error('Test Error')); diff --git a/dev-packages/node-integration-tests/suites/integrations/console/filter/test.ts b/dev-packages/node-integration-tests/suites/integrations/console/filter/test.ts new file mode 100644 index 000000000000..5f0bf9e8b14a --- /dev/null +++ b/dev-packages/node-integration-tests/suites/integrations/console/filter/test.ts @@ -0,0 +1,35 @@ +import { afterAll, describe, expect } from 'vitest'; +import { cleanupChildProcesses, createEsmAndCjsTests } from '../../../../utils/runner'; + +describe('Console Integration', () => { + afterAll(() => { + cleanupChildProcesses(); + }); + + createEsmAndCjsTests(__dirname, 'scenario.mjs', 'instrument.mjs', (createRunner, test) => { + test('filters console messages', async () => { + await createRunner() + .expect({ + event: { + exception: { + values: [ + { + value: 'Test Error', + }, + ], + }, + breadcrumbs: [ + expect.objectContaining({ + message: 'hello', + }), + expect.objectContaining({ + message: 'baz', + }), + ], + }, + }) + .start() + .completed(); + }); + }); +}); diff --git a/packages/node-core/src/integrations/console.ts b/packages/node-core/src/integrations/console.ts index 00f31923bf40..675019d41c96 100644 --- a/packages/node-core/src/integrations/console.ts +++ b/packages/node-core/src/integrations/console.ts @@ -10,7 +10,6 @@ import { maybeInstrument, originalConsoleMethods, triggerHandlers, - addConsoleInstrumentationFilter, } from '@sentry/core'; interface ConsoleIntegrationOptions { From 78dfb85b11b4900bd0ab7661e9cc3fc6cb4d1b56 Mon Sep 17 00:00:00 2001 From: Francesco Gringl-Novy Date: Mon, 11 May 2026 11:38:10 +0200 Subject: [PATCH 4/9] add comment --- packages/node-core/src/integrations/console.ts | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/packages/node-core/src/integrations/console.ts b/packages/node-core/src/integrations/console.ts index 675019d41c96..d85e00e6b9d7 100644 --- a/packages/node-core/src/integrations/console.ts +++ b/packages/node-core/src/integrations/console.ts @@ -45,6 +45,10 @@ export const consoleIntegration = defineIntegration((options: Partial Date: Mon, 11 May 2026 12:21:16 +0200 Subject: [PATCH 5/9] use filter --- packages/core/src/instrument/console.ts | 24 ++++++++++-------------- 1 file changed, 10 insertions(+), 14 deletions(-) diff --git a/packages/core/src/instrument/console.ts b/packages/core/src/instrument/console.ts index a777c3eda04f..43780a732cec 100644 --- a/packages/core/src/instrument/console.ts +++ b/packages/core/src/instrument/console.ts @@ -9,15 +9,12 @@ import { GLOBAL_OBJ } from '../utils/worldwide'; import { addHandler, maybeInstrument, triggerHandlers } from './handlers'; import { debug } from '../utils/debug-logger'; -interface ConsoleInstrumentationOptions { - /** - * Filter out console messages that match the given strings or regular expressions. - * These will neither be passed to the handler, and they will also not be logged to the user, unless they have debug enabled. - */ - filter?: (string | RegExp)[]; -} - -let _options: ConsoleInstrumentationOptions = {}; +/** + * Filter out console messages that match the given strings or regular expressions. + * These will neither be passed to the handler, and they will also not be logged to the user, unless they have debug enabled. + * This is a set to avoid duplicate integration setups to add the same filter multiple times. +*/ +const _filter = new Set([]); /** * Add an instrumentation handler for when a console.xxx method is called. @@ -34,15 +31,14 @@ export function addConsoleInstrumentationHandler(handler: (data: HandlerDataCons } export function addConsoleInstrumentationFilter(filter: (string | RegExp)[]): void { - _options = { - ..._options, - filter: [...(_options.filter || []), ...filter], - }; + for (const f of filter) { + _filter.add(f); + } } /** Only exported for tests. */ export function _INTERNAL_resetConsoleInstrumentationOptions(): void { - _options = {}; + _filter.clear(); } function instrumentConsole(): void { From fd4296d96eedc0658689b2ebf54f3916f168cb81 Mon Sep 17 00:00:00 2001 From: Francesco Gringl-Novy Date: Mon, 11 May 2026 12:23:32 +0200 Subject: [PATCH 6/9] add cleanup --- packages/core/src/instrument/console.ts | 14 ++++++++++++-- packages/core/src/integrations/console.ts | 6 +++--- 2 files changed, 15 insertions(+), 5 deletions(-) diff --git a/packages/core/src/instrument/console.ts b/packages/core/src/instrument/console.ts index 43780a732cec..0f883f932d47 100644 --- a/packages/core/src/instrument/console.ts +++ b/packages/core/src/instrument/console.ts @@ -13,7 +13,7 @@ import { debug } from '../utils/debug-logger'; * Filter out console messages that match the given strings or regular expressions. * These will neither be passed to the handler, and they will also not be logged to the user, unless they have debug enabled. * This is a set to avoid duplicate integration setups to add the same filter multiple times. -*/ + */ const _filter = new Set([]); /** @@ -30,10 +30,20 @@ export function addConsoleInstrumentationHandler(handler: (data: HandlerDataCons return removeHandler; } -export function addConsoleInstrumentationFilter(filter: (string | RegExp)[]): void { +/** + * Add a filter to the console instrumentation to filter out console messages that match the given strings or regular expressions. + * Returns a function to remove the filter. + */ +export function addConsoleInstrumentationFilter(filter: (string | RegExp)[]): () => void { for (const f of filter) { _filter.add(f); } + + return () => { + for (const f of filter) { + _filter.delete(f); + } + }; } /** Only exported for tests. */ diff --git a/packages/core/src/integrations/console.ts b/packages/core/src/integrations/console.ts index 9f672e2ba2c3..21e1ee53d328 100644 --- a/packages/core/src/integrations/console.ts +++ b/packages/core/src/integrations/console.ts @@ -53,12 +53,12 @@ export const consoleIntegration = defineIntegration((options: Partial Date: Mon, 11 May 2026 12:29:14 +0200 Subject: [PATCH 7/9] fix it --- packages/core/src/instrument/console.ts | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/packages/core/src/instrument/console.ts b/packages/core/src/instrument/console.ts index 0f883f932d47..2670232ed88e 100644 --- a/packages/core/src/instrument/console.ts +++ b/packages/core/src/instrument/console.ts @@ -67,9 +67,8 @@ function instrumentConsole(): void { return function (...args: any[]): void { const firstArg = args[0]; const log = originalConsoleMethods[level]; - const filter = _options?.filter; - const isFiltered = filter && typeof firstArg === 'string' && stringMatchesSomePattern(firstArg, filter); + const isFiltered = _filter.size && typeof firstArg === 'string' && stringMatchesSomePattern(firstArg, Array.from(_filter)); // Only trigger handlers for non-filtered messages if (!isFiltered) { From 811742cc5891a9549b8afb2c212adb3506cad82f Mon Sep 17 00:00:00 2001 From: Francesco Gringl-Novy Date: Mon, 11 May 2026 12:33:16 +0200 Subject: [PATCH 8/9] fix linting --- packages/core/src/instrument/console.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/packages/core/src/instrument/console.ts b/packages/core/src/instrument/console.ts index 2670232ed88e..e9f2e25dae18 100644 --- a/packages/core/src/instrument/console.ts +++ b/packages/core/src/instrument/console.ts @@ -68,7 +68,8 @@ function instrumentConsole(): void { const firstArg = args[0]; const log = originalConsoleMethods[level]; - const isFiltered = _filter.size && typeof firstArg === 'string' && stringMatchesSomePattern(firstArg, Array.from(_filter)); + const isFiltered = + _filter.size && typeof firstArg === 'string' && stringMatchesSomePattern(firstArg, Array.from(_filter)); // Only trigger handlers for non-filtered messages if (!isFiltered) { From 2af644752913b08d81896f2075fd4c9f54d0cdd0 Mon Sep 17 00:00:00 2001 From: Francesco Gringl-Novy Date: Mon, 11 May 2026 12:36:15 +0200 Subject: [PATCH 9/9] allow set for stringMatchesSomePattern --- packages/core/src/instrument/console.ts | 3 +-- packages/core/src/utils/string.ts | 11 +++++++++-- 2 files changed, 10 insertions(+), 4 deletions(-) diff --git a/packages/core/src/instrument/console.ts b/packages/core/src/instrument/console.ts index e9f2e25dae18..737e9fd907fa 100644 --- a/packages/core/src/instrument/console.ts +++ b/packages/core/src/instrument/console.ts @@ -68,8 +68,7 @@ function instrumentConsole(): void { const firstArg = args[0]; const log = originalConsoleMethods[level]; - const isFiltered = - _filter.size && typeof firstArg === 'string' && stringMatchesSomePattern(firstArg, Array.from(_filter)); + const isFiltered = _filter.size && typeof firstArg === 'string' && stringMatchesSomePattern(firstArg, _filter); // Only trigger handlers for non-filtered messages if (!isFiltered) { diff --git a/packages/core/src/utils/string.ts b/packages/core/src/utils/string.ts index df548370823f..77e295b4237d 100644 --- a/packages/core/src/utils/string.ts +++ b/packages/core/src/utils/string.ts @@ -136,8 +136,15 @@ export function isMatchingPattern( */ export function stringMatchesSomePattern( testString: string, - patterns: Array boolean)> = [], + patterns: + | Array boolean)> + | Set boolean)> = [], requireExactStringMatch: boolean = false, ): boolean { - return patterns.some(pattern => isMatchingPattern(testString, pattern, requireExactStringMatch)); + for (const pattern of patterns) { + if (isMatchingPattern(testString, pattern, requireExactStringMatch)) { + return true; + } + } + return false; }