From e5b26346841b7e66529f95827c9e107cdb6136f3 Mon Sep 17 00:00:00 2001 From: Abhijeet Prasad Date: Thu, 10 Apr 2025 11:33:47 +0200 Subject: [PATCH 1/3] feat(core): Move console integration and add to cloudflare/vercel-edge --- .../browser/src/integrations/breadcrumbs.ts | 1 + packages/cloudflare/src/index.ts | 1 + packages/cloudflare/src/sdk.ts | 2 + packages/core/src/index.ts | 2 + packages/core/src/integrations/console.ts | 95 +++++++++++++++++++ .../test/lib/integrations/console.test.ts | 75 +++++++++++++++ packages/deno/src/integrations/breadcrumbs.ts | 1 + packages/node/src/index.ts | 2 +- packages/node/src/integrations/console.ts | 38 -------- packages/node/src/sdk/index.ts | 2 +- .../node/test/integration/console.test.ts | 39 -------- packages/vercel-edge/src/index.ts | 1 + packages/vercel-edge/src/sdk.ts | 2 + 13 files changed, 182 insertions(+), 79 deletions(-) create mode 100644 packages/core/src/integrations/console.ts create mode 100644 packages/core/test/lib/integrations/console.test.ts delete mode 100644 packages/node/src/integrations/console.ts delete mode 100644 packages/node/test/integration/console.test.ts diff --git a/packages/browser/src/integrations/breadcrumbs.ts b/packages/browser/src/integrations/breadcrumbs.ts index bec6fbff019e..1abb3beacc50 100644 --- a/packages/browser/src/integrations/breadcrumbs.ts +++ b/packages/browser/src/integrations/breadcrumbs.ts @@ -74,6 +74,7 @@ const _breadcrumbsIntegration = ((options: Partial = {}) => return { name: INTEGRATION_NAME, setup(client) { + // TODO(v10): Remove this functionality and use `consoleIntegration` from @sentry/core instead. if (_options.console) { addConsoleInstrumentationHandler(_getConsoleBreadcrumbHandler(client)); } diff --git a/packages/cloudflare/src/index.ts b/packages/cloudflare/src/index.ts index 05fd40fb4c96..faad474cc801 100644 --- a/packages/cloudflare/src/index.ts +++ b/packages/cloudflare/src/index.ts @@ -76,6 +76,7 @@ export { captureConsoleIntegration, moduleMetadataIntegration, zodErrorsIntegration, + consoleIntegration, SEMANTIC_ATTRIBUTE_SENTRY_OP, SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN, SEMANTIC_ATTRIBUTE_SENTRY_SOURCE, diff --git a/packages/cloudflare/src/sdk.ts b/packages/cloudflare/src/sdk.ts index 9891994e8de1..96e5fcc643a9 100644 --- a/packages/cloudflare/src/sdk.ts +++ b/packages/cloudflare/src/sdk.ts @@ -8,6 +8,7 @@ import { linkedErrorsIntegration, requestDataIntegration, stackParserFromStackParserOptions, + consoleIntegration, } from '@sentry/core'; import type { CloudflareClientOptions, CloudflareOptions } from './client'; import { CloudflareClient } from './client'; @@ -27,6 +28,7 @@ export function getDefaultIntegrations(options: CloudflareOptions): Integration[ linkedErrorsIntegration(), fetchIntegration(), requestDataIntegration(sendDefaultPii ? undefined : { include: { cookies: false } }), + consoleIntegration(), ]; } diff --git a/packages/core/src/index.ts b/packages/core/src/index.ts index 6c9a7fdde82e..2fcc73cdf392 100644 --- a/packages/core/src/index.ts +++ b/packages/core/src/index.ts @@ -108,6 +108,8 @@ export { extraErrorDataIntegration } from './integrations/extraerrordata'; export { rewriteFramesIntegration } from './integrations/rewriteframes'; export { zodErrorsIntegration } from './integrations/zoderrors'; export { thirdPartyErrorFilterIntegration } from './integrations/third-party-errors-filter'; +export { consoleIntegration } from './integrations/console'; + export { profiler } from './profiling'; export { instrumentFetchRequest } from './fetch'; export { trpcMiddleware } from './trpc'; diff --git a/packages/core/src/integrations/console.ts b/packages/core/src/integrations/console.ts new file mode 100644 index 000000000000..d197510eb207 --- /dev/null +++ b/packages/core/src/integrations/console.ts @@ -0,0 +1,95 @@ +import { addBreadcrumb } from '../breadcrumbs'; +import { getClient } from '../currentScopes'; +import { defineIntegration } from '../integration'; +import type { ConsoleLevel } from '../types-hoist'; +import { + CONSOLE_LEVELS, + GLOBAL_OBJ, + addConsoleInstrumentationHandler, + safeJoin, + severityLevelFromString, +} from '../utils-hoist'; + +interface ConsoleIntegrationOptions { + levels: ConsoleLevel[]; +} + +type GlobalObjectWithUtil = typeof GLOBAL_OBJ & { + util: { + format: (...args: unknown[]) => string; + }; +}; + +const INTEGRATION_NAME = 'Console'; + +/** + * Captures calls to the `console` API as logs in Sentry. + * + * 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 + * + * ```js + * Sentry.init({ + * integrations: [Sentry.consoleIntegration({ levels: ['error', 'warn'] })], + * }); + * ``` + */ +export const consoleIntegration = defineIntegration((options: Partial = {}) => { + const levels = new Set(options.levels || CONSOLE_LEVELS); + + return { + name: INTEGRATION_NAME, + setup(client) { + addConsoleInstrumentationHandler(({ args, level }) => { + if (getClient() !== client || !levels.has(level)) { + return; + } + + captureConsoleBreadcrumb(level, args); + }); + }, + }; +}); + +/** + * Capture a console breadcrumb. + * + * Exported just for tests. + */ +export function captureConsoleBreadcrumb(level: ConsoleLevel, args: unknown[]): void { + const breadcrumb = { + category: 'console', + data: { + arguments: args, + logger: 'console', + }, + level: severityLevelFromString(level), + message: formatConsoleArgs(args), + }; + + if (level === 'assert') { + if (args[0] === false) { + const assertionArgs = args.slice(1); + breadcrumb.message = + assertionArgs.length > 0 ? `Assertion failed: ${formatConsoleArgs(assertionArgs)}` : 'Assertion failed'; + breadcrumb.data.arguments = assertionArgs; + } else { + // Don't capture a breadcrumb for passed assertions + return; + } + } + + addBreadcrumb(breadcrumb, { + input: args, + level, + }); +} + +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/test/lib/integrations/console.test.ts b/packages/core/test/lib/integrations/console.test.ts new file mode 100644 index 000000000000..23892e8d5019 --- /dev/null +++ b/packages/core/test/lib/integrations/console.test.ts @@ -0,0 +1,75 @@ +import { describe, it, expect, vi, beforeEach } from 'vitest'; +import { captureConsoleBreadcrumb } from '../../../src/integrations/console'; +import { addBreadcrumb } from '../../../src/breadcrumbs'; + +vi.mock('../../../src/breadcrumbs', () => ({ + addBreadcrumb: vi.fn(), +})); + +describe('captureConsoleBreadcrumb', () => { + beforeEach(() => { + vi.clearAllMocks(); + }); + + it('creates a breadcrumb with correct properties for basic console log', () => { + const level = 'log'; + const args = ['test message', 123]; + + captureConsoleBreadcrumb(level, args); + + expect(addBreadcrumb).toHaveBeenCalledWith( + expect.objectContaining({ + category: 'console', + data: { + arguments: args, + logger: 'console', + }, + level: 'log', + message: 'test message 123', + }), + { + input: args, + level, + }, + ); + }); + + it('handles different console levels correctly', () => { + const levels = ['debug', 'info', 'warn', 'error'] as const; + + levels.forEach(level => { + captureConsoleBreadcrumb(level, ['test']); + expect(addBreadcrumb).toHaveBeenCalledWith( + expect.objectContaining({ + level: expect.any(String), + }), + expect.any(Object), + ); + }); + }); + + it('skips breadcrumb for passed assertions', () => { + captureConsoleBreadcrumb('assert', [true, 'should not be captured']); + expect(addBreadcrumb).not.toHaveBeenCalled(); + }); + + it('creates breadcrumb for failed assertions', () => { + const args = [false, 'assertion failed', 'details']; + + captureConsoleBreadcrumb('assert', args); + + expect(addBreadcrumb).toHaveBeenCalledWith( + expect.objectContaining({ + message: expect.stringContaining('Assertion failed'), + data: { + arguments: args.slice(1), + logger: 'console', + }, + }), + { + input: args, + level: 'assert', + }, + ); + }); +}); diff --git a/packages/deno/src/integrations/breadcrumbs.ts b/packages/deno/src/integrations/breadcrumbs.ts index 4d83b7972b21..47a04b08fc93 100644 --- a/packages/deno/src/integrations/breadcrumbs.ts +++ b/packages/deno/src/integrations/breadcrumbs.ts @@ -42,6 +42,7 @@ const _breadcrumbsIntegration = ((options: Partial = {}) => return { name: INTEGRATION_NAME, setup(client) { + // TODO(v10): Remove this functionality and use `consoleIntegration` from @sentry/core instead. if (_options.console) { addConsoleInstrumentationHandler(_getConsoleBreadcrumbHandler(client)); } diff --git a/packages/node/src/index.ts b/packages/node/src/index.ts index 31e383040f70..8467f3e3727d 100644 --- a/packages/node/src/index.ts +++ b/packages/node/src/index.ts @@ -2,7 +2,6 @@ export { httpIntegration } from './integrations/http'; export { nativeNodeFetchIntegration } from './integrations/node-fetch'; export { fsIntegration } from './integrations/fs'; -export { consoleIntegration } from './integrations/console'; export { nodeContextIntegration } from './integrations/context'; export { contextLinesIntegration } from './integrations/contextlines'; export { localVariablesIntegration } from './integrations/local-variables'; @@ -131,6 +130,7 @@ export { zodErrorsIntegration, profiler, consoleLoggingIntegration, + consoleIntegration, } from '@sentry/core'; export type { diff --git a/packages/node/src/integrations/console.ts b/packages/node/src/integrations/console.ts deleted file mode 100644 index d1bb0463551e..000000000000 --- a/packages/node/src/integrations/console.ts +++ /dev/null @@ -1,38 +0,0 @@ -import * as util from 'node:util'; -import { - addBreadcrumb, - addConsoleInstrumentationHandler, - defineIntegration, - getClient, - severityLevelFromString, -} from '@sentry/core'; - -const INTEGRATION_NAME = 'Console'; - -/** - * Capture console logs as breadcrumbs. - */ -export const consoleIntegration = defineIntegration(() => { - return { - name: INTEGRATION_NAME, - setup(client) { - addConsoleInstrumentationHandler(({ args, level }) => { - if (getClient() !== client) { - return; - } - - addBreadcrumb( - { - category: 'console', - level: severityLevelFromString(level), - message: util.format.apply(undefined, args), - }, - { - input: [...args], - level, - }, - ); - }); - }, - }; -}); diff --git a/packages/node/src/sdk/index.ts b/packages/node/src/sdk/index.ts index 21088a253fe3..7df3696c3d58 100644 --- a/packages/node/src/sdk/index.ts +++ b/packages/node/src/sdk/index.ts @@ -11,6 +11,7 @@ import { propagationContextFromHeaders, requestDataIntegration, stackParserFromStackParserOptions, + consoleIntegration, } from '@sentry/core'; import { enhanceDscWithOpenTelemetryRootSpanName, @@ -20,7 +21,6 @@ import { } from '@sentry/opentelemetry'; import { DEBUG_BUILD } from '../debug-build'; import { childProcessIntegration } from '../integrations/childProcess'; -import { consoleIntegration } from '../integrations/console'; import { nodeContextIntegration } from '../integrations/context'; import { contextLinesIntegration } from '../integrations/contextlines'; import { httpIntegration } from '../integrations/http'; diff --git a/packages/node/test/integration/console.test.ts b/packages/node/test/integration/console.test.ts deleted file mode 100644 index 691ccd4397ee..000000000000 --- a/packages/node/test/integration/console.test.ts +++ /dev/null @@ -1,39 +0,0 @@ -import * as SentryCore from '@sentry/core'; -import { resetInstrumentationHandlers } from '@sentry/core'; -import { afterEach, describe, expect, it, vi } from 'vitest'; -import { getClient } from '../../src'; -import type { NodeClient } from '../../src'; -import { consoleIntegration } from '../../src/integrations/console'; - -const addBreadcrumbSpy = vi.spyOn(SentryCore, 'addBreadcrumb'); - -vi.spyOn(console, 'log').mockImplementation(() => { - // noop so that we don't spam the logs -}); - -afterEach(() => { - vi.clearAllMocks(); - resetInstrumentationHandlers(); -}); - -describe('Console integration', () => { - it('should add a breadcrumb on console.log', () => { - consoleIntegration().setup?.(getClient() as NodeClient); - - // eslint-disable-next-line no-console - console.log('test'); - - expect(addBreadcrumbSpy).toHaveBeenCalledTimes(1); - expect(addBreadcrumbSpy).toHaveBeenCalledWith( - { - category: 'console', - level: 'log', - message: 'test', - }, - { - input: ['test'], - level: 'log', - }, - ); - }); -}); diff --git a/packages/vercel-edge/src/index.ts b/packages/vercel-edge/src/index.ts index eb6429c441fa..64ae281481d1 100644 --- a/packages/vercel-edge/src/index.ts +++ b/packages/vercel-edge/src/index.ts @@ -76,6 +76,7 @@ export { captureConsoleIntegration, moduleMetadataIntegration, zodErrorsIntegration, + consoleIntegration, SEMANTIC_ATTRIBUTE_SENTRY_OP, SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN, SEMANTIC_ATTRIBUTE_SENTRY_SOURCE, diff --git a/packages/vercel-edge/src/sdk.ts b/packages/vercel-edge/src/sdk.ts index 8c3939d26cba..1a09b16496a9 100644 --- a/packages/vercel-edge/src/sdk.ts +++ b/packages/vercel-edge/src/sdk.ts @@ -22,6 +22,7 @@ import { nodeStackLineParser, requestDataIntegration, stackParserFromStackParserOptions, + consoleIntegration, } from '@sentry/core'; import { SentryPropagator, @@ -57,6 +58,7 @@ export function getDefaultIntegrations(options: Options): Integration[] { functionToStringIntegration(), linkedErrorsIntegration(), winterCGFetchIntegration(), + consoleIntegration(), ...(options.sendDefaultPii ? [requestDataIntegration()] : []), ]; } From 21f4d5bcb6048ab30610779f07939887c5ed79e2 Mon Sep 17 00:00:00 2001 From: Abhijeet Prasad Date: Thu, 10 Apr 2025 11:47:45 +0200 Subject: [PATCH 2/3] Update packages/core/src/integrations/console.ts Co-authored-by: Francesco Gringl-Novy --- packages/core/src/integrations/console.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/core/src/integrations/console.ts b/packages/core/src/integrations/console.ts index d197510eb207..ca90b18aaf57 100644 --- a/packages/core/src/integrations/console.ts +++ b/packages/core/src/integrations/console.ts @@ -23,7 +23,7 @@ type GlobalObjectWithUtil = typeof GLOBAL_OBJ & { const INTEGRATION_NAME = 'Console'; /** - * Captures calls to the `console` API as logs in Sentry. + * Captures calls to the `console` API as breadcrumbs in Sentry. * * 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 From ac03cecbeae16d0eb6a8a689403d364c49bdc3d1 Mon Sep 17 00:00:00 2001 From: Abhijeet Prasad Date: Thu, 10 Apr 2025 11:58:34 +0200 Subject: [PATCH 3/3] use it.each for test --- packages/core/src/integrations/console.ts | 4 +-- .../test/lib/integrations/console.test.ts | 30 ++++++++----------- 2 files changed, 15 insertions(+), 19 deletions(-) diff --git a/packages/core/src/integrations/console.ts b/packages/core/src/integrations/console.ts index ca90b18aaf57..3cd0bff04a1e 100644 --- a/packages/core/src/integrations/console.ts +++ b/packages/core/src/integrations/console.ts @@ -48,7 +48,7 @@ export const consoleIntegration = defineIntegration((options: Partial ({ addBreadcrumb: vi.fn(), })); -describe('captureConsoleBreadcrumb', () => { +describe('addConsoleBreadcrumb', () => { beforeEach(() => { vi.clearAllMocks(); }); @@ -15,7 +15,7 @@ describe('captureConsoleBreadcrumb', () => { const level = 'log'; const args = ['test message', 123]; - captureConsoleBreadcrumb(level, args); + addConsoleBreadcrumb(level, args); expect(addBreadcrumb).toHaveBeenCalledWith( expect.objectContaining({ @@ -34,29 +34,25 @@ describe('captureConsoleBreadcrumb', () => { ); }); - it('handles different console levels correctly', () => { - const levels = ['debug', 'info', 'warn', 'error'] as const; - - levels.forEach(level => { - captureConsoleBreadcrumb(level, ['test']); - expect(addBreadcrumb).toHaveBeenCalledWith( - expect.objectContaining({ - level: expect.any(String), - }), - expect.any(Object), - ); - }); + it.each(['debug', 'info', 'warn', 'error'] as const)('handles %s level correctly', level => { + addConsoleBreadcrumb(level, ['test']); + expect(addBreadcrumb).toHaveBeenCalledWith( + expect.objectContaining({ + level: expect.any(String), + }), + expect.any(Object), + ); }); it('skips breadcrumb for passed assertions', () => { - captureConsoleBreadcrumb('assert', [true, 'should not be captured']); + addConsoleBreadcrumb('assert', [true, 'should not be captured']); expect(addBreadcrumb).not.toHaveBeenCalled(); }); it('creates breadcrumb for failed assertions', () => { const args = [false, 'assertion failed', 'details']; - captureConsoleBreadcrumb('assert', args); + addConsoleBreadcrumb('assert', args); expect(addBreadcrumb).toHaveBeenCalledWith( expect.objectContaining({