From 8ef1584b6dabf71bd080e509973cf564457a2018 Mon Sep 17 00:00:00 2001 From: JPeer264 Date: Wed, 21 Jan 2026 14:34:36 +0100 Subject: [PATCH 1/4] feat(winston): Add customLevelMap for winston transport --- .../suites/winston/subject.ts | 38 ++++++++ .../suites/winston/test.ts | 91 +++++++++++++++++++ .../node-core/src/integrations/winston.ts | 20 +++- 3 files changed, 148 insertions(+), 1 deletion(-) diff --git a/dev-packages/node-integration-tests/suites/winston/subject.ts b/dev-packages/node-integration-tests/suites/winston/subject.ts index 1047f2f1cd47..16d47bb99404 100644 --- a/dev-packages/node-integration-tests/suites/winston/subject.ts +++ b/dev-packages/node-integration-tests/suites/winston/subject.ts @@ -64,6 +64,44 @@ async function run(): Promise { }); } + // If custom level mapping is requested + if (process.env.CUSTOM_LEVEL_MAPPING === 'true') { + const customLevels = { + levels: { + customCritical: 0, + customWarning: 1, + customNotice: 2, + }, + }; + + const SentryWinstonTransport = Sentry.createSentryWinstonTransport(Transport, { + customLevelMap: { + customCritical: 'fatal', + customWarning: 'warn', + customNotice: 'info', + }, + }); + + const mappedLogger = winston.createLogger({ + levels: customLevels.levels, + // https://github.com/winstonjs/winston/issues/1491 + // when custom levels are set with a transport, + // the level must be set on the logger + level: 'customNotice', + transports: [new SentryWinstonTransport()], + }); + + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-expect-error - custom levels are not part of the winston logger + mappedLogger.customCritical('This is a critical message'); + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-expect-error - custom levels are not part of the winston logger + mappedLogger.customWarning('This is a warning message'); + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-expect-error - custom levels are not part of the winston logger + mappedLogger.customNotice('This is a notice message'); + } + await Sentry.flush(); } diff --git a/dev-packages/node-integration-tests/suites/winston/test.ts b/dev-packages/node-integration-tests/suites/winston/test.ts index 777b1149c871..8b1b77c8d8b3 100644 --- a/dev-packages/node-integration-tests/suites/winston/test.ts +++ b/dev-packages/node-integration-tests/suites/winston/test.ts @@ -183,4 +183,95 @@ describe('winston integration', () => { await runner.completed(); }); + + test('should map custom winston levels to Sentry severity levels', async () => { + const runner = createRunner(__dirname, 'subject.ts') + .withEnv({ CUSTOM_LEVEL_MAPPING: 'true' }) + .expect({ + log: { + items: [ + // First, the default logger captures info and error + { + timestamp: expect.any(Number), + level: 'info', + body: 'Test info message', + severity_number: expect.any(Number), + trace_id: expect.any(String), + attributes: { + 'sentry.origin': { value: 'auto.log.winston', type: 'string' }, + 'sentry.release': { value: '1.0.0', type: 'string' }, + 'sentry.environment': { value: 'test', type: 'string' }, + 'sentry.sdk.name': { value: 'sentry.javascript.node', type: 'string' }, + 'sentry.sdk.version': { value: expect.any(String), type: 'string' }, + 'server.address': { value: expect.any(String), type: 'string' }, + }, + }, + { + timestamp: expect.any(Number), + level: 'error', + body: 'Test error message', + severity_number: expect.any(Number), + trace_id: expect.any(String), + attributes: { + 'sentry.origin': { value: 'auto.log.winston', type: 'string' }, + 'sentry.release': { value: '1.0.0', type: 'string' }, + 'sentry.environment': { value: 'test', type: 'string' }, + 'sentry.sdk.name': { value: 'sentry.javascript.node', type: 'string' }, + 'sentry.sdk.version': { value: expect.any(String), type: 'string' }, + 'server.address': { value: expect.any(String), type: 'string' }, + }, + }, + // Then the mapped logger uses custom level mappings + { + timestamp: expect.any(Number), + level: 'fatal', // 'critical' maps to 'fatal' + body: 'This is a critical message', + severity_number: expect.any(Number), + trace_id: expect.any(String), + attributes: { + 'sentry.origin': { value: 'auto.log.winston', type: 'string' }, + 'sentry.release': { value: '1.0.0', type: 'string' }, + 'sentry.environment': { value: 'test', type: 'string' }, + 'sentry.sdk.name': { value: 'sentry.javascript.node', type: 'string' }, + 'sentry.sdk.version': { value: expect.any(String), type: 'string' }, + 'server.address': { value: expect.any(String), type: 'string' }, + }, + }, + { + timestamp: expect.any(Number), + level: 'warn', // 'warning' maps to 'warn' + body: 'This is a warning message', + severity_number: expect.any(Number), + trace_id: expect.any(String), + attributes: { + 'sentry.origin': { value: 'auto.log.winston', type: 'string' }, + 'sentry.release': { value: '1.0.0', type: 'string' }, + 'sentry.environment': { value: 'test', type: 'string' }, + 'sentry.sdk.name': { value: 'sentry.javascript.node', type: 'string' }, + 'sentry.sdk.version': { value: expect.any(String), type: 'string' }, + 'server.address': { value: expect.any(String), type: 'string' }, + }, + }, + { + timestamp: expect.any(Number), + level: 'info', // 'notice' maps to 'info' + body: 'This is a notice message', + severity_number: expect.any(Number), + trace_id: expect.any(String), + attributes: { + 'sentry.origin': { value: 'auto.log.winston', type: 'string' }, + 'sentry.release': { value: '1.0.0', type: 'string' }, + 'sentry.environment': { value: 'test', type: 'string' }, + 'sentry.sdk.name': { value: 'sentry.javascript.node', type: 'string' }, + 'sentry.sdk.version': { value: expect.any(String), type: 'string' }, + 'server.address': { value: expect.any(String), type: 'string' }, + }, + }, + ], + }, + }) + .start(); + + await runner.completed(); + }); }); diff --git a/packages/node-core/src/integrations/winston.ts b/packages/node-core/src/integrations/winston.ts index bea0fa584bf7..d5024e23710b 100644 --- a/packages/node-core/src/integrations/winston.ts +++ b/packages/node-core/src/integrations/winston.ts @@ -25,6 +25,21 @@ interface WinstonTransportOptions { * ``` */ levels?: Array; + + /** + * Use this option to map custom levels to Sentry log severity levels. + * + * @example + * ```ts + * const SentryWinstonTransport = Sentry.createSentryWinstonTransport(Transport, { + * customLevelMap: { + * myCustomLevel: 'info', + * customError: 'error', + * }, + * }); + * ``` + */ + customLevelMap?: Record; } /** @@ -85,7 +100,10 @@ export function createSentryWinstonTransport Date: Mon, 26 Jan 2026 13:01:34 +0100 Subject: [PATCH 2/4] fixup! feat(winston): Add customLevelMap for winston transport --- .../suites/winston/subject.ts | 30 +++++++++ .../suites/winston/test.ts | 67 +++++++++++++++++++ .../node-core/src/integrations/winston.ts | 4 ++ 3 files changed, 101 insertions(+) diff --git a/dev-packages/node-integration-tests/suites/winston/subject.ts b/dev-packages/node-integration-tests/suites/winston/subject.ts index 16d47bb99404..9de27a3e24e9 100644 --- a/dev-packages/node-integration-tests/suites/winston/subject.ts +++ b/dev-packages/node-integration-tests/suites/winston/subject.ts @@ -9,6 +9,7 @@ Sentry.init({ environment: 'test', enableLogs: true, transport: loggingTransport, + debug: true, }); async function run(): Promise { @@ -64,6 +65,35 @@ async function run(): Promise { }); } + // If unmapped custom level is requested (tests debug line for unknown levels) + if (process.env.UNMAPPED_CUSTOM_LEVEL === 'true') { + const customLevels = { + levels: { + myUnknownLevel: 0, + error: 1, + }, + }; + + // Create transport WITHOUT customLevelMap for myUnknownLevel + // myUnknownLevel will default to 'info', but we only capture 'error' + const UnmappedSentryWinstonTransport = Sentry.createSentryWinstonTransport(Transport, { + levels: ['error'], + }); + + const unmappedLogger = winston.createLogger({ + levels: customLevels.levels, + level: 'error', + transports: [new UnmappedSentryWinstonTransport()], + }); + + // This should NOT be captured (unknown level defaults to 'info', which is not in levels) + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-expect-error - custom levels are not part of the winston logger + unmappedLogger.myUnknownLevel('This unknown level message should be skipped'); + // This SHOULD be captured + unmappedLogger.error('This error message should be captured'); + } + // If custom level mapping is requested if (process.env.CUSTOM_LEVEL_MAPPING === 'true') { const customLevels = { diff --git a/dev-packages/node-integration-tests/suites/winston/test.ts b/dev-packages/node-integration-tests/suites/winston/test.ts index 8b1b77c8d8b3..c54858e0174e 100644 --- a/dev-packages/node-integration-tests/suites/winston/test.ts +++ b/dev-packages/node-integration-tests/suites/winston/test.ts @@ -184,6 +184,73 @@ describe('winston integration', () => { await runner.completed(); }); + test('should skip unmapped custom levels when not in the levels option', async () => { + const runner = createRunner(__dirname, 'subject.ts') + .withEnv({ UNMAPPED_CUSTOM_LEVEL: 'true' }) + .expect({ + log: { + items: [ + // First, the default logger captures info and error + { + timestamp: expect.any(Number), + level: 'info', + body: 'Test info message', + severity_number: expect.any(Number), + trace_id: expect.any(String), + attributes: { + 'sentry.origin': { value: 'auto.log.winston', type: 'string' }, + 'sentry.release': { value: '1.0.0', type: 'string' }, + 'sentry.environment': { value: 'test', type: 'string' }, + 'sentry.sdk.name': { value: 'sentry.javascript.node', type: 'string' }, + 'sentry.sdk.version': { value: expect.any(String), type: 'string' }, + 'server.address': { value: expect.any(String), type: 'string' }, + }, + }, + { + timestamp: expect.any(Number), + level: 'error', + body: 'Test error message', + severity_number: expect.any(Number), + trace_id: expect.any(String), + attributes: { + 'sentry.origin': { value: 'auto.log.winston', type: 'string' }, + 'sentry.release': { value: '1.0.0', type: 'string' }, + 'sentry.environment': { value: 'test', type: 'string' }, + 'sentry.sdk.name': { value: 'sentry.javascript.node', type: 'string' }, + 'sentry.sdk.version': { value: expect.any(String), type: 'string' }, + 'server.address': { value: expect.any(String), type: 'string' }, + }, + }, + // Then the unmapped logger only captures error (myUnknownLevel defaults to info, which is skipped) + { + timestamp: expect.any(Number), + level: 'error', + body: 'This error message should be captured', + severity_number: expect.any(Number), + trace_id: expect.any(String), + attributes: { + 'sentry.origin': { value: 'auto.log.winston', type: 'string' }, + 'sentry.release': { value: '1.0.0', type: 'string' }, + 'sentry.environment': { value: 'test', type: 'string' }, + 'sentry.sdk.name': { value: 'sentry.javascript.node', type: 'string' }, + 'sentry.sdk.version': { value: expect.any(String), type: 'string' }, + 'server.address': { value: expect.any(String), type: 'string' }, + }, + }, + ], + }, + }) + .start(); + + await runner.completed(); + + const logs = runner.getLogs(); + + const warning = logs.find(log => log.includes('Winston log level myUnknownLevel is not captured by Sentry.')); + + expect(warning).toBeDefined(); + }); + test('should map custom winston levels to Sentry severity levels', async () => { const runner = createRunner(__dirname, 'subject.ts') .withEnv({ CUSTOM_LEVEL_MAPPING: 'true' }) diff --git a/packages/node-core/src/integrations/winston.ts b/packages/node-core/src/integrations/winston.ts index d5024e23710b..fdb083f0d4eb 100644 --- a/packages/node-core/src/integrations/winston.ts +++ b/packages/node-core/src/integrations/winston.ts @@ -1,5 +1,7 @@ /* eslint-disable @typescript-eslint/ban-ts-comment */ import type { LogSeverityLevel } from '@sentry/core'; +import { debug } from '@sentry/core'; +import { DEBUG_BUILD } from '../debug-build'; import { captureLog } from '../logs/capture'; const DEFAULT_CAPTURED_LEVELS: Array = ['trace', 'debug', 'info', 'warn', 'error', 'fatal']; @@ -109,6 +111,8 @@ export function createSentryWinstonTransport Date: Mon, 26 Jan 2026 13:44:52 +0100 Subject: [PATCH 3/4] fix: Switch to ts-ignore. ts-expect-error was introduced in TS3.9 --- .../node-integration-tests/suites/winston/subject.ts | 12 ++++-------- .../node-integration-tests/suites/winston/test.ts | 8 ++++---- packages/node-core/src/integrations/winston.ts | 5 ++++- 3 files changed, 12 insertions(+), 13 deletions(-) diff --git a/dev-packages/node-integration-tests/suites/winston/subject.ts b/dev-packages/node-integration-tests/suites/winston/subject.ts index 9de27a3e24e9..504ecb640a3f 100644 --- a/dev-packages/node-integration-tests/suites/winston/subject.ts +++ b/dev-packages/node-integration-tests/suites/winston/subject.ts @@ -87,8 +87,7 @@ async function run(): Promise { }); // This should NOT be captured (unknown level defaults to 'info', which is not in levels) - // eslint-disable-next-line @typescript-eslint/ban-ts-comment - // @ts-expect-error - custom levels are not part of the winston logger + // @ts-ignore - custom levels are not part of the winston logger unmappedLogger.myUnknownLevel('This unknown level message should be skipped'); // This SHOULD be captured unmappedLogger.error('This error message should be captured'); @@ -121,14 +120,11 @@ async function run(): Promise { transports: [new SentryWinstonTransport()], }); - // eslint-disable-next-line @typescript-eslint/ban-ts-comment - // @ts-expect-error - custom levels are not part of the winston logger + // @ts-ignore - custom levels are not part of the winston logger mappedLogger.customCritical('This is a critical message'); - // eslint-disable-next-line @typescript-eslint/ban-ts-comment - // @ts-expect-error - custom levels are not part of the winston logger + // @ts-ignore - custom levels are not part of the winston logger mappedLogger.customWarning('This is a warning message'); - // eslint-disable-next-line @typescript-eslint/ban-ts-comment - // @ts-expect-error - custom levels are not part of the winston logger + // @ts-ignore - custom levels are not part of the winston logger mappedLogger.customNotice('This is a notice message'); } diff --git a/dev-packages/node-integration-tests/suites/winston/test.ts b/dev-packages/node-integration-tests/suites/winston/test.ts index c54858e0174e..7383d7ae3672 100644 --- a/dev-packages/node-integration-tests/suites/winston/test.ts +++ b/dev-packages/node-integration-tests/suites/winston/test.ts @@ -242,13 +242,13 @@ describe('winston integration', () => { }) .start(); - await runner.completed(); + await runner.completed(); - const logs = runner.getLogs(); + const logs = runner.getLogs(); - const warning = logs.find(log => log.includes('Winston log level myUnknownLevel is not captured by Sentry.')); + const warning = logs.find(log => log.includes('Winston log level myUnknownLevel is not captured by Sentry.')); - expect(warning).toBeDefined(); + expect(warning).toBeDefined(); }); test('should map custom winston levels to Sentry severity levels', async () => { diff --git a/packages/node-core/src/integrations/winston.ts b/packages/node-core/src/integrations/winston.ts index fdb083f0d4eb..be7f8f59464e 100644 --- a/packages/node-core/src/integrations/winston.ts +++ b/packages/node-core/src/integrations/winston.ts @@ -112,7 +112,10 @@ export function createSentryWinstonTransport Date: Mon, 26 Jan 2026 15:04:46 +0100 Subject: [PATCH 4/4] fixup! feat(winston): Add customLevelMap for winston transport --- .../suites/winston/subject.ts | 12 ++++ .../suites/winston/test.ts | 65 +++++++++++++++++++ .../node-core/src/integrations/winston.ts | 6 +- 3 files changed, 80 insertions(+), 3 deletions(-) diff --git a/dev-packages/node-integration-tests/suites/winston/subject.ts b/dev-packages/node-integration-tests/suites/winston/subject.ts index 504ecb640a3f..2c9d88456cce 100644 --- a/dev-packages/node-integration-tests/suites/winston/subject.ts +++ b/dev-packages/node-integration-tests/suites/winston/subject.ts @@ -65,6 +65,18 @@ async function run(): Promise { }); } + if (process.env.WITH_FILTER === 'true') { + const FilteredSentryWinstonTransport = Sentry.createSentryWinstonTransport(Transport, { + levels: ['error'], + }); + const filteredLogger = winston.createLogger({ + transports: [new FilteredSentryWinstonTransport()], + }); + + filteredLogger.info('Ignored message'); + filteredLogger.error('Test error message'); + } + // If unmapped custom level is requested (tests debug line for unknown levels) if (process.env.UNMAPPED_CUSTOM_LEVEL === 'true') { const customLevels = { diff --git a/dev-packages/node-integration-tests/suites/winston/test.ts b/dev-packages/node-integration-tests/suites/winston/test.ts index 7383d7ae3672..1b359cc20f80 100644 --- a/dev-packages/node-integration-tests/suites/winston/test.ts +++ b/dev-packages/node-integration-tests/suites/winston/test.ts @@ -123,6 +123,71 @@ describe('winston integration', () => { await runner.completed(); }); + test("should capture winston logs with filter but don't show custom level warnings", async () => { + const runner = createRunner(__dirname, 'subject.ts') + .withEnv({ WITH_FILTER: 'true' }) + .expect({ + log: { + items: [ + { + timestamp: expect.any(Number), + level: 'info', + body: 'Test info message', + severity_number: expect.any(Number), + trace_id: expect.any(String), + attributes: { + 'sentry.origin': { value: 'auto.log.winston', type: 'string' }, + 'sentry.release': { value: '1.0.0', type: 'string' }, + 'sentry.environment': { value: 'test', type: 'string' }, + 'sentry.sdk.name': { value: 'sentry.javascript.node', type: 'string' }, + 'sentry.sdk.version': { value: expect.any(String), type: 'string' }, + 'server.address': { value: expect.any(String), type: 'string' }, + }, + }, + { + timestamp: expect.any(Number), + level: 'error', + body: 'Test error message', + severity_number: expect.any(Number), + trace_id: expect.any(String), + attributes: { + 'sentry.origin': { value: 'auto.log.winston', type: 'string' }, + 'sentry.release': { value: '1.0.0', type: 'string' }, + 'sentry.environment': { value: 'test', type: 'string' }, + 'sentry.sdk.name': { value: 'sentry.javascript.node', type: 'string' }, + 'sentry.sdk.version': { value: expect.any(String), type: 'string' }, + 'server.address': { value: expect.any(String), type: 'string' }, + }, + }, + { + timestamp: expect.any(Number), + level: 'error', + body: 'Test error message', + severity_number: expect.any(Number), + trace_id: expect.any(String), + attributes: { + 'sentry.origin': { value: 'auto.log.winston', type: 'string' }, + 'sentry.release': { value: '1.0.0', type: 'string' }, + 'sentry.environment': { value: 'test', type: 'string' }, + 'sentry.sdk.name': { value: 'sentry.javascript.node', type: 'string' }, + 'sentry.sdk.version': { value: expect.any(String), type: 'string' }, + 'server.address': { value: expect.any(String), type: 'string' }, + }, + }, + ], + }, + }) + .start(); + + await runner.completed(); + + const logs = runner.getLogs(); + + const warning = logs.find(log => log.includes('Winston log level info is not captured by Sentry.')); + + expect(warning).not.toBeDefined(); + }); + test('should capture winston logs with metadata', async () => { const runner = createRunner(__dirname, 'subject.ts') .withEnv({ WITH_METADATA: 'true' }) diff --git a/packages/node-core/src/integrations/winston.ts b/packages/node-core/src/integrations/winston.ts index be7f8f59464e..a461d8797338 100644 --- a/packages/node-core/src/integrations/winston.ts +++ b/packages/node-core/src/integrations/winston.ts @@ -103,15 +103,15 @@ export function createSentryWinstonTransport