diff --git a/.size-limit.js b/.size-limit.js index 685b40b00fbe..77e60133b748 100644 --- a/.size-limit.js +++ b/.size-limit.js @@ -84,6 +84,13 @@ module.exports = [ gzip: true, limit: '95 KB', }, + { + name: '@sentry/browser (incl. Logs)', + path: 'packages/browser/build/npm/esm/index.js', + import: createImport('init', 'logger'), + gzip: true, + limit: '26 KB', + }, { name: '@sentry/browser (incl. Feedback)', path: 'packages/browser/build/npm/esm/index.js', diff --git a/packages/browser/src/log.ts b/packages/browser/src/log.ts index ef2614b81f55..de7d15e4b3ad 100644 --- a/packages/browser/src/log.ts +++ b/packages/browser/src/log.ts @@ -1,4 +1,4 @@ -import type { Log, LogSeverityLevel, ParameterizedString } from '@sentry/core'; +import type { Client, Log, LogSeverityLevel, ParameterizedString, Scope } from '@sentry/core'; import { _INTERNAL_captureLog } from '@sentry/core'; /** @@ -7,15 +7,15 @@ import { _INTERNAL_captureLog } from '@sentry/core'; * @param level - The level of the log. * @param message - The message to log. * @param attributes - Arbitrary structured data that stores information about the log - e.g., userId: 100. - * @param severityNumber - The severity number of the log. */ function captureLog( level: LogSeverityLevel, message: ParameterizedString, attributes?: Log['attributes'], - severityNumber?: Log['severityNumber'], + client?: Client, + scope?: Scope, ): void { - _INTERNAL_captureLog({ level, message, attributes, severityNumber }); + _INTERNAL_captureLog({ level, message, attributes }, client, scope); } /** @@ -43,8 +43,13 @@ function captureLog( * }); * ``` */ -export function trace(message: ParameterizedString, attributes?: Log['attributes']): void { - captureLog('trace', message, attributes); +export function trace( + message: ParameterizedString, + attributes?: Log['attributes'], + client?: Client, + scope?: Scope, +): void { + captureLog('trace', message, attributes, client, scope); } /** @@ -73,8 +78,13 @@ export function trace(message: ParameterizedString, attributes?: Log['attributes * }); * ``` */ -export function debug(message: ParameterizedString, attributes?: Log['attributes']): void { - captureLog('debug', message, attributes); +export function debug( + message: ParameterizedString, + attributes?: Log['attributes'], + client?: Client, + scope?: Scope, +): void { + captureLog('debug', message, attributes, client, scope); } /** @@ -103,8 +113,13 @@ export function debug(message: ParameterizedString, attributes?: Log['attributes * }); * ``` */ -export function info(message: ParameterizedString, attributes?: Log['attributes']): void { - captureLog('info', message, attributes); +export function info( + message: ParameterizedString, + attributes?: Log['attributes'], + client?: Client, + scope?: Scope, +): void { + captureLog('info', message, attributes, client, scope); } /** @@ -134,8 +149,13 @@ export function info(message: ParameterizedString, attributes?: Log['attributes' * }); * ``` */ -export function warn(message: ParameterizedString, attributes?: Log['attributes']): void { - captureLog('warn', message, attributes); +export function warn( + message: ParameterizedString, + attributes?: Log['attributes'], + client?: Client, + scope?: Scope, +): void { + captureLog('warn', message, attributes, client, scope); } /** @@ -166,8 +186,13 @@ export function warn(message: ParameterizedString, attributes?: Log['attributes' * }); * ``` */ -export function error(message: ParameterizedString, attributes?: Log['attributes']): void { - captureLog('error', message, attributes); +export function error( + message: ParameterizedString, + attributes?: Log['attributes'], + client?: Client, + scope?: Scope, +): void { + captureLog('error', message, attributes, client, scope); } /** @@ -198,8 +223,13 @@ export function error(message: ParameterizedString, attributes?: Log['attributes * }); * ``` */ -export function fatal(message: ParameterizedString, attributes?: Log['attributes']): void { - captureLog('fatal', message, attributes); +export function fatal( + message: ParameterizedString, + attributes?: Log['attributes'], + client?: Client, + scope?: Scope, +): void { + captureLog('fatal', message, attributes, client, scope); } export { fmt } from '@sentry/core'; diff --git a/packages/browser/test/log.test.ts b/packages/browser/test/log.test.ts index 68c87069966c..02491462de46 100644 --- a/packages/browser/test/log.test.ts +++ b/packages/browser/test/log.test.ts @@ -5,7 +5,8 @@ import * as sentryCore from '@sentry/core'; import { getCurrentScope, getGlobalScope, getIsolationScope } from '@sentry/core'; import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; -import { init, logger } from '../src'; +import { BrowserClient, init, logger } from '../src'; +import { getDefaultBrowserClientOptions } from './helper/browser-client-options'; import { makeSimpleTransport } from './mocks/simpletransport'; const dsn = 'https://53039209a22b4ec1bcc296a3c9fdecd6@sentry.io/4291'; @@ -66,87 +67,132 @@ describe('Logger', () => { it('should call _INTERNAL_captureLog with trace level', () => { logger.trace('Test trace message', { key: 'value' }); - expect(mockCaptureLog).toHaveBeenCalledWith({ - level: 'trace', - message: 'Test trace message', - attributes: { key: 'value' }, - severityNumber: undefined, - }); + expect(mockCaptureLog).toHaveBeenCalledWith( + { + level: 'trace', + message: 'Test trace message', + attributes: { key: 'value' }, + }, + undefined, + undefined, + ); }); it('should call _INTERNAL_captureLog with debug level', () => { logger.debug('Test debug message', { key: 'value' }); - expect(mockCaptureLog).toHaveBeenCalledWith({ - level: 'debug', - message: 'Test debug message', - attributes: { key: 'value' }, - severityNumber: undefined, - }); + expect(mockCaptureLog).toHaveBeenCalledWith( + { + level: 'debug', + message: 'Test debug message', + attributes: { key: 'value' }, + }, + undefined, + undefined, + ); }); it('should call _INTERNAL_captureLog with info level', () => { logger.info('Test info message', { key: 'value' }); - expect(mockCaptureLog).toHaveBeenCalledWith({ - level: 'info', - message: 'Test info message', - attributes: { key: 'value' }, - severityNumber: undefined, - }); + expect(mockCaptureLog).toHaveBeenCalledWith( + { + level: 'info', + message: 'Test info message', + attributes: { key: 'value' }, + }, + undefined, + undefined, + ); }); it('should call _INTERNAL_captureLog with warn level', () => { logger.warn('Test warn message', { key: 'value' }); - expect(mockCaptureLog).toHaveBeenCalledWith({ - level: 'warn', - message: 'Test warn message', - attributes: { key: 'value' }, - severityNumber: undefined, - }); + expect(mockCaptureLog).toHaveBeenCalledWith( + { + level: 'warn', + message: 'Test warn message', + attributes: { key: 'value' }, + }, + undefined, + undefined, + ); }); it('should call _INTERNAL_captureLog with error level', () => { logger.error('Test error message', { key: 'value' }); - expect(mockCaptureLog).toHaveBeenCalledWith({ - level: 'error', - message: 'Test error message', - attributes: { key: 'value' }, - severityNumber: undefined, - }); + expect(mockCaptureLog).toHaveBeenCalledWith( + { + level: 'error', + message: 'Test error message', + attributes: { key: 'value' }, + }, + undefined, + undefined, + ); }); it('should call _INTERNAL_captureLog with fatal level', () => { logger.fatal('Test fatal message', { key: 'value' }); - expect(mockCaptureLog).toHaveBeenCalledWith({ - level: 'fatal', - message: 'Test fatal message', - attributes: { key: 'value' }, - severityNumber: undefined, - }); + expect(mockCaptureLog).toHaveBeenCalledWith( + { + level: 'fatal', + message: 'Test fatal message', + attributes: { key: 'value' }, + }, + undefined, + undefined, + ); }); }); it('should handle parameterized strings with parameters', () => { logger.info(logger.fmt`Hello ${'John'}, your balance is ${100}`, { userId: 123 }); - expect(mockCaptureLog).toHaveBeenCalledWith({ - level: 'info', - message: expect.objectContaining({ - __sentry_template_string__: 'Hello %s, your balance is %s', - __sentry_template_values__: ['John', 100], - }), - attributes: { - userId: 123, + expect(mockCaptureLog).toHaveBeenCalledWith( + { + level: 'info', + message: expect.objectContaining({ + __sentry_template_string__: 'Hello %s, your balance is %s', + __sentry_template_values__: ['John', 100], + }), + attributes: { + userId: 123, + }, }, - }); + undefined, + undefined, + ); }); it('should handle parameterized strings without additional attributes', () => { logger.debug(logger.fmt`User ${'Alice'} logged in from ${'mobile'}`); - expect(mockCaptureLog).toHaveBeenCalledWith({ - level: 'debug', - message: expect.objectContaining({ - __sentry_template_string__: 'User %s logged in from %s', - __sentry_template_values__: ['Alice', 'mobile'], - }), - }); + expect(mockCaptureLog).toHaveBeenCalledWith( + { + level: 'debug', + message: expect.objectContaining({ + __sentry_template_string__: 'User %s logged in from %s', + __sentry_template_values__: ['Alice', 'mobile'], + }), + }, + undefined, + undefined, + ); }); + + it.each(['trace', 'debug', 'info', 'warn', 'error', 'fatal'] as const)( + 'should allow to pass a client and scope for %s level', + level => { + const client = new BrowserClient(getDefaultBrowserClientOptions()); + const scope = new sentryCore.Scope(); + + logger[level]('Test message', { key: 'value' }, client, scope); + expect(mockCaptureLog).toHaveBeenCalledWith( + { + level, + message: 'Test message', + attributes: { key: 'value' }, + }, + client, + scope, + ); + }, + ); }); diff --git a/packages/node-core/src/logs/capture.ts b/packages/node-core/src/logs/capture.ts index 17f94399f9bf..f3a67480d51b 100644 --- a/packages/node-core/src/logs/capture.ts +++ b/packages/node-core/src/logs/capture.ts @@ -1,10 +1,22 @@ import { format } from 'node:util'; -import type { Log, LogSeverityLevel, ParameterizedString } from '@sentry/core'; +import type { Client, Log, LogSeverityLevel, ParameterizedString, Scope } from '@sentry/core'; import { _INTERNAL_captureLog } from '@sentry/core'; -export type CaptureLogArgs = - | [message: ParameterizedString, attributes?: Log['attributes']] - | [messageTemplate: string, messageParams: Array, attributes?: Log['attributes']]; +type CaptureLogsArgsParametrized = [ + message: ParameterizedString, + attributes?: Log['attributes'], + client?: Client, + scope?: Scope, +]; +type CaptureLogsArgsTemplate = [ + messageTemplate: string, + messageParams: Array, + attributes?: Log['attributes'], + client?: Client, + scope?: Scope, +]; + +export type CaptureLogArgs = CaptureLogsArgsParametrized | CaptureLogsArgsTemplate; /** * Capture a log with the given level. @@ -14,16 +26,21 @@ export type CaptureLogArgs = * @param attributes - Arbitrary structured data that stores information about the log - e.g., userId: 100. */ export function captureLog(level: LogSeverityLevel, ...args: CaptureLogArgs): void { - const [messageOrMessageTemplate, paramsOrAttributes, maybeAttributes] = args; - if (Array.isArray(paramsOrAttributes)) { - const attributes = { ...maybeAttributes }; - attributes['sentry.message.template'] = messageOrMessageTemplate; - paramsOrAttributes.forEach((param, index) => { + if (isTemplateArgs(args)) { + const [messageTemplate, messageParams, messageAttributes, client, scope] = args; + const attributes = { ...messageAttributes }; + attributes['sentry.message.template'] = messageTemplate; + messageParams.forEach((param, index) => { attributes[`sentry.message.parameter.${index}`] = param; }); - const message = format(messageOrMessageTemplate, ...paramsOrAttributes); - _INTERNAL_captureLog({ level, message, attributes }); + const message = format(messageTemplate, ...messageParams); + _INTERNAL_captureLog({ level, message, attributes }, client, scope); } else { - _INTERNAL_captureLog({ level, message: messageOrMessageTemplate, attributes: paramsOrAttributes }); + const [message, attributes, client, scope] = args; + _INTERNAL_captureLog({ level, message, attributes }, client, scope); } } + +function isTemplateArgs(args: CaptureLogArgs): args is CaptureLogsArgsTemplate { + return Array.isArray(args[1]); +} diff --git a/packages/node-core/test/logs/exports.test.ts b/packages/node-core/test/logs/exports.test.ts index 9e1cc4900e29..97e57b7b42d0 100644 --- a/packages/node-core/test/logs/exports.test.ts +++ b/packages/node-core/test/logs/exports.test.ts @@ -1,6 +1,8 @@ import * as sentryCore from '@sentry/core'; import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; +import { NodeClient } from '../../src'; import * as nodeLogger from '../../src/logs/exports'; +import { getDefaultNodeClientOptions } from '../helpers/getDefaultNodeClientOptions'; // Mock the core functions vi.mock('@sentry/core', async () => { @@ -36,110 +38,212 @@ describe('Node Logger', () => { it('should call _INTERNAL_captureLog with trace level', () => { nodeLogger.trace('Test trace message', { key: 'value' }); - expect(mockCaptureLog).toHaveBeenCalledWith({ - level: 'trace', - message: 'Test trace message', - attributes: { key: 'value' }, - }); + expect(mockCaptureLog).toHaveBeenCalledWith( + { + level: 'trace', + message: 'Test trace message', + attributes: { key: 'value' }, + }, + undefined, + undefined, + ); }); it('should call _INTERNAL_captureLog with debug level', () => { nodeLogger.debug('Test debug message', { key: 'value' }); - expect(mockCaptureLog).toHaveBeenCalledWith({ - level: 'debug', - message: 'Test debug message', - attributes: { key: 'value' }, - }); + expect(mockCaptureLog).toHaveBeenCalledWith( + { + level: 'debug', + message: 'Test debug message', + attributes: { key: 'value' }, + }, + undefined, + undefined, + ); }); it('should call _INTERNAL_captureLog with info level', () => { nodeLogger.info('Test info message', { key: 'value' }); - expect(mockCaptureLog).toHaveBeenCalledWith({ - level: 'info', - message: 'Test info message', - attributes: { key: 'value' }, - }); + expect(mockCaptureLog).toHaveBeenCalledWith( + { + level: 'info', + message: 'Test info message', + attributes: { key: 'value' }, + }, + undefined, + undefined, + ); }); it('should call _INTERNAL_captureLog with warn level', () => { nodeLogger.warn('Test warn message', { key: 'value' }); - expect(mockCaptureLog).toHaveBeenCalledWith({ - level: 'warn', - message: 'Test warn message', - attributes: { key: 'value' }, - }); + expect(mockCaptureLog).toHaveBeenCalledWith( + { + level: 'warn', + message: 'Test warn message', + attributes: { key: 'value' }, + }, + undefined, + undefined, + ); }); it('should call _INTERNAL_captureLog with error level', () => { nodeLogger.error('Test error message', { key: 'value' }); - expect(mockCaptureLog).toHaveBeenCalledWith({ - level: 'error', - message: 'Test error message', - attributes: { key: 'value' }, - }); + expect(mockCaptureLog).toHaveBeenCalledWith( + { + level: 'error', + message: 'Test error message', + attributes: { key: 'value' }, + }, + undefined, + undefined, + ); }); it('should call _INTERNAL_captureLog with fatal level', () => { nodeLogger.fatal('Test fatal message', { key: 'value' }); - expect(mockCaptureLog).toHaveBeenCalledWith({ - level: 'fatal', - message: 'Test fatal message', - attributes: { key: 'value' }, - }); + expect(mockCaptureLog).toHaveBeenCalledWith( + { + level: 'fatal', + message: 'Test fatal message', + attributes: { key: 'value' }, + }, + undefined, + undefined, + ); }); }); describe('Template string logging', () => { it('should handle template strings with parameters', () => { nodeLogger.info('Hello %s, your balance is %d', ['John', 100], { userId: 123 }); - expect(mockCaptureLog).toHaveBeenCalledWith({ - level: 'info', - message: 'Hello John, your balance is 100', - attributes: { - userId: 123, - 'sentry.message.template': 'Hello %s, your balance is %d', - 'sentry.message.parameter.0': 'John', - 'sentry.message.parameter.1': 100, + expect(mockCaptureLog).toHaveBeenCalledWith( + { + level: 'info', + message: 'Hello John, your balance is 100', + attributes: { + userId: 123, + 'sentry.message.template': 'Hello %s, your balance is %d', + 'sentry.message.parameter.0': 'John', + 'sentry.message.parameter.1': 100, + }, }, - }); + undefined, + undefined, + ); }); it('should handle template strings without additional attributes', () => { nodeLogger.debug('User %s logged in from %s', ['Alice', 'mobile']); - 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', + 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', + }, }, - }); + undefined, + undefined, + ); }); it('should handle parameterized strings with parameters', () => { nodeLogger.info(nodeLogger.fmt`Hello ${'John'}, your balance is ${100}`, { userId: 123 }); - expect(mockCaptureLog).toHaveBeenCalledWith({ - level: 'info', - message: expect.objectContaining({ - __sentry_template_string__: 'Hello %s, your balance is %s', - __sentry_template_values__: ['John', 100], - }), - attributes: { - userId: 123, + expect(mockCaptureLog).toHaveBeenCalledWith( + { + level: 'info', + message: expect.objectContaining({ + __sentry_template_string__: 'Hello %s, your balance is %s', + __sentry_template_values__: ['John', 100], + }), + attributes: { + userId: 123, + }, }, - }); + undefined, + undefined, + ); }); it('should handle parameterized strings without additional attributes', () => { nodeLogger.debug(nodeLogger.fmt`User ${'Alice'} logged in from ${'mobile'}`); - expect(mockCaptureLog).toHaveBeenCalledWith({ - level: 'debug', - message: expect.objectContaining({ - __sentry_template_string__: 'User %s logged in from %s', - __sentry_template_values__: ['Alice', 'mobile'], - }), - }); + expect(mockCaptureLog).toHaveBeenCalledWith( + { + level: 'debug', + message: expect.objectContaining({ + __sentry_template_string__: 'User %s logged in from %s', + __sentry_template_values__: ['Alice', 'mobile'], + }), + }, + undefined, + undefined, + ); }); }); + + describe.each(['trace', 'debug', 'info', 'warn', 'error', 'fatal'] as const)( + 'passing client and scope for %s level', + level => { + it('should allow to pass a client and scope with plain message', () => { + const client = new NodeClient(getDefaultNodeClientOptions()); + const scope = new sentryCore.Scope(); + + nodeLogger[level]('Test message', { key: 'value' }, client, scope); + expect(mockCaptureLog).toHaveBeenCalledWith( + { + level, + message: 'Test message', + attributes: { key: 'value' }, + }, + client, + scope, + ); + }); + + it('should allow to pass a client and scope with parametrized string', () => { + const client = new NodeClient(getDefaultNodeClientOptions()); + const scope = new sentryCore.Scope(); + + nodeLogger[level](nodeLogger.fmt`Hello ${'John'}, your balance is ${100}`, { userId: 123 }, client, scope); + expect(mockCaptureLog).toHaveBeenCalledWith( + { + level, + message: expect.objectContaining({ + __sentry_template_string__: 'Hello %s, your balance is %s', + __sentry_template_values__: ['John', 100], + }), + attributes: { + userId: 123, + }, + }, + client, + scope, + ); + }); + + it('should allow to pass a client and scope with template string', () => { + const client = new NodeClient(getDefaultNodeClientOptions()); + const scope = new sentryCore.Scope(); + + nodeLogger[level]('User %s logged in from %s', ['Alice', 'mobile'], {}, client, scope); + expect(mockCaptureLog).toHaveBeenCalledWith( + { + level, + 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', + }, + }, + client, + scope, + ); + }); + }, + ); });