diff --git a/dev-packages/browser-integration-tests/suites/public-api/logger/integration/init.js b/dev-packages/browser-integration-tests/suites/public-api/logger/integration/init.js new file mode 100644 index 000000000000..e0ceaaebf017 --- /dev/null +++ b/dev-packages/browser-integration-tests/suites/public-api/logger/integration/init.js @@ -0,0 +1,11 @@ +import * as Sentry from '@sentry/browser'; + +window.Sentry = Sentry; + +Sentry.init({ + dsn: 'https://public@dsn.ingest.sentry.io/1337', + _experiments: { + enableLogs: true, + }, + integrations: [Sentry.consoleLoggingIntegration()], +}); 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 new file mode 100644 index 000000000000..6c2e9cfdde7a --- /dev/null +++ b/dev-packages/browser-integration-tests/suites/public-api/logger/integration/subject.js @@ -0,0 +1,11 @@ +console.trace('console.trace', 123, false); +console.debug('console.debug', 123, false); +console.log('console.log', 123, false); +console.info('console.info', 123, false); +console.warn('console.warn', 123, false); +console.error('console.error', 123, false); +console.assert(false, 'console.assert', 123, false); + +console.log(''); + +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 new file mode 100644 index 000000000000..00e918ce9719 --- /dev/null +++ b/dev-packages/browser-integration-tests/suites/public-api/logger/integration/test.ts @@ -0,0 +1,116 @@ +import { expect } from '@playwright/test'; +import type { OtelLogEnvelope } from '@sentry/core'; + +import { sentryTest } from '../../../../utils/fixtures'; +import { getFirstSentryEnvelopeRequest, properFullEnvelopeRequestParser } from '../../../../utils/helpers'; + +sentryTest('should capture console object calls', async ({ getLocalTestUrl, page }) => { + const bundle = process.env.PW_BUNDLE || ''; + // Only run this for npm package exports + if (bundle.startsWith('bundle') || bundle.startsWith('loader')) { + sentryTest.skip(); + } + + const url = await getLocalTestUrl({ testDir: __dirname }); + + const event = await getFirstSentryEnvelopeRequest(page, url, properFullEnvelopeRequestParser); + const envelopeItems = event[1]; + + expect(envelopeItems[0]).toEqual([ + { + type: 'otel_log', + }, + { + severityText: 'trace', + body: { stringValue: 'console.trace 123 false' }, + attributes: [], + timeUnixNano: expect.any(String), + traceId: expect.any(String), + severityNumber: 1, + }, + ]); + + expect(envelopeItems[1]).toEqual([ + { + type: 'otel_log', + }, + { + severityText: 'debug', + body: { stringValue: 'console.debug 123 false' }, + attributes: [], + timeUnixNano: expect.any(String), + traceId: expect.any(String), + severityNumber: 5, + }, + ]); + + expect(envelopeItems[2]).toEqual([ + { + type: 'otel_log', + }, + { + severityText: 'info', + body: { stringValue: 'console.log 123 false' }, + attributes: [], + timeUnixNano: expect.any(String), + traceId: expect.any(String), + severityNumber: 10, + }, + ]); + + expect(envelopeItems[3]).toEqual([ + { + type: 'otel_log', + }, + { + severityText: 'info', + body: { stringValue: 'console.info 123 false' }, + attributes: [], + timeUnixNano: expect.any(String), + traceId: expect.any(String), + severityNumber: 9, + }, + ]); + + expect(envelopeItems[4]).toEqual([ + { + type: 'otel_log', + }, + { + severityText: 'warn', + body: { stringValue: 'console.warn 123 false' }, + attributes: [], + timeUnixNano: expect.any(String), + traceId: expect.any(String), + severityNumber: 13, + }, + ]); + + expect(envelopeItems[5]).toEqual([ + { + type: 'otel_log', + }, + { + severityText: 'error', + body: { stringValue: 'console.error 123 false' }, + attributes: [], + timeUnixNano: expect.any(String), + traceId: expect.any(String), + severityNumber: 17, + }, + ]); + + expect(envelopeItems[6]).toEqual([ + { + type: 'otel_log', + }, + { + severityText: 'error', + body: { stringValue: 'Assertion failed: console.assert 123 false' }, + attributes: [], + timeUnixNano: expect.any(String), + traceId: expect.any(String), + severityNumber: 17, + }, + ]); +}); diff --git a/dev-packages/browser-integration-tests/suites/public-api/logger/subject.js b/dev-packages/browser-integration-tests/suites/public-api/logger/simple/subject.js similarity index 100% rename from dev-packages/browser-integration-tests/suites/public-api/logger/subject.js rename to dev-packages/browser-integration-tests/suites/public-api/logger/simple/subject.js diff --git a/dev-packages/browser-integration-tests/suites/public-api/logger/test.ts b/dev-packages/browser-integration-tests/suites/public-api/logger/simple/test.ts similarity index 98% rename from dev-packages/browser-integration-tests/suites/public-api/logger/test.ts rename to dev-packages/browser-integration-tests/suites/public-api/logger/simple/test.ts index 53a5f31ffcbb..84f2a34e8fe5 100644 --- a/dev-packages/browser-integration-tests/suites/public-api/logger/test.ts +++ b/dev-packages/browser-integration-tests/suites/public-api/logger/simple/test.ts @@ -1,8 +1,8 @@ import { expect } from '@playwright/test'; import type { OtelLogEnvelope } from '@sentry/core'; -import { sentryTest } from '../../../utils/fixtures'; -import { getFirstSentryEnvelopeRequest, properFullEnvelopeRequestParser } from '../../../utils/helpers'; +import { sentryTest } from '../../../../utils/fixtures'; +import { getFirstSentryEnvelopeRequest, properFullEnvelopeRequestParser } from '../../../../utils/helpers'; sentryTest('should capture all logging methods', async ({ getLocalTestUrl, page }) => { const bundle = process.env.PW_BUNDLE || ''; diff --git a/packages/astro/src/index.server.ts b/packages/astro/src/index.server.ts index 6467affd841c..d89503eb9dfb 100644 --- a/packages/astro/src/index.server.ts +++ b/packages/astro/src/index.server.ts @@ -128,6 +128,7 @@ export { zodErrorsIntegration, profiler, logger, + consoleLoggingIntegration, } from '@sentry/node'; export { init } from './server/sdk'; diff --git a/packages/aws-serverless/src/index.ts b/packages/aws-serverless/src/index.ts index 473b87c793d9..59465831a734 100644 --- a/packages/aws-serverless/src/index.ts +++ b/packages/aws-serverless/src/index.ts @@ -114,6 +114,7 @@ export { amqplibIntegration, vercelAIIntegration, logger, + consoleLoggingIntegration, } from '@sentry/node'; export { diff --git a/packages/browser/src/index.ts b/packages/browser/src/index.ts index 9103bbab99b5..275144cd280c 100644 --- a/packages/browser/src/index.ts +++ b/packages/browser/src/index.ts @@ -14,6 +14,7 @@ export { extraErrorDataIntegration, rewriteFramesIntegration, captureFeedback, + consoleLoggingIntegration, } from '@sentry/core'; export { replayIntegration, getReplay } from '@sentry-internal/replay'; diff --git a/packages/bun/src/index.ts b/packages/bun/src/index.ts index c2aff0f1ca52..a1c26d5a2819 100644 --- a/packages/bun/src/index.ts +++ b/packages/bun/src/index.ts @@ -133,6 +133,7 @@ export { amqplibIntegration, vercelAIIntegration, logger, + consoleLoggingIntegration, } from '@sentry/node'; export { diff --git a/packages/core/src/index.ts b/packages/core/src/index.ts index 5f5c4e6768b7..6c9a7fdde82e 100644 --- a/packages/core/src/index.ts +++ b/packages/core/src/index.ts @@ -114,6 +114,7 @@ export { trpcMiddleware } from './trpc'; export { captureFeedback } from './feedback'; export type { ReportDialogOptions } from './report-dialog'; export { _INTERNAL_captureLog, _INTERNAL_flushLogsBuffer } from './logs/exports'; +export { consoleLoggingIntegration } from './logs/console-integration'; // TODO: Make this structure pretty again and don't do "export *" export * from './utils-hoist/index'; diff --git a/packages/core/src/logs/console-integration.ts b/packages/core/src/logs/console-integration.ts new file mode 100644 index 000000000000..6dbb829db5a6 --- /dev/null +++ b/packages/core/src/logs/console-integration.ts @@ -0,0 +1,82 @@ +import { getClient } from '../currentScopes'; +import { DEBUG_BUILD } from '../debug-build'; +import { defineIntegration } from '../integration'; +import type { ConsoleLevel, IntegrationFn } from '../types-hoist'; +import { CONSOLE_LEVELS, GLOBAL_OBJ, addConsoleInstrumentationHandler, logger, safeJoin } from '../utils-hoist'; +import { _INTERNAL_captureLog } from './exports'; + +interface CaptureConsoleOptions { + levels: ConsoleLevel[]; +} + +type GlobalObjectWithUtil = typeof GLOBAL_OBJ & { + util: { + format: (...args: unknown[]) => string; + }; +}; + +const INTEGRATION_NAME = 'ConsoleLogs'; + +const _consoleLoggingIntegration = ((options: Partial = {}) => { + const levels = options.levels || CONSOLE_LEVELS; + + return { + name: INTEGRATION_NAME, + setup(client) { + if (!client.getOptions()._experiments?.enableLogs) { + DEBUG_BUILD && logger.warn('`_experiments.enableLogs` is not enabled, ConsoleLogs integration disabled'); + return; + } + + addConsoleInstrumentationHandler(({ args, level }) => { + if (getClient() !== client || !levels.includes(level)) { + return; + } + + if (level === 'assert') { + if (!args[0]) { + const followingArgs = args.slice(1); + const message = + followingArgs.length > 0 ? `Assertion failed: ${formatConsoleArgs(followingArgs)}` : 'Assertion failed'; + _INTERNAL_captureLog({ level: 'error', message }); + } + return; + } + + const isLevelLog = level === 'log'; + _INTERNAL_captureLog({ + level: isLevelLog ? 'info' : level, + message: formatConsoleArgs(args), + severityNumber: isLevelLog ? 10 : undefined, + }); + }); + }, + }; +}) satisfies IntegrationFn; + +/** + * Captures calls to the `console` API as logs in Sentry. Requires `_experiments.enableLogs` to be enabled. + * + * @experimental This feature is experimental and may be changed or removed in future versions. + * + * By default the integration instruments `console.debug`, `console.info`, `console.warn`, `console.error`, + * `console.log`, `console.trace`, and `console.assert`. You can use the `levels` option to customize which + * levels are captured. + * + * @example + * + * ```ts + * import * as Sentry from '@sentry/browser'; + * + * Sentry.init({ + * integrations: [Sentry.consoleLoggingIntegration({ levels: ['error', 'warn'] })], + * }); + * ``` + */ +export const consoleLoggingIntegration = defineIntegration(_consoleLoggingIntegration); + +function formatConsoleArgs(values: unknown[]): string { + return 'util' in GLOBAL_OBJ && typeof (GLOBAL_OBJ as GlobalObjectWithUtil).util.format === 'function' + ? (GLOBAL_OBJ as GlobalObjectWithUtil).util.format(...values) + : safeJoin(values, ' '); +} diff --git a/packages/core/src/types-hoist/log.ts b/packages/core/src/types-hoist/log.ts index 35e30ce42b65..05f8e05ad228 100644 --- a/packages/core/src/types-hoist/log.ts +++ b/packages/core/src/types-hoist/log.ts @@ -36,7 +36,7 @@ export interface Log { level: LogSeverityLevel; /** - * The message to be logged - for example, 'hello world' would become a log like '[INFO] hello world' + * The message to be logged. */ message: ParameterizedString; diff --git a/packages/google-cloud-serverless/src/index.ts b/packages/google-cloud-serverless/src/index.ts index 83a18f62c6df..54ae30fb5c8c 100644 --- a/packages/google-cloud-serverless/src/index.ts +++ b/packages/google-cloud-serverless/src/index.ts @@ -114,6 +114,7 @@ export { childProcessIntegration, vercelAIIntegration, logger, + consoleLoggingIntegration, } from '@sentry/node'; export { diff --git a/packages/node/src/index.ts b/packages/node/src/index.ts index decfbd578c68..31e383040f70 100644 --- a/packages/node/src/index.ts +++ b/packages/node/src/index.ts @@ -130,6 +130,7 @@ export { updateSpanName, zodErrorsIntegration, profiler, + consoleLoggingIntegration, } from '@sentry/core'; export type { diff --git a/packages/remix/src/server/index.ts b/packages/remix/src/server/index.ts index 7809455ce3fa..6c5319349294 100644 --- a/packages/remix/src/server/index.ts +++ b/packages/remix/src/server/index.ts @@ -113,6 +113,7 @@ export { withScope, zodErrorsIntegration, logger, + consoleLoggingIntegration, } from '@sentry/node'; // Keeping the `*` exports for backwards compatibility and types diff --git a/packages/solidstart/src/server/index.ts b/packages/solidstart/src/server/index.ts index a36b7fbfaad1..da00b43a4fde 100644 --- a/packages/solidstart/src/server/index.ts +++ b/packages/solidstart/src/server/index.ts @@ -116,6 +116,7 @@ export { withScope, zodErrorsIntegration, logger, + consoleLoggingIntegration, } from '@sentry/node'; // We can still leave this for the carrier init and type exports diff --git a/packages/sveltekit/src/server/index.ts b/packages/sveltekit/src/server/index.ts index 9df3ddd688c8..f50420fd2937 100644 --- a/packages/sveltekit/src/server/index.ts +++ b/packages/sveltekit/src/server/index.ts @@ -118,6 +118,7 @@ export { withScope, zodErrorsIntegration, logger, + consoleLoggingIntegration, } from '@sentry/node'; // We can still leave this for the carrier init and type exports