diff --git a/dev-packages/browser-integration-tests/suites/public-api/logger/integration/subject.js b/dev-packages/browser-integration-tests/suites/public-api/logger/integration/subject.js index 6974f191b76b..1c8c628b1358 100644 --- a/dev-packages/browser-integration-tests/suites/public-api/logger/integration/subject.js +++ b/dev-packages/browser-integration-tests/suites/public-api/logger/integration/subject.js @@ -13,4 +13,12 @@ console.log('Mixed:', 'prefix', { obj: true }, [4, 5, 6], 'suffix'); console.log(''); +// Test console substitution patterns (should NOT generate template attributes) +console.log('String substitution %s %d', 'test', 42); +console.log('Object substitution %o', { key: 'value' }); + +// Test multiple arguments without substitutions (should generate template attributes) +console.log('first', 0, 1, 2); +console.log('hello', true, null, undefined); + Sentry.flush(); diff --git a/dev-packages/browser-integration-tests/suites/public-api/logger/integration/test.ts b/dev-packages/browser-integration-tests/suites/public-api/logger/integration/test.ts index 7561b76e8b72..442800456f9b 100644 --- a/dev-packages/browser-integration-tests/suites/public-api/logger/integration/test.ts +++ b/dev-packages/browser-integration-tests/suites/public-api/logger/integration/test.ts @@ -18,7 +18,7 @@ sentryTest('should capture console object calls', async ({ getLocalTestUrl, page expect(envelopeItems[0]).toEqual([ { type: 'log', - item_count: 11, + item_count: 15, content_type: 'application/vnd.sentry.items.log+json', }, { @@ -33,6 +33,9 @@ sentryTest('should capture console object calls', async ({ getLocalTestUrl, page 'sentry.origin': { value: 'auto.console.logging', type: 'string' }, 'sentry.sdk.name': { value: 'sentry.javascript.browser', type: 'string' }, 'sentry.sdk.version': { value: expect.any(String), type: 'string' }, + 'sentry.message.template': { value: 'console.trace {} {}', type: 'string' }, + 'sentry.message.parameter.0': { value: 123, type: 'integer' }, + 'sentry.message.parameter.1': { value: false, type: 'boolean' }, }, }, { @@ -45,6 +48,9 @@ sentryTest('should capture console object calls', async ({ getLocalTestUrl, page 'sentry.origin': { value: 'auto.console.logging', type: 'string' }, 'sentry.sdk.name': { value: 'sentry.javascript.browser', type: 'string' }, 'sentry.sdk.version': { value: expect.any(String), type: 'string' }, + 'sentry.message.template': { value: 'console.debug {} {}', type: 'string' }, + 'sentry.message.parameter.0': { value: 123, type: 'integer' }, + 'sentry.message.parameter.1': { value: false, type: 'boolean' }, }, }, { @@ -57,6 +63,9 @@ sentryTest('should capture console object calls', async ({ getLocalTestUrl, page 'sentry.origin': { value: 'auto.console.logging', type: 'string' }, 'sentry.sdk.name': { value: 'sentry.javascript.browser', type: 'string' }, 'sentry.sdk.version': { value: expect.any(String), type: 'string' }, + 'sentry.message.template': { value: 'console.log {} {}', type: 'string' }, + 'sentry.message.parameter.0': { value: 123, type: 'integer' }, + 'sentry.message.parameter.1': { value: false, type: 'boolean' }, }, }, { @@ -69,6 +78,9 @@ sentryTest('should capture console object calls', async ({ getLocalTestUrl, page 'sentry.origin': { value: 'auto.console.logging', type: 'string' }, 'sentry.sdk.name': { value: 'sentry.javascript.browser', type: 'string' }, 'sentry.sdk.version': { value: expect.any(String), type: 'string' }, + 'sentry.message.template': { value: 'console.info {} {}', type: 'string' }, + 'sentry.message.parameter.0': { value: 123, type: 'integer' }, + 'sentry.message.parameter.1': { value: false, type: 'boolean' }, }, }, { @@ -81,6 +93,9 @@ sentryTest('should capture console object calls', async ({ getLocalTestUrl, page 'sentry.origin': { value: 'auto.console.logging', type: 'string' }, 'sentry.sdk.name': { value: 'sentry.javascript.browser', type: 'string' }, 'sentry.sdk.version': { value: expect.any(String), type: 'string' }, + 'sentry.message.template': { value: 'console.warn {} {}', type: 'string' }, + 'sentry.message.parameter.0': { value: 123, type: 'integer' }, + 'sentry.message.parameter.1': { value: false, type: 'boolean' }, }, }, { @@ -93,6 +108,9 @@ sentryTest('should capture console object calls', async ({ getLocalTestUrl, page 'sentry.origin': { value: 'auto.console.logging', type: 'string' }, 'sentry.sdk.name': { value: 'sentry.javascript.browser', type: 'string' }, 'sentry.sdk.version': { value: expect.any(String), type: 'string' }, + 'sentry.message.template': { value: 'console.error {} {}', type: 'string' }, + 'sentry.message.parameter.0': { value: 123, type: 'integer' }, + 'sentry.message.parameter.1': { value: false, type: 'boolean' }, }, }, { @@ -117,6 +135,8 @@ sentryTest('should capture console object calls', async ({ getLocalTestUrl, page 'sentry.origin': { value: 'auto.console.logging', type: 'string' }, 'sentry.sdk.name': { value: 'sentry.javascript.browser', type: 'string' }, 'sentry.sdk.version': { value: expect.any(String), type: 'string' }, + 'sentry.message.template': { value: 'Object: {}', type: 'string' }, + 'sentry.message.parameter.0': { value: '{"key":"value","nested":{"prop":123}}', type: 'string' }, }, }, { @@ -129,6 +149,8 @@ sentryTest('should capture console object calls', async ({ getLocalTestUrl, page 'sentry.origin': { value: 'auto.console.logging', type: 'string' }, 'sentry.sdk.name': { value: 'sentry.javascript.browser', type: 'string' }, 'sentry.sdk.version': { value: expect.any(String), type: 'string' }, + 'sentry.message.template': { value: 'Array: {}', type: 'string' }, + 'sentry.message.parameter.0': { value: '[1,2,3,"string"]', type: 'string' }, }, }, { @@ -141,6 +163,11 @@ sentryTest('should capture console object calls', async ({ getLocalTestUrl, page 'sentry.origin': { value: 'auto.console.logging', type: 'string' }, 'sentry.sdk.name': { value: 'sentry.javascript.browser', type: 'string' }, 'sentry.sdk.version': { value: expect.any(String), type: 'string' }, + 'sentry.message.template': { value: 'Mixed: {} {} {} {}', type: 'string' }, + 'sentry.message.parameter.0': { value: 'prefix', type: 'string' }, + 'sentry.message.parameter.1': { value: '{"obj":true}', type: 'string' }, + 'sentry.message.parameter.2': { value: '[4,5,6]', type: 'string' }, + 'sentry.message.parameter.3': { value: 'suffix', type: 'string' }, }, }, { @@ -155,6 +182,62 @@ sentryTest('should capture console object calls', async ({ getLocalTestUrl, page 'sentry.sdk.version': { value: expect.any(String), type: 'string' }, }, }, + { + timestamp: expect.any(Number), + level: 'info', + severity_number: 10, + trace_id: expect.any(String), + body: 'String substitution %s %d test 42', + attributes: { + 'sentry.origin': { value: 'auto.console.logging', type: 'string' }, + 'sentry.sdk.name': { value: 'sentry.javascript.browser', type: 'string' }, + 'sentry.sdk.version': { value: expect.any(String), type: 'string' }, + }, + }, + { + timestamp: expect.any(Number), + level: 'info', + severity_number: 10, + trace_id: expect.any(String), + body: 'Object substitution %o {"key":"value"}', + attributes: { + 'sentry.origin': { value: 'auto.console.logging', type: 'string' }, + 'sentry.sdk.name': { value: 'sentry.javascript.browser', type: 'string' }, + 'sentry.sdk.version': { value: expect.any(String), type: 'string' }, + }, + }, + { + timestamp: expect.any(Number), + level: 'info', + severity_number: 10, + trace_id: expect.any(String), + body: 'first 0 1 2', + attributes: { + 'sentry.origin': { value: 'auto.console.logging', type: 'string' }, + 'sentry.sdk.name': { value: 'sentry.javascript.browser', type: 'string' }, + 'sentry.sdk.version': { value: expect.any(String), type: 'string' }, + 'sentry.message.template': { value: 'first {} {} {}', type: 'string' }, + 'sentry.message.parameter.0': { value: 0, type: 'integer' }, + 'sentry.message.parameter.1': { value: 1, type: 'integer' }, + 'sentry.message.parameter.2': { value: 2, type: 'integer' }, + }, + }, + { + timestamp: expect.any(Number), + level: 'info', + severity_number: 10, + trace_id: expect.any(String), + body: 'hello true null undefined', + attributes: { + 'sentry.origin': { value: 'auto.console.logging', type: 'string' }, + 'sentry.sdk.name': { value: 'sentry.javascript.browser', type: 'string' }, + 'sentry.sdk.version': { value: expect.any(String), type: 'string' }, + 'sentry.message.template': { value: 'hello {} {} {}', type: 'string' }, + 'sentry.message.parameter.0': { value: true, type: 'boolean' }, + 'sentry.message.parameter.1': { value: 'null', type: 'string' }, + 'sentry.message.parameter.2': { value: '', type: 'string' }, + }, + }, ], }, ]); diff --git a/packages/core/src/logs/console-integration.ts b/packages/core/src/logs/console-integration.ts index a79da511373f..bf49c745e788 100644 --- a/packages/core/src/logs/console-integration.ts +++ b/packages/core/src/logs/console-integration.ts @@ -7,7 +7,7 @@ import type { ConsoleLevel } from '../types-hoist/instrument'; import type { IntegrationFn } from '../types-hoist/integration'; import { CONSOLE_LEVELS, debug } from '../utils/debug-logger'; import { _INTERNAL_captureLog } from './internal'; -import { formatConsoleArgs } from './utils'; +import { createConsoleTemplateAttributes, formatConsoleArgs, hasConsoleSubstitutions } from './utils'; interface CaptureConsoleOptions { levels: ConsoleLevel[]; @@ -36,9 +36,11 @@ const _consoleLoggingIntegration = ((options: Partial = { return; } + const firstArg = args[0]; + const followingArgs = args.slice(1); + if (level === 'assert') { - if (!args[0]) { - const followingArgs = args.slice(1); + if (!firstArg) { const assertionMessage = followingArgs.length > 0 ? `Assertion failed: ${formatConsoleArgs(followingArgs, normalizeDepth, normalizeMaxBreadth)}` @@ -49,11 +51,19 @@ const _consoleLoggingIntegration = ((options: Partial = { } const isLevelLog = level === 'log'; + + const shouldGenerateTemplate = + args.length > 1 && typeof args[0] === 'string' && !hasConsoleSubstitutions(args[0]); + const attributes = { + ...DEFAULT_ATTRIBUTES, + ...(shouldGenerateTemplate ? createConsoleTemplateAttributes(firstArg, followingArgs) : {}), + }; + _INTERNAL_captureLog({ level: isLevelLog ? 'info' : level, message: formatConsoleArgs(args, normalizeDepth, normalizeMaxBreadth), severityNumber: isLevelLog ? 10 : undefined, - attributes: DEFAULT_ATTRIBUTES, + attributes, }); }); }, diff --git a/packages/core/src/logs/utils.ts b/packages/core/src/logs/utils.ts index c30bfd75530b..5f95e0db3aad 100644 --- a/packages/core/src/logs/utils.ts +++ b/packages/core/src/logs/utils.ts @@ -37,3 +37,35 @@ export function safeJoinConsoleArgs(values: unknown[], normalizeDepth: number, n ) .join(' '); } + +/** + * Checks if a string contains console substitution patterns like %s, %d, %i, %f, %o, %O, %c. + * + * @param str - The string to check + * @returns true if the string contains console substitution patterns + */ +export function hasConsoleSubstitutions(str: string): boolean { + // Match console substitution patterns: %s, %d, %i, %f, %o, %O, %c + return /%[sdifocO]/.test(str); +} + +/** + * Creates template attributes for multiple console arguments. + * + * @param args - The console arguments + * @returns An object with template and parameter attributes + */ +export function createConsoleTemplateAttributes(firstArg: unknown, followingArgs: unknown[]): Record { + const attributes: Record = {}; + + // Create template with placeholders for each argument + const template = new Array(followingArgs.length).fill('{}').join(' '); + attributes['sentry.message.template'] = `${firstArg} ${template}`; + + // Add each argument as a parameter + followingArgs.forEach((arg, index) => { + attributes[`sentry.message.parameter.${index}`] = arg; + }); + + return attributes; +}