diff --git a/dev-packages/node-core-integration-tests/suites/public-api/logs/subject.ts b/dev-packages/node-core-integration-tests/suites/public-api/logs/subject.ts new file mode 100644 index 000000000000..7a1bc01025d8 --- /dev/null +++ b/dev-packages/node-core-integration-tests/suites/public-api/logs/subject.ts @@ -0,0 +1,27 @@ +import * as Sentry from '@sentry/node-core'; +import { loggingTransport } from '@sentry-internal/node-integration-tests'; + +const client = new Sentry.NodeClient({ + dsn: 'https://public@dsn.ingest.sentry.io/1337', + transport: loggingTransport, + stackParser: Sentry.defaultStackParser, + integrations: [], + enableLogs: true, + sendDefaultPii: true, +}); + +const customScope = new Sentry.Scope(); +customScope.setClient(client); +customScope.update({ user: { username: 'h4cktor' } }); +client.init(); + +async function run(): Promise { + Sentry.logger.info('test info', { foo: 'bar1' }, { scope: customScope }); + Sentry.logger.info('test info with %d', [1], { foo: 'bar2' }, { scope: customScope }); + Sentry.logger.info(Sentry.logger.fmt`test info with fmt ${1}`, { foo: 'bar3' }, { scope: customScope }); + + await Sentry.flush(); +} + +// eslint-disable-next-line @typescript-eslint/no-floating-promises +void run(); diff --git a/dev-packages/node-core-integration-tests/suites/public-api/logs/test.ts b/dev-packages/node-core-integration-tests/suites/public-api/logs/test.ts new file mode 100644 index 000000000000..9469308c1ed6 --- /dev/null +++ b/dev-packages/node-core-integration-tests/suites/public-api/logs/test.ts @@ -0,0 +1,124 @@ +import { afterAll, describe, expect, test } from 'vitest'; +import { cleanupChildProcesses, createRunner } from '../../../utils/runner'; + +describe('logger public API', () => { + afterAll(() => { + cleanupChildProcesses(); + }); + + test('captures logs with custom scopes and parameters in different forms', async () => { + const runner = createRunner(__dirname, 'subject.ts') + .expect({ + log: { + items: [ + { + attributes: { + foo: { + type: 'string', + value: 'bar1', + }, + 'sentry.sdk.name': { + type: 'string', + value: 'sentry.javascript.node', + }, + 'sentry.sdk.version': { + type: 'string', + value: expect.any(String), + }, + 'server.address': { + type: 'string', + value: 'M6QX4Q5HKV.local', + }, + 'user.name': { + type: 'string', + value: 'h4cktor', + }, + }, + body: 'test info', + level: 'info', + severity_number: 9, + timestamp: expect.any(Number), + trace_id: expect.stringMatching(/^[\da-f]{32}$/), + }, + { + attributes: { + foo: { + type: 'string', + value: 'bar2', + }, + 'sentry.message.parameter.0': { + type: 'integer', + value: 1, + }, + 'sentry.message.template': { + type: 'string', + value: 'test info with %d', + }, + 'sentry.sdk.name': { + type: 'string', + value: 'sentry.javascript.node', + }, + 'sentry.sdk.version': { + type: 'string', + value: expect.any(String), + }, + 'server.address': { + type: 'string', + value: 'M6QX4Q5HKV.local', + }, + 'user.name': { + type: 'string', + value: 'h4cktor', + }, + }, + body: 'test info with 1', + level: 'info', + severity_number: 9, + timestamp: expect.any(Number), + trace_id: expect.stringMatching(/^[\da-f]{32}$/), + }, + { + attributes: { + foo: { + type: 'string', + value: 'bar3', + }, + 'sentry.message.parameter.0': { + type: 'integer', + value: 1, + }, + 'sentry.message.template': { + type: 'string', + value: 'test info with fmt %s', + }, + 'sentry.sdk.name': { + type: 'string', + value: 'sentry.javascript.node', + }, + 'sentry.sdk.version': { + type: 'string', + value: expect.any(String), + }, + 'server.address': { + type: 'string', + value: 'M6QX4Q5HKV.local', + }, + 'user.name': { + type: 'string', + value: 'h4cktor', + }, + }, + body: 'test info with fmt 1', + level: 'info', + severity_number: 9, + timestamp: expect.any(Number), + trace_id: expect.stringMatching(/^[\da-f]{32}$/), + }, + ], + }, + }) + .start(); + + await runner.completed(); + }); +}); diff --git a/packages/node-core/src/logs/capture.ts b/packages/node-core/src/logs/capture.ts index 4c2fdc73a34c..8c77186785ff 100644 --- a/packages/node-core/src/logs/capture.ts +++ b/packages/node-core/src/logs/capture.ts @@ -34,7 +34,8 @@ export type CaptureLogArgs = CaptureLogArgWithTemplate | CaptureLogArgWithoutTem export function captureLog(level: LogSeverityLevel, ...args: CaptureLogArgs): void { const [messageOrMessageTemplate, paramsOrAttributes, maybeAttributesOrMetadata, maybeMetadata] = args; if (Array.isArray(paramsOrAttributes)) { - const attributes = { ...(maybeAttributesOrMetadata as Log['attributes']) }; + // type-casting here because from the type definitions we know that `maybeAttributesOrMetadata` is an attributes object (or undefined) + const attributes = { ...(maybeAttributesOrMetadata as Log['attributes'] | undefined) }; attributes['sentry.message.template'] = messageOrMessageTemplate; paramsOrAttributes.forEach((param, index) => { attributes[`sentry.message.parameter.${index}`] = param; @@ -44,7 +45,8 @@ export function captureLog(level: LogSeverityLevel, ...args: CaptureLogArgs): vo } else { _INTERNAL_captureLog( { level, message: messageOrMessageTemplate, attributes: paramsOrAttributes }, - maybeMetadata?.scope, + // type-casting here because from the type definitions we know that `maybeAttributesOrMetadata` is a metadata object (or undefined) + (maybeAttributesOrMetadata as CaptureLogMetadata | undefined)?.scope ?? maybeMetadata?.scope, ); } } diff --git a/packages/node-core/test/logs/exports.test.ts b/packages/node-core/test/logs/exports.test.ts index 45da1722abc8..782ba85ee2c0 100644 --- a/packages/node-core/test/logs/exports.test.ts +++ b/packages/node-core/test/logs/exports.test.ts @@ -1,4 +1,5 @@ import * as sentryCore from '@sentry/core'; +import { Scope } from '@sentry/core'; import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; import * as nodeLogger from '../../src/logs/exports'; @@ -172,4 +173,50 @@ describe('Node Logger', () => { ); }); }); + + describe('scustom cope', () => { + it('calls _INTERNAL_captureLog with custom scope for basic log message', () => { + const customScope = new Scope(); + nodeLogger.debug('User logged in', undefined, { scope: customScope }); + expect(mockCaptureLog).toHaveBeenCalledWith( + { + level: 'debug', + message: 'User logged in', + }, + customScope, + ); + }); + + it('calls _INTERNAL_captureLog with custom scope for parametrized log message', () => { + const customScope = new Scope(); + nodeLogger.debug('User %s logged in from %s', ['Alice', 'mobile'], undefined, { scope: customScope }); + expect(mockCaptureLog).toHaveBeenCalledWith( + { + level: 'debug', + message: 'User Alice logged in from mobile', + attributes: { + 'sentry.message.template': 'User %s logged in from %s', + 'sentry.message.parameter.0': 'Alice', + 'sentry.message.parameter.1': 'mobile', + }, + }, + customScope, + ); + }); + + it('calls _INTERNAL_captureLog with custom scope for fmt log message', () => { + const customScope = new Scope(); + nodeLogger.debug(nodeLogger.fmt`User ${'Alice'} logged in from ${'mobile'}`, undefined, { scope: customScope }); + expect(mockCaptureLog).toHaveBeenCalledWith( + { + level: 'debug', + message: expect.objectContaining({ + __sentry_template_string__: 'User %s logged in from %s', + __sentry_template_values__: ['Alice', 'mobile'], + }), + }, + customScope, + ); + }); + }); });