diff --git a/.size-limit.js b/.size-limit.js index c6e86836fd4c..64a28aee01f6 100644 --- a/.size-limit.js +++ b/.size-limit.js @@ -30,7 +30,13 @@ module.exports = [ ); config.optimization.minimize = true; - config.optimization.minimizer = [new TerserPlugin()]; + config.optimization.minimizer = [ + new TerserPlugin({ + terserOptions: { + ecma: 'es2020', + }, + }), + ]; return config; }, @@ -69,7 +75,13 @@ module.exports = [ ); config.optimization.minimize = true; - config.optimization.minimizer = [new TerserPlugin()]; + config.optimization.minimizer = [ + new TerserPlugin({ + terserOptions: { + ecma: 'es2020', + }, + }), + ]; return config; }, @@ -248,7 +260,13 @@ module.exports = [ ); config.optimization.minimize = true; - config.optimization.minimizer = [new TerserPlugin()]; + config.optimization.minimizer = [ + new TerserPlugin({ + terserOptions: { + ecma: 'es2020', + }, + }), + ]; return config; }, diff --git a/dev-packages/e2e-tests/test-applications/node-exports-test-app/scripts/consistentExports.ts b/dev-packages/e2e-tests/test-applications/node-exports-test-app/scripts/consistentExports.ts index 8c3e51b14024..06823d5e387f 100644 --- a/dev-packages/e2e-tests/test-applications/node-exports-test-app/scripts/consistentExports.ts +++ b/dev-packages/e2e-tests/test-applications/node-exports-test-app/scripts/consistentExports.ts @@ -18,6 +18,7 @@ const NODE_EXPORTS_IGNORE = [ 'setNodeAsyncContextStrategy', 'getDefaultIntegrationsWithoutPerformance', 'initWithoutDefaultIntegrations', + 'initWithDefaultIntegrations', 'SentryContextManager', 'validateOpenTelemetrySetup', 'preloadOpenTelemetry', diff --git a/packages/angular/src/sdk.ts b/packages/angular/src/sdk.ts index 404305f770ff..1506a3ff8dcc 100755 --- a/packages/angular/src/sdk.ts +++ b/packages/angular/src/sdk.ts @@ -5,7 +5,7 @@ import { browserSessionIntegration, globalHandlersIntegration, httpContextIntegration, - init as browserInit, + initWithDefaultIntegrations, linkedErrorsIntegration, setContext, } from '@sentry/browser'; @@ -49,14 +49,14 @@ export function getDefaultIntegrations(_options: BrowserOptions = {}): Integrati */ export function init(options: BrowserOptions): Client | undefined { const opts = { - defaultIntegrations: getDefaultIntegrations(), ...options, }; applySdkMetadata(opts, 'angular'); checkAndSetAngularVersion(); - return browserInit(opts); + + return initWithDefaultIntegrations(opts, getDefaultIntegrations); } function checkAndSetAngularVersion(): void { diff --git a/packages/astro/src/client/sdk.ts b/packages/astro/src/client/sdk.ts index 24d29b3cc109..80c6500437c4 100644 --- a/packages/astro/src/client/sdk.ts +++ b/packages/astro/src/client/sdk.ts @@ -2,10 +2,10 @@ import type { BrowserOptions } from '@sentry/browser'; import { browserTracingIntegration, getDefaultIntegrations as getBrowserDefaultIntegrations, - init as initBrowserSdk, + initWithDefaultIntegrations, } from '@sentry/browser'; -import { applySdkMetadata } from '@sentry/core'; import type { Client, Integration } from '@sentry/core'; +import { applySdkMetadata } from '@sentry/core'; // Tree-shakable guard to remove all code related to tracing declare const __SENTRY_TRACING__: boolean; @@ -17,13 +17,12 @@ declare const __SENTRY_TRACING__: boolean; */ export function init(options: BrowserOptions): Client | undefined { const opts = { - defaultIntegrations: getDefaultIntegrations(options), ...options, }; applySdkMetadata(opts, 'astro', ['astro', 'browser']); - return initBrowserSdk(opts); + return initWithDefaultIntegrations(opts, getDefaultIntegrations); } function getDefaultIntegrations(options: BrowserOptions): Integration[] { diff --git a/packages/astro/src/index.types.ts b/packages/astro/src/index.types.ts index eeadf11fa3d5..0ddb8d2e7e62 100644 --- a/packages/astro/src/index.types.ts +++ b/packages/astro/src/index.types.ts @@ -14,6 +14,10 @@ import sentryAstro from './index.server'; /** Initializes Sentry Astro SDK */ export declare function init(options: Options | clientSdk.BrowserOptions | NodeOptions): Client | undefined; +export declare function initWithDefaultIntegrations( + options: Options | clientSdk.BrowserOptions | NodeOptions, + getDefaultIntegrations: (options: Options) => Integration[], +): Client | undefined; export declare const linkedErrorsIntegration: typeof clientSdk.linkedErrorsIntegration; export declare const contextLinesIntegration: typeof clientSdk.contextLinesIntegration; diff --git a/packages/astro/test/client/sdk.test.ts b/packages/astro/test/client/sdk.test.ts index 9f3ea651697d..861d278b21e5 100644 --- a/packages/astro/test/client/sdk.test.ts +++ b/packages/astro/test/client/sdk.test.ts @@ -1,6 +1,5 @@ import { afterEach, describe, expect, it, vi } from 'vitest'; -import type { BrowserClient } from '@sentry/browser'; import { browserTracingIntegration, getActiveSpan, @@ -9,11 +8,11 @@ import { getIsolationScope, } from '@sentry/browser'; import * as SentryBrowser from '@sentry/browser'; -import { SDK_VERSION, getClient } from '@sentry/browser'; +import { SDK_VERSION } from '@sentry/browser'; import { init } from '../../src/client/sdk'; -const browserInit = vi.spyOn(SentryBrowser, 'init'); +const browserInit = vi.spyOn(SentryBrowser, 'initWithDefaultIntegrations'); describe('Sentry client SDK', () => { describe('init', () => { @@ -45,6 +44,7 @@ describe('Sentry client SDK', () => { }, }, }), + expect.any(Function), ); }); @@ -54,37 +54,31 @@ describe('Sentry client SDK', () => { ['tracesSampler', { tracesSampler: () => 1.0 }], ['no tracing option set', {}], ])('adds browserTracingIntegration if tracing is enabled via %s', (_, tracingOptions) => { - init({ + const client = init({ dsn: 'https://public@dsn.ingest.sentry.io/1337', ...tracingOptions, }); - const integrationsToInit = browserInit.mock.calls[0]![0]?.defaultIntegrations; - const browserTracing = getClient()?.getIntegrationByName('BrowserTracing'); - - expect(integrationsToInit).toContainEqual(expect.objectContaining({ name: 'BrowserTracing' })); + const browserTracing = client?.getIntegrationByName('BrowserTracing'); expect(browserTracing).toBeDefined(); }); it("doesn't add browserTracingIntegration if `__SENTRY_TRACING__` is set to false", () => { (globalThis as any).__SENTRY_TRACING__ = false; - init({ + const client = init({ dsn: 'https://public@dsn.ingest.sentry.io/1337', tracesSampleRate: 1, }); - const integrationsToInit = browserInit.mock.calls[0]![0]?.defaultIntegrations || []; - const browserTracing = getClient()?.getIntegrationByName('BrowserTracing'); - - expect(integrationsToInit).not.toContainEqual(expect.objectContaining({ name: 'BrowserTracing' })); + const browserTracing = client?.getIntegrationByName('BrowserTracing'); expect(browserTracing).toBeUndefined(); delete (globalThis as any).__SENTRY_TRACING__; }); it('Overrides the automatically default browserTracingIntegration instance with a a user-provided browserTracingIntegration instance', () => { - init({ + const client = init({ dsn: 'https://public@dsn.ingest.sentry.io/1337', integrations: [ browserTracingIntegration({ finalTimeout: 10, instrumentNavigation: false, instrumentPageLoad: false }), @@ -92,7 +86,7 @@ describe('Sentry client SDK', () => { tracesSampleRate: 1, }); - const browserTracing = getClient()?.getIntegrationByName('BrowserTracing'); + const browserTracing = client?.getIntegrationByName('BrowserTracing'); expect(browserTracing).toBeDefined(); // no active span means the settings were respected diff --git a/packages/aws-serverless/src/sdk.ts b/packages/aws-serverless/src/sdk.ts index 2523e025c377..f1fce3f85241 100644 --- a/packages/aws-serverless/src/sdk.ts +++ b/packages/aws-serverless/src/sdk.ts @@ -17,7 +17,7 @@ import { flush, getCurrentScope, getDefaultIntegrationsWithoutPerformance, - initWithoutDefaultIntegrations, + initWithDefaultIntegrations, startSpanManual, withScope, } from '@sentry/node'; @@ -77,13 +77,12 @@ export function getDefaultIntegrations(_options: Options): Integration[] { */ export function init(options: NodeOptions = {}): NodeClient | undefined { const opts = { - defaultIntegrations: getDefaultIntegrations(options), ...options, }; applySdkMetadata(opts, 'aws-serverless'); - return initWithoutDefaultIntegrations(opts); + return initWithDefaultIntegrations(opts, getDefaultIntegrations); } /** */ diff --git a/packages/aws-serverless/test/sdk.test.ts b/packages/aws-serverless/test/sdk.test.ts index 4f68eb0b6edb..544cba5de136 100644 --- a/packages/aws-serverless/test/sdk.test.ts +++ b/packages/aws-serverless/test/sdk.test.ts @@ -25,7 +25,7 @@ jest.mock('@sentry/node', () => { const original = jest.requireActual('@sentry/node'); return { ...original, - initWithoutDefaultIntegrations: (options: unknown) => { + initWithDefaultIntegrations: (options: unknown) => { mockInit(options); }, startInactiveSpan: (...args: unknown[]) => { diff --git a/packages/browser/src/index.ts b/packages/browser/src/index.ts index 42c388d73547..5e5215cfbc17 100644 --- a/packages/browser/src/index.ts +++ b/packages/browser/src/index.ts @@ -1,5 +1,7 @@ export * from './exports'; +export { initWithDefaultIntegrations } from './sdk'; + export { reportingObserverIntegration } from './integrations/reportingobserver'; export { httpClientIntegration } from './integrations/httpclient'; export { contextLinesIntegration } from './integrations/contextlines'; diff --git a/packages/browser/src/sdk.ts b/packages/browser/src/sdk.ts index d91f62d1ec47..fb6bb6e09067 100644 --- a/packages/browser/src/sdk.ts +++ b/packages/browser/src/sdk.ts @@ -47,9 +47,8 @@ export function getDefaultIntegrations(_options: Options): Integration[] { } /** Exported only for tests. */ -export function applyDefaultOptions(optionsArg: BrowserOptions = {}): BrowserOptions { +export function applyDefaultOptions(optionsArg: BrowserOptions): BrowserOptions { const defaultOptions: BrowserOptions = { - defaultIntegrations: getDefaultIntegrations(optionsArg), release: typeof __SENTRY_RELEASE__ === 'string' // This allows build tooling to find-and-replace __SENTRY_RELEASE__ to inject a release value ? __SENTRY_RELEASE__ @@ -59,27 +58,10 @@ export function applyDefaultOptions(optionsArg: BrowserOptions = {}): BrowserOpt return { ...defaultOptions, - ...dropTopLevelUndefinedKeys(optionsArg), + ...optionsArg, }; } -/** - * In contrast to the regular `dropUndefinedKeys` method, - * this one does not deep-drop keys, but only on the top level. - */ -function dropTopLevelUndefinedKeys(obj: T): Partial { - const mutatetedObj: Partial = {}; - - for (const k of Object.getOwnPropertyNames(obj)) { - const key = k as keyof T; - if (obj[key] !== undefined) { - mutatetedObj[key] = obj[key]; - } - } - - return mutatetedObj; -} - type ExtensionProperties = { chrome?: Runtime; browser?: Runtime; @@ -91,33 +73,6 @@ type Runtime = { }; }; -function shouldShowBrowserExtensionError(): boolean { - const windowWithMaybeExtension = - typeof WINDOW.window !== 'undefined' && (WINDOW as typeof WINDOW & ExtensionProperties); - if (!windowWithMaybeExtension) { - // No need to show the error if we're not in a browser window environment (e.g. service workers) - return false; - } - - const extensionKey = windowWithMaybeExtension.chrome ? 'chrome' : 'browser'; - const extensionObject = windowWithMaybeExtension[extensionKey]; - - const runtimeId = extensionObject?.runtime?.id; - const href = getLocationHref() || ''; - - const extensionProtocols = ['chrome-extension:', 'moz-extension:', 'ms-browser-extension:', 'safari-web-extension:']; - - // Running the SDK in a dedicated extension page and calling Sentry.init is fine; no risk of data leakage - const isDedicatedExtensionPage = - !!runtimeId && WINDOW === WINDOW.top && extensionProtocols.some(protocol => href.startsWith(`${protocol}//`)); - - // Running the SDK in NW.js, which appears like a browser extension but isn't, is also fine - // see: https://github.com/getsentry/sentry-javascript/issues/12668 - const isNWjs = typeof windowWithMaybeExtension.nw !== 'undefined'; - - return !!runtimeId && !isDedicatedExtensionPage && !isNWjs; -} - /** * A magic string that build tooling can leverage in order to inject a release value into the SDK. */ @@ -170,32 +125,56 @@ declare const __SENTRY_RELEASE__: string | undefined; * @see {@link BrowserOptions} for documentation on configuration options. */ export function init(browserOptions: BrowserOptions = {}): Client | undefined { + // Note: If we call `initWithDefaultIntegrations()` here, webpack seems unable to tree-shake the DEBUG_BUILD usage inside of it + // So we duplicate the logic here like this to ensure maximum saved bytes const options = applyDefaultOptions(browserOptions); + const defaultIntegrations = getDefaultIntegrations(browserOptions); - if (!options.skipBrowserExtensionCheck && shouldShowBrowserExtensionError()) { - if (DEBUG_BUILD) { - consoleSandbox(() => { - // eslint-disable-next-line no-console - console.error( - '[Sentry] You cannot run Sentry this way in a browser extension, check: https://docs.sentry.io/platforms/javascript/best-practices/browser-extensions/', - ); - }); - } + const isBrowserExtension = !options.skipBrowserExtensionCheck && shouldShowBrowserExtensionError(); + + if (DEBUG_BUILD) { + logBrowserEnvironmentWarnings({ + browserExtension: isBrowserExtension, + fetch: !supportsFetch(), + }); + } + + if (isBrowserExtension) { return; } - if (DEBUG_BUILD && !supportsFetch()) { - logger.warn( - 'No Fetch API detected. The Sentry SDK requires a Fetch API compatible environment to send events. Please add a Fetch API polyfill.', - ); + const clientOptions = getClientOptions(options, defaultIntegrations); + return initAndBind(BrowserClient, clientOptions); +} + +/** + * Initialize a browser client with the provided options and default integrations getter function. + * This is an internal method the SDK uses under the hood to set up things - you should not use this as a user! + * Instead, use `init()` to initialize the SDK. + * + * @hidden + * @internal + */ +export function initWithDefaultIntegrations( + browserOptions: BrowserOptions = {}, + getDefaultIntegrationsImpl: (options: BrowserOptions) => Integration[], +): BrowserClient | undefined { + const options = applyDefaultOptions(browserOptions); + const defaultIntegrations = getDefaultIntegrationsImpl(browserOptions); + + const isBrowserExtension = !options.skipBrowserExtensionCheck && shouldShowBrowserExtensionError(); + + if (DEBUG_BUILD) { + logBrowserEnvironmentWarnings({ + browserExtension: isBrowserExtension, + fetch: !supportsFetch(), + }); } - const clientOptions: BrowserClientOptions = { - ...options, - stackParser: stackParserFromStackParserOptions(options.stackParser || defaultStackParser), - integrations: getIntegrationsToSetup(options), - transport: options.transport || makeFetchTransport, - }; + if (isBrowserExtension) { + return; + } + const clientOptions = getClientOptions(options, defaultIntegrations); return initAndBind(BrowserClient, clientOptions); } @@ -280,3 +259,59 @@ export function forceLoad(): void { export function onLoad(callback: () => void): void { callback(); } + +function shouldShowBrowserExtensionError(): boolean { + const windowWithMaybeExtension = + typeof WINDOW.window !== 'undefined' && (WINDOW as typeof WINDOW & ExtensionProperties); + if (!windowWithMaybeExtension) { + // No need to show the error if we're not in a browser window environment (e.g. service workers) + return false; + } + + const extensionKey = windowWithMaybeExtension.chrome ? 'chrome' : 'browser'; + const extensionObject = windowWithMaybeExtension[extensionKey]; + + const runtimeId = extensionObject?.runtime?.id; + const href = getLocationHref() || ''; + + const extensionProtocols = ['chrome-extension:', 'moz-extension:', 'ms-browser-extension:', 'safari-web-extension:']; + + // Running the SDK in a dedicated extension page and calling Sentry.init is fine; no risk of data leakage + const isDedicatedExtensionPage = + !!runtimeId && WINDOW === WINDOW.top && extensionProtocols.some(protocol => href.startsWith(`${protocol}//`)); + + // Running the SDK in NW.js, which appears like a browser extension but isn't, is also fine + // see: https://github.com/getsentry/sentry-javascript/issues/12668 + const isNWjs = typeof windowWithMaybeExtension.nw !== 'undefined'; + + return !!runtimeId && !isDedicatedExtensionPage && !isNWjs; +} + +function logBrowserEnvironmentWarnings({ + fetch, + browserExtension, +}: { fetch: boolean; browserExtension: boolean }): void { + if (browserExtension) { + consoleSandbox(() => { + // eslint-disable-next-line no-console + console.error( + '[Sentry] You cannot run Sentry this way in a browser extension, check: https://docs.sentry.io/platforms/javascript/best-practices/browser-extensions/', + ); + }); + } + + if (fetch) { + logger.warn( + 'No Fetch API detected. The Sentry SDK requires a Fetch API compatible environment to send events. Please add a Fetch API polyfill.', + ); + } +} + +function getClientOptions(options: BrowserOptions, defaultIntegrations: Integration[]): BrowserClientOptions { + return { + ...options, + stackParser: stackParserFromStackParserOptions(options.stackParser || defaultStackParser), + integrations: getIntegrationsToSetup(options, defaultIntegrations), + transport: options.transport || makeFetchTransport, + }; +} diff --git a/packages/browser/test/sdk.test.ts b/packages/browser/test/sdk.test.ts index a6fc49edee89..5d30baceab61 100644 --- a/packages/browser/test/sdk.test.ts +++ b/packages/browser/test/sdk.test.ts @@ -4,7 +4,7 @@ /* eslint-disable @typescript-eslint/unbound-method */ import type { Mock } from 'vitest'; -import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; +import { afterEach, describe, expect, it, vi } from 'vitest'; import * as SentryCore from '@sentry/core'; import { createTransport } from '@sentry/core'; @@ -13,7 +13,7 @@ import type { Integration } from '@sentry/core'; import type { BrowserOptions } from '../src'; import { WINDOW } from '../src'; -import { applyDefaultOptions, getDefaultIntegrations, init } from '../src/sdk'; +import { applyDefaultOptions, init, initWithDefaultIntegrations } from '../src/sdk'; const PUBLIC_DSN = 'https://username@domain/123'; @@ -35,15 +35,11 @@ export class MockIntegration implements Integration { } describe('init', () => { - beforeEach(() => { - vi.clearAllMocks(); + afterEach(() => { + vi.restoreAllMocks(); }); - afterAll(() => { - vi.resetAllMocks(); - }); - - test('installs default integrations', () => { + test('installs passed default integrations', () => { const DEFAULT_INTEGRATIONS: Integration[] = [ new MockIntegration('MockIntegration 0.1'), new MockIntegration('MockIntegration 0.2'), @@ -56,28 +52,41 @@ describe('init', () => { expect(DEFAULT_INTEGRATIONS[1]!.setupOnce as Mock).toHaveBeenCalledTimes(1); }); + it('installs default integrations', () => { + // Note: We need to prevent this from actually adding all the default integrations, as otherwise + // following tests may fail (e.g. because console is monkey patched etc.) + const spyGetIntegrationsToSetup = vi.spyOn(SentryCore, 'getIntegrationsToSetup').mockImplementation(() => []); + + const options = getDefaultBrowserOptions({ dsn: PUBLIC_DSN }); + init(options); + + expect(spyGetIntegrationsToSetup).toHaveBeenCalledTimes(1); + expect(spyGetIntegrationsToSetup).toHaveBeenCalledWith( + expect.objectContaining(options), + expect.arrayContaining([expect.objectContaining({ name: 'InboundFilters' })]), + ); + }); + it('installs default integrations if `defaultIntegrations: undefined`', () => { - // @ts-expect-error this is fine for testing - const initAndBindSpy = vi.spyOn(SentryCore, 'initAndBind').mockImplementationOnce(() => {}); + // Note: We need to prevent this from actually adding all the default integrations, as otherwise + // following tests may fail (e.g. because console is monkey patched etc.) + const spyGetIntegrationsToSetup = vi.spyOn(SentryCore, 'getIntegrationsToSetup').mockImplementation(() => []); + const options = getDefaultBrowserOptions({ dsn: PUBLIC_DSN, defaultIntegrations: undefined }); init(options); - expect(initAndBindSpy).toHaveBeenCalledTimes(1); - - const optionsPassed = initAndBindSpy.mock.calls[0]?.[1]; - expect(optionsPassed?.integrations.length).toBeGreaterThan(0); + expect(spyGetIntegrationsToSetup).toHaveBeenCalledTimes(1); + expect(spyGetIntegrationsToSetup).toHaveBeenCalledWith( + expect.objectContaining(options), + expect.arrayContaining([expect.objectContaining({ name: 'InboundFilters' })]), + ); }); - test("doesn't install default integrations if told not to", () => { - const DEFAULT_INTEGRATIONS: Integration[] = [ - new MockIntegration('MockIntegration 0.3'), - new MockIntegration('MockIntegration 0.4'), - ]; + test("doesn't install any default integrations if told not to", () => { const options = getDefaultBrowserOptions({ dsn: PUBLIC_DSN, defaultIntegrations: false }); - init(options); + const client = init(options); - expect(DEFAULT_INTEGRATIONS[0]!.setupOnce as Mock).toHaveBeenCalledTimes(0); - expect(DEFAULT_INTEGRATIONS[1]!.setupOnce as Mock).toHaveBeenCalledTimes(0); + expect(client?.['_integrations']).toEqual({}); }); it('installs merged default integrations, with overrides provided through options', () => { @@ -137,7 +146,7 @@ describe('init', () => { Object.defineProperty(WINDOW, 'browser', { value: undefined, writable: true }); Object.defineProperty(WINDOW, 'nw', { value: undefined, writable: true }); Object.defineProperty(WINDOW, 'window', { value: WINDOW, writable: true }); - vi.clearAllMocks(); + vi.restoreAllMocks(); }); it('logs a browser extension error if executed inside a Chrome extension', () => { @@ -154,8 +163,6 @@ describe('init', () => { expect(consoleErrorSpy).toHaveBeenCalledWith( '[Sentry] You cannot run Sentry this way in a browser extension, check: https://docs.sentry.io/platforms/javascript/best-practices/browser-extensions/', ); - - consoleErrorSpy.mockRestore(); }); it('logs a browser extension error if executed inside a Firefox/Safari extension', () => { @@ -169,8 +176,6 @@ describe('init', () => { expect(consoleErrorSpy).toHaveBeenCalledWith( '[Sentry] You cannot run Sentry this way in a browser extension, check: https://docs.sentry.io/platforms/javascript/best-practices/browser-extensions/', ); - - consoleErrorSpy.mockRestore(); }); it.each(['chrome-extension', 'moz-extension', 'ms-browser-extension', 'safari-web-extension'])( @@ -249,20 +254,41 @@ describe('init', () => { }); }); +describe('initWithDefaultIntegrations', () => { + afterEach(() => { + vi.restoreAllMocks(); + }); + + test('installs with provided getDefaultIntegrations function', () => { + const integration1 = new MockIntegration(SentryCore.uuid4()); + const integration2 = new MockIntegration(SentryCore.uuid4()); + const getDefaultIntegrations = vi.fn(() => [integration1, integration2]); + const options = getDefaultBrowserOptions({ dsn: PUBLIC_DSN }); + + const client = initWithDefaultIntegrations(options, getDefaultIntegrations); + + expect(getDefaultIntegrations).toHaveBeenCalledTimes(1); + expect(getDefaultIntegrations).toHaveBeenCalledWith(options); + + expect(client).toBeDefined(); + expect(client?.['_integrations']).toEqual({ + [integration1.name]: integration1, + [integration2.name]: integration2, + }); + expect(integration1.setupOnce).toHaveBeenCalledTimes(1); + expect(integration2.setupOnce).toHaveBeenCalledTimes(1); + }); +}); + describe('applyDefaultOptions', () => { test('it works with empty options', () => { const options = {}; const actual = applyDefaultOptions(options); expect(actual).toEqual({ - defaultIntegrations: expect.any(Array), release: undefined, sendClientReports: true, }); - - expect((actual.defaultIntegrations as { name: string }[]).map(i => i.name)).toEqual( - getDefaultIntegrations(options).map(i => i.name), - ); }); test('it works with options', () => { @@ -273,15 +299,10 @@ describe('applyDefaultOptions', () => { const actual = applyDefaultOptions(options); expect(actual).toEqual({ - defaultIntegrations: expect.any(Array), release: '1.0.0', sendClientReports: true, tracesSampleRate: 0.5, }); - - expect((actual.defaultIntegrations as { name: string }[]).map(i => i.name)).toEqual( - getDefaultIntegrations(options).map(i => i.name), - ); }); test('it works with defaultIntegrations=false', () => { @@ -309,7 +330,7 @@ describe('applyDefaultOptions', () => { const actual = applyDefaultOptions(options); // Not defined, not even undefined - expect('tracesSampleRate' in actual).toBe(false); + expect(actual.tracesSampleRate).toStrictEqual(undefined); }); test('it works with tracesSampleRate=null', () => { diff --git a/packages/bun/src/sdk.ts b/packages/bun/src/sdk.ts index 8bded2d492af..a282d009a9e8 100644 --- a/packages/bun/src/sdk.ts +++ b/packages/bun/src/sdk.ts @@ -1,16 +1,16 @@ +import type { Integration, Options } from '@sentry/core'; import { functionToStringIntegration, inboundFiltersIntegration, linkedErrorsIntegration, requestDataIntegration, } from '@sentry/core'; -import type { Integration, Options } from '@sentry/core'; import type { NodeClient } from '@sentry/node'; import { consoleIntegration, contextLinesIntegration, httpIntegration, - init as initNode, + initWithDefaultIntegrations, modulesIntegration, nativeNodeFetchIntegration, nodeContextIntegration, @@ -93,12 +93,11 @@ export function getDefaultIntegrations(_options: Options): Integration[] { * @see {@link BunOptions} for documentation on configuration options. */ export function init(options: BunOptions = {}): NodeClient | undefined { - options.clientClass = BunClient; - options.transport = options.transport || makeFetchTransport; - - if (options.defaultIntegrations === undefined) { - options.defaultIntegrations = getDefaultIntegrations(options); - } + const opts = { + transport: makeFetchTransport, + ...options, + clientClass: BunClient, + }; - return initNode(options); + return initWithDefaultIntegrations(opts, getDefaultIntegrations); } diff --git a/packages/cloudflare/src/sdk.ts b/packages/cloudflare/src/sdk.ts index 89f3fe99d050..82bfc92cd4f7 100644 --- a/packages/cloudflare/src/sdk.ts +++ b/packages/cloudflare/src/sdk.ts @@ -32,14 +32,10 @@ export function getDefaultIntegrations(options: CloudflareOptions): Integration[ * Initializes the cloudflare SDK. */ export function init(options: CloudflareOptions): CloudflareClient | undefined { - if (options.defaultIntegrations === undefined) { - options.defaultIntegrations = getDefaultIntegrations(options); - } - const clientOptions: CloudflareClientOptions = { ...options, stackParser: stackParserFromStackParserOptions(options.stackParser || defaultStackParser), - integrations: getIntegrationsToSetup(options), + integrations: getIntegrationsToSetup(options, getDefaultIntegrations(options)), transport: options.transport || makeCloudflareTransport, }; diff --git a/packages/core/src/integration.ts b/packages/core/src/integration.ts index 31d5426d4fbc..5fe7c7e24ee8 100644 --- a/packages/core/src/integration.ts +++ b/packages/core/src/integration.ts @@ -40,24 +40,42 @@ function filterDuplicates(integrations: Integration[]): Integration[] { } /** Gets integrations to install */ -export function getIntegrationsToSetup(options: Pick): Integration[] { - const defaultIntegrations = options.defaultIntegrations || []; +export function getIntegrationsToSetup( + options: Pick, + defaultIntegrations: Integration[] = [], +): Integration[] { const userIntegrations = options.integrations; + // User-defined defaultIntegrations + // TODO(v10): If an array is passed, we use this - this is deprecated and will eventually be removed + const passedDefaultIntegrations = Array.isArray(options.defaultIntegrations) + ? options.defaultIntegrations + : undefined; + + if (DEBUG_BUILD && passedDefaultIntegrations) { + logger.warn('Sentry: The `defaultIntegrations` option is deprecated. Use the `integrations` option instead.'); + } + + // If `defaultIntegrations: false` is defined, we disable all default integrations + + // Else, we use the default integrations that are directly passed to this function as second argument + const defaultIntegrationsToUse = + options.defaultIntegrations === false ? [] : passedDefaultIntegrations || defaultIntegrations; + // We flag default instances, so that later we can tell them apart from any user-created instances of the same class - defaultIntegrations.forEach((integration: IntegrationWithDefaultInstance) => { + defaultIntegrationsToUse.forEach((integration: IntegrationWithDefaultInstance) => { integration.isDefaultInstance = true; }); let integrations: Integration[]; if (Array.isArray(userIntegrations)) { - integrations = [...defaultIntegrations, ...userIntegrations]; + integrations = [...defaultIntegrationsToUse, ...userIntegrations]; } else if (typeof userIntegrations === 'function') { - const resolvedUserIntegrations = userIntegrations(defaultIntegrations); + const resolvedUserIntegrations = userIntegrations(defaultIntegrationsToUse); integrations = Array.isArray(resolvedUserIntegrations) ? resolvedUserIntegrations : [resolvedUserIntegrations]; } else { - integrations = defaultIntegrations; + integrations = defaultIntegrationsToUse; } return filterDuplicates(integrations); diff --git a/packages/core/src/sdk.ts b/packages/core/src/sdk.ts index 2665e91ec938..516da1c93652 100644 --- a/packages/core/src/sdk.ts +++ b/packages/core/src/sdk.ts @@ -14,10 +14,7 @@ export type ClientClass = new (option * @param clientClass The client class to instantiate. * @param options Options to pass to the client. */ -export function initAndBind( - clientClass: ClientClass, - options: O, -): Client { +export function initAndBind(clientClass: ClientClass, options: O): F { if (options.debug === true) { if (DEBUG_BUILD) { logger.enable(); diff --git a/packages/core/src/types-hoist/options.ts b/packages/core/src/types-hoist/options.ts index 8e52b32eacf7..59a2ca4d39ed 100644 --- a/packages/core/src/types-hoist/options.ts +++ b/packages/core/src/types-hoist/options.ts @@ -307,9 +307,12 @@ export interface Options /** * If this is set to false, default integrations will not be added, otherwise this will internally be set to the * recommended default integrations. + * + * It is deprecated to pass `Integrations[]` here. This capability will be removed in v10. + * + * TODO(v10): Remove `Integration[]` support. */ defaultIntegrations?: false | Integration[]; - /** * List of integrations that should be installed after SDK was initialized. * Accepts either a list of integrations or a function that receives diff --git a/packages/core/test/lib/integration.test.ts b/packages/core/test/lib/integration.test.ts index aa4be2432699..bbb3701bad60 100644 --- a/packages/core/test/lib/integration.test.ts +++ b/packages/core/test/lib/integration.test.ts @@ -33,7 +33,8 @@ class MockIntegration implements Integration { type TestCase = [ string, // test name - Options['defaultIntegrations'], // default integrations + Integration[] | undefined, // SDK-provided default intergations + Options['defaultIntegrations'], // user-provided defaultIntegrations Options['integrations'], // user-provided integrations Array, // expected results ]; @@ -46,31 +47,49 @@ describe('getIntegrationsToSetup', () => { const testCases: TestCase[] = [ // each test case is [testName, defaultIntegrations, userIntegrations, expectedResult] - ['no default integrations, no user integrations provided', false, undefined, []], - ['no default integrations, empty user-provided array', false, [], []], - ['no default integrations, user-provided array', false, userIntegrationsArray, ['CatchTreats']], - ['no default integrations, user-provided function', false, userIntegrationsFunction, ['CatchTreats']], - ['with default integrations, no user integrations provided', defaultIntegrations, undefined, ['ChaseSquirrels']], - ['with default integrations, empty user-provided array', defaultIntegrations, [], ['ChaseSquirrels']], + ['no default integrations, no user integrations provided', [], false, undefined, []], + ['no default integrations, empty user-provided array', [], false, [], []], + ['no default integrations, user-provided array', [], false, userIntegrationsArray, ['CatchTreats']], + ['no default integrations, user-provided function', [], false, userIntegrationsFunction, ['CatchTreats']], + [ + 'with default integrations, no user integrations provided', + defaultIntegrations, + undefined, + undefined, + ['ChaseSquirrels'], + ], + [ + 'with custom defaultIntegrations, no user integrations provided', + [], + defaultIntegrations, + undefined, + ['ChaseSquirrels'], + ], + ['with default integrations, empty user-provided array', defaultIntegrations, undefined, [], ['ChaseSquirrels']], [ 'with default integrations, user-provided array', defaultIntegrations, + undefined, userIntegrationsArray, ['ChaseSquirrels', 'CatchTreats'], ], [ 'with default integrations, user-provided function', defaultIntegrations, + undefined, userIntegrationsFunction, ['ChaseSquirrels', 'CatchTreats'], ], ]; - test.each(testCases)('%s', (_, defaultIntegrations, userIntegrations, expected) => { - const integrations = getIntegrationsToSetup({ - defaultIntegrations, - integrations: userIntegrations, - }); + test.each(testCases)('%s', (_, sdkDefaultIntegrations, defaultIntegrations, userIntegrations, expected) => { + const integrations = getIntegrationsToSetup( + { + defaultIntegrations, + integrations: userIntegrations, + }, + sdkDefaultIntegrations, + ); expect(integrations.map(i => i.name)).toEqual(expected); }); }); @@ -113,10 +132,11 @@ describe('getIntegrationsToSetup', () => { ]; const testCases: TestCase[] = [ - // each test case is [testName, defaultIntegrations, userIntegrations, expectedResult] + // each test case is [testName, defaultIntegrations, userDefaultIntergations, userIntegrations, expectedResult] [ 'duplicate default integrations', duplicateDefaultIntegrations, + undefined, userIntegrationsArray, [ ['ChaseSquirrels', 'defaultB'], @@ -126,6 +146,7 @@ describe('getIntegrationsToSetup', () => { [ 'duplicate user integrations, user-provided array', defaultIntegrations, + undefined, duplicateUserIntegrationsArray, [ ['ChaseSquirrels', 'defaultA'], @@ -135,6 +156,7 @@ describe('getIntegrationsToSetup', () => { [ 'duplicate user integrations, user-provided function with defaults first', defaultIntegrations, + undefined, duplicateUserIntegrationsFunctionDefaultsFirst, [ ['ChaseSquirrels', 'defaultA'], @@ -144,6 +166,7 @@ describe('getIntegrationsToSetup', () => { [ 'duplicate user integrations, user-provided function with defaults second', defaultIntegrations, + undefined, duplicateUserIntegrationsFunctionDefaultsSecond, [ ['CatchTreats', 'userB'], @@ -153,6 +176,7 @@ describe('getIntegrationsToSetup', () => { [ 'same integration in default and user integrations, user-provided array', defaultIntegrations, + undefined, userIntegrationsMatchingDefaultsArray, [ ['ChaseSquirrels', 'userA'], @@ -162,6 +186,7 @@ describe('getIntegrationsToSetup', () => { [ 'same integration in default and user integrations, user-provided function with defaults first', defaultIntegrations, + undefined, userIntegrationsMatchingDefaultsFunctionDefaultsFirst, [ ['ChaseSquirrels', 'userA'], @@ -171,6 +196,7 @@ describe('getIntegrationsToSetup', () => { [ 'same integration in default and user integrations, user-provided function with defaults second', defaultIntegrations, + undefined, userIntegrationsMatchingDefaultsFunctionDefaultsSecond, [ ['ChaseSquirrels', 'userA'], @@ -179,11 +205,13 @@ describe('getIntegrationsToSetup', () => { ], ]; - test.each(testCases)('%s', (_, defaultIntegrations, userIntegrations, expected) => { - const integrations = getIntegrationsToSetup({ - defaultIntegrations: defaultIntegrations, - integrations: userIntegrations, - }) as MockIntegration[]; + test.each(testCases)('%s', (_, defaultIntegrations, _defaultIntegrations, userIntegrations, expected) => { + const integrations = getIntegrationsToSetup( + { + integrations: userIntegrations, + }, + defaultIntegrations, + ) as MockIntegration[]; expect(integrations.map(i => [i.name, i.tag])).toEqual(expected); }); diff --git a/packages/deno/src/sdk.ts b/packages/deno/src/sdk.ts index 25dc550fc353..3ea413676010 100644 --- a/packages/deno/src/sdk.ts +++ b/packages/deno/src/sdk.ts @@ -84,14 +84,12 @@ const defaultStackParser: StackParser = createStackParser(nodeStackLineParser()) * @see {@link DenoOptions} for documentation on configuration options. */ export function init(options: DenoOptions = {}): Client { - if (options.defaultIntegrations === undefined) { - options.defaultIntegrations = getDefaultIntegrations(options); - } + const defaultIntegrations = getDefaultIntegrations(options); const clientOptions: ServerRuntimeClientOptions = { ...options, stackParser: stackParserFromStackParserOptions(options.stackParser || defaultStackParser), - integrations: getIntegrationsToSetup(options), + integrations: getIntegrationsToSetup(options, defaultIntegrations), transport: options.transport || makeFetchTransport, }; diff --git a/packages/google-cloud-serverless/src/sdk.ts b/packages/google-cloud-serverless/src/sdk.ts index c7d4d7c9c50f..dddb0cd0d512 100644 --- a/packages/google-cloud-serverless/src/sdk.ts +++ b/packages/google-cloud-serverless/src/sdk.ts @@ -1,7 +1,7 @@ import type { Integration, Options } from '@sentry/core'; import { applySdkMetadata } from '@sentry/core'; import type { NodeClient, NodeOptions } from '@sentry/node'; -import { getDefaultIntegrationsWithoutPerformance, init as initNode } from '@sentry/node'; +import { getDefaultIntegrationsWithoutPerformance, initWithDefaultIntegrations } from '@sentry/node'; import { googleCloudGrpcIntegration } from './integrations/google-cloud-grpc'; import { googleCloudHttpIntegration } from './integrations/google-cloud-http'; @@ -29,11 +29,10 @@ export function getDefaultIntegrations(_options: Options): Integration[] { */ export function init(options: NodeOptions = {}): NodeClient | undefined { const opts = { - defaultIntegrations: getDefaultIntegrations(options), ...options, }; applySdkMetadata(opts, 'google-cloud-serverless'); - return initNode(opts); + return initWithDefaultIntegrations(opts, getDefaultIntegrations); } diff --git a/packages/google-cloud-serverless/test/gcpfunction/http.test.ts b/packages/google-cloud-serverless/test/gcpfunction/http.test.ts index 7f456237ad9c..d531e5ac77da 100644 --- a/packages/google-cloud-serverless/test/gcpfunction/http.test.ts +++ b/packages/google-cloud-serverless/test/gcpfunction/http.test.ts @@ -25,8 +25,8 @@ jest.mock('@sentry/node', () => { const original = jest.requireActual('@sentry/node'); return { ...original, - init: (options: unknown) => { - mockInit(options); + initWithDefaultIntegrations: (options: unknown, getDefaultIntergations: unknown) => { + mockInit(options, getDefaultIntergations); }, startSpanManual: (...args: unknown[]) => { mockStartSpanManual(...args); @@ -171,10 +171,9 @@ describe('GCPFunction', () => { await handleHttp(wrappedHandler); - const initOptions = (mockInit as unknown as jest.SpyInstance).mock.calls[0]; - const defaultIntegrations = initOptions[0]?.defaultIntegrations.map((i: Integration) => i.name); - - expect(defaultIntegrations).toContain('RequestData'); + const getDefaultIntegrationsFn = mockInit.mock.calls[0][1] as () => Integration[]; + const integrationNames = getDefaultIntegrationsFn().map(i => i.name); + expect(integrationNames).toContain('RequestData'); expect(mockScope.setSDKProcessingMetadata).toHaveBeenCalledWith({ normalizedRequest: { diff --git a/packages/google-cloud-serverless/test/sdk.test.ts b/packages/google-cloud-serverless/test/sdk.test.ts index ee4a1fc6fa17..000793d4c146 100644 --- a/packages/google-cloud-serverless/test/sdk.test.ts +++ b/packages/google-cloud-serverless/test/sdk.test.ts @@ -6,7 +6,7 @@ jest.mock('@sentry/node', () => { const original = jest.requireActual('@sentry/node'); return { ...original, - init: (options: unknown) => { + initWithDefaultIntegrations: (options: unknown) => { mockInit(options); }, }; diff --git a/packages/nestjs/src/sdk.ts b/packages/nestjs/src/sdk.ts index d9c00369e8b3..37fa148a4deb 100644 --- a/packages/nestjs/src/sdk.ts +++ b/packages/nestjs/src/sdk.ts @@ -1,12 +1,12 @@ +import type { Integration } from '@sentry/core'; import { SEMANTIC_ATTRIBUTE_SENTRY_OP, SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN, applySdkMetadata, spanToJSON, } from '@sentry/core'; -import type { Integration } from '@sentry/core'; import type { NodeClient, NodeOptions, Span } from '@sentry/node'; -import { getDefaultIntegrations as getDefaultNodeIntegrations, init as nodeInit } from '@sentry/node'; +import { getDefaultIntegrations as getDefaultNodeIntegrations, initWithDefaultIntegrations } from '@sentry/node'; import { nestIntegration } from './integrations/nest'; /** @@ -14,13 +14,12 @@ import { nestIntegration } from './integrations/nest'; */ export function init(options: NodeOptions | undefined = {}): NodeClient | undefined { const opts: NodeOptions = { - defaultIntegrations: getDefaultIntegrations(options), ...options, }; applySdkMetadata(opts, 'nestjs'); - const client = nodeInit(opts); + const client = initWithDefaultIntegrations(opts, getDefaultIntegrations); if (client) { client.on('spanStart', span => { @@ -33,7 +32,7 @@ export function init(options: NodeOptions | undefined = {}): NodeClient | undefi } /** Get the default integrations for the NestJS SDK. */ -export function getDefaultIntegrations(options: NodeOptions): Integration[] | undefined { +export function getDefaultIntegrations(options: NodeOptions): Integration[] { return [nestIntegration(), ...getDefaultNodeIntegrations(options)]; } diff --git a/packages/nestjs/test/sdk.test.ts b/packages/nestjs/test/sdk.test.ts index 0f61ee249261..33212088d908 100644 --- a/packages/nestjs/test/sdk.test.ts +++ b/packages/nestjs/test/sdk.test.ts @@ -5,7 +5,7 @@ import * as SentryNode from '@sentry/node'; import { init as nestInit } from '../src/sdk'; -const nodeInit = vi.spyOn(SentryNode, 'init'); +const nodeInit = vi.spyOn(SentryNode, 'initWithDefaultIntegrations'); const PUBLIC_DSN = 'https://username@domain/123'; describe('Initialize Nest SDK', () => { @@ -30,6 +30,6 @@ describe('Initialize Nest SDK', () => { expect(client).not.toBeUndefined(); expect(nodeInit).toHaveBeenCalledTimes(1); - expect(nodeInit).toHaveBeenLastCalledWith(expect.objectContaining(expectedMetadata)); + expect(nodeInit).toHaveBeenLastCalledWith(expect.objectContaining(expectedMetadata), expect.any(Function)); }); }); diff --git a/packages/nextjs/src/client/index.ts b/packages/nextjs/src/client/index.ts index 163e29f0b9a7..d50246124bd8 100644 --- a/packages/nextjs/src/client/index.ts +++ b/packages/nextjs/src/client/index.ts @@ -1,7 +1,7 @@ import type { Client, EventProcessor, Integration } from '@sentry/core'; import { GLOBAL_OBJ, addEventProcessor, applySdkMetadata } from '@sentry/core'; import type { BrowserOptions } from '@sentry/react'; -import { getDefaultIntegrations as getReactDefaultIntegrations, init as reactInit } from '@sentry/react'; +import { getDefaultIntegrations as getReactDefaultIntegrations, initWithDefaultIntegrations } from '@sentry/react'; import { devErrorSymbolicationEventProcessor } from '../common/devErrorSymbolicationEventProcessor'; import { getVercelEnv } from '../common/getVercelEnv'; import { isRedirectNavigationError } from '../common/nextNavigationErrorUtils'; @@ -26,14 +26,13 @@ declare const __SENTRY_TRACING__: boolean; export function init(options: BrowserOptions): Client | undefined { const opts = { environment: getVercelEnv(true) || process.env.NODE_ENV, - defaultIntegrations: getDefaultIntegrations(options), ...options, } satisfies BrowserOptions; applyTunnelRouteOption(opts); applySdkMetadata(opts, 'nextjs', ['nextjs', 'react']); - const client = reactInit(opts); + const client = initWithDefaultIntegrations(opts, getDefaultIntegrations); const filterTransactions: EventProcessor = event => event.type === 'transaction' && event.transaction === '/404' ? null : event; diff --git a/packages/nextjs/src/edge/index.ts b/packages/nextjs/src/edge/index.ts index 24ec193df0d1..493c913c0a4d 100644 --- a/packages/nextjs/src/edge/index.ts +++ b/packages/nextjs/src/edge/index.ts @@ -46,7 +46,6 @@ export function init(options: VercelEdgeOptions = {}): void { } const opts = { - defaultIntegrations: customDefaultIntegrations, ...options, }; diff --git a/packages/nextjs/src/index.types.ts b/packages/nextjs/src/index.types.ts index 7b6173f9c8ea..d47bed320b1a 100644 --- a/packages/nextjs/src/index.types.ts +++ b/packages/nextjs/src/index.types.ts @@ -18,6 +18,10 @@ import type * as serverSdk from './server'; export declare function init( options: Options | clientSdk.BrowserOptions | serverSdk.NodeOptions | edgeSdk.EdgeOptions, ): Client | undefined; +export declare function initWithDefaultIntegrations( + options: Options | clientSdk.BrowserOptions | serverSdk.NodeOptions | edgeSdk.EdgeOptions, + getDefaultIntegrations: (options: Options) => Integration[], +): Client | undefined; export declare const linkedErrorsIntegration: typeof clientSdk.linkedErrorsIntegration; export declare const contextLinesIntegration: typeof clientSdk.contextLinesIntegration; diff --git a/packages/nextjs/src/server/index.ts b/packages/nextjs/src/server/index.ts index bc5cbb50893d..6a9f3c594a6c 100644 --- a/packages/nextjs/src/server/index.ts +++ b/packages/nextjs/src/server/index.ts @@ -6,24 +6,27 @@ import { SEMATTRS_HTTP_METHOD, SEMATTRS_HTTP_TARGET, } from '@opentelemetry/semantic-conventions'; +import type { EventProcessor, Integration } from '@sentry/core'; import { + GLOBAL_OBJ, SEMANTIC_ATTRIBUTE_SENTRY_OP, SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN, SEMANTIC_ATTRIBUTE_SENTRY_SOURCE, applySdkMetadata, + extractTraceparentData, getCapturedScopesOnSpan, getClient, getCurrentScope, getGlobalScope, getIsolationScope, getRootSpan, + logger, setCapturedScopesOnSpan, spanToJSON, + stripUrlQueryAndFragment, } from '@sentry/core'; -import { GLOBAL_OBJ, extractTraceparentData, logger, stripUrlQueryAndFragment } from '@sentry/core'; -import type { EventProcessor } from '@sentry/core'; import type { NodeClient, NodeOptions } from '@sentry/node'; -import { getDefaultIntegrations, httpIntegration, init as nodeInit } from '@sentry/node'; +import { getDefaultIntegrations, httpIntegration, initWithDefaultIntegrations } from '@sentry/node'; import { getScopesFromContext } from '@sentry/opentelemetry'; import { DEBUG_BUILD } from '../common/debug-build'; import { devErrorSymbolicationEventProcessor } from '../common/devErrorSymbolicationEventProcessor'; @@ -37,8 +40,9 @@ import { isBuild } from '../common/utils/isBuild'; import { distDirRewriteFramesIntegration } from './distDirRewriteFramesIntegration'; export * from '@sentry/node'; - +export * from '../common'; export { captureUnderscoreErrorException } from '../common/pages-router-instrumentation/_error'; +export { wrapApiHandlerWithSentry } from '../common/pages-router-instrumentation/wrapApiHandlerWithSentry'; const globalWithInjectedValues = GLOBAL_OBJ as typeof GLOBAL_OBJ & { _sentryRewriteFramesDistDir?: string; @@ -93,29 +97,8 @@ export function init(options: NodeOptions): NodeClient | undefined { return; } - const customDefaultIntegrations = getDefaultIntegrations(options) - .filter(integration => integration.name !== 'Http') - .concat( - // We are using the HTTP integration without instrumenting incoming HTTP requests because Next.js does that by itself. - httpIntegration({ - disableIncomingRequestSpans: true, - }), - ); - - // Turn off Next.js' own fetch instrumentation - // https://github.com/lforst/nextjs-fork/blob/1994fd186defda77ad971c36dc3163db263c993f/packages/next/src/server/lib/patch-fetch.ts#L245 - process.env.NEXT_OTEL_FETCH_DISABLED = '1'; - - // This value is injected at build time, based on the output directory specified in the build config. Though a default - // is set there, we set it here as well, just in case something has gone wrong with the injection. - const distDirName = process.env._sentryRewriteFramesDistDir || globalWithInjectedValues._sentryRewriteFramesDistDir; - if (distDirName) { - customDefaultIntegrations.push(distDirRewriteFramesIntegration({ distDirName })); - } - const opts: NodeOptions = { environment: process.env.SENTRY_ENVIRONMENT || getVercelEnv(false) || process.env.NODE_ENV, - defaultIntegrations: customDefaultIntegrations, ...options, }; @@ -132,7 +115,11 @@ export function init(options: NodeOptions): NodeClient | undefined { applySdkMetadata(opts, 'nextjs', ['nextjs', 'node']); - const client = nodeInit(opts); + // Turn off Next.js' own fetch instrumentation + // https://github.com/lforst/nextjs-fork/blob/1994fd186defda77ad971c36dc3163db263c993f/packages/next/src/server/lib/patch-fetch.ts#L245 + process.env.NEXT_OTEL_FETCH_DISABLED = '1'; + + const client = initWithDefaultIntegrations(opts, getNextDefaultIntegrations); client?.on('beforeSampling', ({ spanAttributes }, samplingDecision) => { // There are situations where the Next.js Node.js server forwards requests for the Edge Runtime server (e.g. in // middleware) and this causes spans for Sentry ingest requests to be created. These are not exempt from our tracing @@ -366,6 +353,22 @@ function sdkAlreadyInitialized(): boolean { return !!getClient(); } -export * from '../common'; +function getNextDefaultIntegrations(options: NodeOptions): Integration[] { + const customDefaultIntegrations = getDefaultIntegrations(options) + .filter(integration => integration.name !== 'Http') + .concat( + // We are using the HTTP integration without instrumenting incoming HTTP requests because Next.js does that by itself. + httpIntegration({ + disableIncomingRequestSpans: true, + }), + ); -export { wrapApiHandlerWithSentry } from '../common/pages-router-instrumentation/wrapApiHandlerWithSentry'; + // This value is injected at build time, based on the output directory specified in the build config. Though a default + // is set there, we set it here as well, just in case something has gone wrong with the injection. + const distDirName = process.env._sentryRewriteFramesDistDir || globalWithInjectedValues._sentryRewriteFramesDistDir; + if (distDirName) { + customDefaultIntegrations.push(distDirRewriteFramesIntegration({ distDirName })); + } + + return customDefaultIntegrations; +} diff --git a/packages/nextjs/test/clientSdk.test.ts b/packages/nextjs/test/clientSdk.test.ts index 3464c5de3c93..2e7c67574b3c 100644 --- a/packages/nextjs/test/clientSdk.test.ts +++ b/packages/nextjs/test/clientSdk.test.ts @@ -7,7 +7,7 @@ import { JSDOM } from 'jsdom'; import { breadcrumbsIntegration, browserTracingIntegration, init } from '../src/client'; -const reactInit = jest.spyOn(SentryReact, 'init'); +const reactInit = jest.spyOn(SentryReact, 'initWithDefaultIntegrations'); const loggerLogSpy = jest.spyOn(logger, 'log'); // We're setting up JSDom here because the Next.js routing instrumentations requires a few things to be present on pageload: @@ -69,13 +69,13 @@ describe('Client init()', () => { }, }, environment: 'test', - defaultIntegrations: expect.arrayContaining([ - expect.objectContaining({ - name: 'NextjsClientStackFrameNormalization', - }), - ]), }), + expect.any(Function), ); + + const getDefaultIntegrationsFn = reactInit.mock.calls[0]?.[1] as () => Integration[]; + const integrationNames = getDefaultIntegrationsFn().map(i => i.name); + expect(integrationNames).toContain('NextjsClientStackFrameNormalization'); }); it('adds 404 transaction filter', () => { diff --git a/packages/nextjs/test/serverSdk.test.ts b/packages/nextjs/test/serverSdk.test.ts index 17e46e0f90e5..148c034b05a2 100644 --- a/packages/nextjs/test/serverSdk.test.ts +++ b/packages/nextjs/test/serverSdk.test.ts @@ -1,5 +1,4 @@ import { GLOBAL_OBJ } from '@sentry/core'; -import type { Integration } from '@sentry/core'; import { getCurrentScope } from '@sentry/node'; import * as SentryNode from '@sentry/node'; @@ -8,11 +7,7 @@ import { init } from '../src/server'; // normally this is set as part of the build process, so mock it here (GLOBAL_OBJ as typeof GLOBAL_OBJ & { _sentryRewriteFramesDistDir: string })._sentryRewriteFramesDistDir = '.next'; -const nodeInit = jest.spyOn(SentryNode, 'init'); - -function findIntegrationByName(integrations: Integration[] = [], name: string): Integration | undefined { - return integrations.find(integration => integration.name === name); -} +const nodeInit = jest.spyOn(SentryNode, 'initWithDefaultIntegrations'); describe('Server init()', () => { afterEach(() => { @@ -49,15 +44,8 @@ describe('Server init()', () => { }, }, environment: 'test', - - // Integrations are tested separately, and we can't be more specific here without depending on the order in - // which integrations appear in the array, which we can't guarantee. - // - // TODO: If we upgrade to Jest 28+, we can follow Jest's example matcher and create an - // `expect.ArrayContainingInAnyOrder`. See - // https://github.com/facebook/jest/blob/main/examples/expect-extend/toBeWithinRange.ts. - defaultIntegrations: expect.any(Array), }), + expect.any(Function), ); }); @@ -85,29 +73,23 @@ describe('Server init()', () => { }); describe('integrations', () => { - // Options passed by `@sentry/nextjs`'s `init` to `@sentry/node`'s `init` after modifying them - type ModifiedInitOptions = { integrations: Integration[]; defaultIntegrations: Integration[] }; - it('adds default integrations', () => { - init({}); + const client = init({ dsn: 'http://examplePublicKey@localhost/1' }); - const nodeInitOptions = nodeInit.mock.calls[0]?.[0] as ModifiedInitOptions; - const integrationNames = nodeInitOptions.defaultIntegrations.map(integration => integration.name); - const onUncaughtExceptionIntegration = findIntegrationByName( - nodeInitOptions.defaultIntegrations, - 'OnUncaughtException', - ); + const onUncaughtExceptionIntegration = client?.getIntegrationByName('OnUncaughtException'); + const rewriteFramesIntegration = client?.getIntegrationByName('DistDirRewriteFrames'); - expect(integrationNames).toContain('DistDirRewriteFrames'); + expect(rewriteFramesIntegration).toBeDefined(); expect(onUncaughtExceptionIntegration).toBeDefined(); }); it('supports passing unrelated integrations through options', () => { - init({ integrations: [SentryNode.consoleIntegration()] }); - - const nodeInitOptions = nodeInit.mock.calls[0]?.[0] as ModifiedInitOptions; - const consoleIntegration = findIntegrationByName(nodeInitOptions.integrations, 'Console'); + const client = init({ + dsn: 'http://examplePublicKey@localhost/1', + integrations: [SentryNode.consoleIntegration()], + }); + const consoleIntegration = client?.getIntegrationByName('Console'); expect(consoleIntegration).toBeDefined(); }); }); diff --git a/packages/node/src/index.ts b/packages/node/src/index.ts index 6493e0280b3b..ec14be2435b3 100644 --- a/packages/node/src/index.ts +++ b/packages/node/src/index.ts @@ -41,7 +41,9 @@ export { init, getDefaultIntegrations, getDefaultIntegrationsWithoutPerformance, + // eslint-disable-next-line deprecation/deprecation initWithoutDefaultIntegrations, + initWithDefaultIntegrations, validateOpenTelemetrySetup, } from './sdk'; export { initOpenTelemetry, preloadOpenTelemetry } from './sdk/initOtel'; diff --git a/packages/node/src/sdk/index.ts b/packages/node/src/sdk/index.ts index 7b9f98ed7461..46ada06cb868 100644 --- a/packages/node/src/sdk/index.ts +++ b/packages/node/src/sdk/index.ts @@ -73,7 +73,7 @@ export function getDefaultIntegrationsWithoutPerformance(): Integration[] { } /** Get the default integrations for the Node SDK. */ -export function getDefaultIntegrations(options: Options): Integration[] { +export function getDefaultIntegrations(options: NodeOptions): Integration[] { return [ ...getDefaultIntegrationsWithoutPerformance(), // We only add performance integrations if tracing is enabled @@ -88,22 +88,27 @@ export function getDefaultIntegrations(options: Options): Integration[] { * Initialize Sentry for Node. */ export function init(options: NodeOptions | undefined = {}): NodeClient | undefined { - return _init(options, getDefaultIntegrations); + return initWithDefaultIntegrations(options, getDefaultIntegrations); } /** * Initialize Sentry for Node, without any integrations added by default. */ export function initWithoutDefaultIntegrations(options: NodeOptions | undefined = {}): NodeClient { - return _init(options, () => []); + return initWithDefaultIntegrations(options, () => []); } /** - * Initialize Sentry for Node, without performance instrumentation. + * Initialize a Node client with the provided options and default integrations getter function. + * This is an internal method the SDK uses under the hood to set up things - you should not use this as a user! + * Instead, use `init()` to initialize the SDK. + * + * @hidden + * @internal */ -function _init( +export function initWithDefaultIntegrations( _options: NodeOptions | undefined = {}, - getDefaultIntegrationsImpl: (options: Options) => Integration[], + getDefaultIntegrationsImpl: (options: NodeOptions) => Integration[], ): NodeClient { const options = getClientOptions(_options, getDefaultIntegrationsImpl); @@ -229,17 +234,12 @@ function getClientOptions( ...overwriteOptions, }; - if (options.defaultIntegrations === undefined) { - options.defaultIntegrations = getDefaultIntegrationsImpl(mergedOptions); - } + const defaultIntegrations = getDefaultIntegrationsImpl(mergedOptions); const clientOptions: NodeClientOptions = { ...mergedOptions, stackParser: stackParserFromStackParserOptions(options.stackParser || defaultStackParser), - integrations: getIntegrationsToSetup({ - defaultIntegrations: options.defaultIntegrations, - integrations: options.integrations, - }), + integrations: getIntegrationsToSetup(mergedOptions, defaultIntegrations), }; return clientOptions; diff --git a/packages/nuxt/src/client/sdk.ts b/packages/nuxt/src/client/sdk.ts index 45c02583cbfc..9e6fe6403009 100644 --- a/packages/nuxt/src/client/sdk.ts +++ b/packages/nuxt/src/client/sdk.ts @@ -1,4 +1,4 @@ -import { getDefaultIntegrations as getBrowserDefaultIntegrations, init as initBrowser } from '@sentry/browser'; +import { init as initBrowser } from '@sentry/browser'; import { applySdkMetadata } from '@sentry/core'; import type { Client } from '@sentry/core'; import type { SentryNuxtClientOptions } from '../common/types'; @@ -10,8 +10,6 @@ import type { SentryNuxtClientOptions } from '../common/types'; */ export function init(options: SentryNuxtClientOptions): Client | undefined { const sentryOptions = { - /* BrowserTracing is added later with the Nuxt client plugin */ - defaultIntegrations: [...getBrowserDefaultIntegrations(options)], ...options, }; diff --git a/packages/nuxt/src/index.types.ts b/packages/nuxt/src/index.types.ts index dc9bf360af9e..b4467bb2ed13 100644 --- a/packages/nuxt/src/index.types.ts +++ b/packages/nuxt/src/index.types.ts @@ -10,6 +10,10 @@ export * from './index.server'; // re-export colliding types export declare function init(options: Options | SentryNuxtClientOptions | serverSdk.NodeOptions): Client | undefined; +export declare function initWithDefaultIntegrations( + options: Options | SentryNuxtClientOptions | serverSdk.NodeOptions, + getDefaultIntegrations: (options: Options) => Integration[], +): Client | undefined; export declare const linkedErrorsIntegration: typeof clientSdk.linkedErrorsIntegration; export declare const contextLinesIntegration: typeof clientSdk.contextLinesIntegration; export declare const getDefaultIntegrations: (options: Options) => Integration[]; diff --git a/packages/nuxt/src/server/sdk.ts b/packages/nuxt/src/server/sdk.ts index 9eaa2f274818..43dec804bda5 100644 --- a/packages/nuxt/src/server/sdk.ts +++ b/packages/nuxt/src/server/sdk.ts @@ -5,7 +5,7 @@ import { type NodeOptions, getDefaultIntegrations as getDefaultNodeIntegrations, httpIntegration, - init as initNode, + initWithDefaultIntegrations, } from '@sentry/node'; import { DEBUG_BUILD } from '../common/debug-build'; import type { SentryNuxtServerOptions } from '../common/types'; @@ -18,12 +18,11 @@ import type { SentryNuxtServerOptions } from '../common/types'; export function init(options: SentryNuxtServerOptions): Client | undefined { const sentryOptions = { ...options, - defaultIntegrations: getNuxtDefaultIntegrations(options), }; applySdkMetadata(sentryOptions, 'nuxt', ['nuxt', 'node']); - const client = initNode(sentryOptions); + const client = initWithDefaultIntegrations(sentryOptions, getNuxtDefaultIntegrations); getGlobalScope().addEventProcessor(lowQualityTransactionsFilter(options)); getGlobalScope().addEventProcessor(clientSourceMapErrorFilter(options)); diff --git a/packages/nuxt/test/server/sdk.test.ts b/packages/nuxt/test/server/sdk.test.ts index e5c1a58d15c3..e90a5edc9df1 100644 --- a/packages/nuxt/test/server/sdk.test.ts +++ b/packages/nuxt/test/server/sdk.test.ts @@ -8,7 +8,7 @@ import { beforeEach, describe, expect, it, vi } from 'vitest'; import { init } from '../../src/server'; import { clientSourceMapErrorFilter } from '../../src/server/sdk'; -const nodeInit = vi.spyOn(SentryNode, 'init'); +const nodeInit = vi.spyOn(SentryNode, 'initWithDefaultIntegrations'); describe('Nuxt Server SDK', () => { describe('init', () => { @@ -37,7 +37,7 @@ describe('Nuxt Server SDK', () => { }; expect(nodeInit).toHaveBeenCalledTimes(1); - expect(nodeInit).toHaveBeenLastCalledWith(expect.objectContaining(expectedMetadata)); + expect(nodeInit).toHaveBeenLastCalledWith(expect.objectContaining(expectedMetadata), expect.any(Function)); }); it('returns client from init', () => { diff --git a/packages/react/src/index.ts b/packages/react/src/index.ts index ae441a66837b..9ce4fbaed1a8 100644 --- a/packages/react/src/index.ts +++ b/packages/react/src/index.ts @@ -1,6 +1,6 @@ export * from '@sentry/browser'; -export { init } from './sdk'; +export { init, initWithDefaultIntegrations } from './sdk'; export { reactErrorHandler } from './error'; export { Profiler, withProfiler, useProfiler } from './profiler'; export type { ErrorBoundaryProps, FallbackRender } from './errorboundary'; diff --git a/packages/react/src/sdk.ts b/packages/react/src/sdk.ts index da63e6d9aa08..6db882036fa1 100644 --- a/packages/react/src/sdk.ts +++ b/packages/react/src/sdk.ts @@ -1,7 +1,11 @@ import type { BrowserOptions } from '@sentry/browser'; -import { init as browserInit, setContext } from '@sentry/browser'; +import { + init as browserInit, + initWithDefaultIntegrations as browserInitWithDefaultIntegrations, + setContext, +} from '@sentry/browser'; +import type { Client, Integration } from '@sentry/core'; import { applySdkMetadata } from '@sentry/core'; -import type { Client } from '@sentry/core'; import { version } from 'react'; @@ -17,3 +21,24 @@ export function init(options: BrowserOptions): Client | undefined { setContext('react', { version }); return browserInit(opts); } + +/** + * Initialize a React client with the provided options and default integrations getter function. + * This is an internal method the SDK uses under the hood to set up things - you should not use this as a user! + * Instead, use `init()` to initialize the SDK. + * + * @hidden + * @internal + */ +export function initWithDefaultIntegrations( + options: BrowserOptions, + defaultIntegrations: (options: BrowserOptions) => Integration[], +): Client | undefined { + const opts = { + ...options, + }; + + applySdkMetadata(opts, 'react'); + setContext('react', { version }); + return browserInitWithDefaultIntegrations(opts, defaultIntegrations); +} diff --git a/packages/remix/src/index.server.ts b/packages/remix/src/index.server.ts index 9084284217a9..658080664c41 100644 --- a/packages/remix/src/index.server.ts +++ b/packages/remix/src/index.server.ts @@ -1,7 +1,8 @@ import { applySdkMetadata, logger } from '@sentry/core'; import type { Integration } from '@sentry/core'; import type { NodeClient, NodeOptions } from '@sentry/node'; -import { getDefaultIntegrations as getDefaultNodeIntegrations, init as nodeInit, isInitialized } from '@sentry/node'; +import { initWithDefaultIntegrations } from '@sentry/node'; +import { getDefaultIntegrations as getDefaultNodeIntegrations, isInitialized } from '@sentry/node'; import { DEBUG_BUILD } from './utils/debug-build'; import { instrumentServer } from './utils/instrumentServer'; @@ -155,17 +156,17 @@ export function getRemixDefaultIntegrations(options: RemixOptions): Integration[ /** Initializes Sentry Remix SDK on Node. */ export function init(options: RemixOptions): NodeClient | undefined { - applySdkMetadata(options, 'remix', ['remix', 'node']); - if (isInitialized()) { DEBUG_BUILD && logger.log('SDK already initialized'); - return; } - options.defaultIntegrations = getRemixDefaultIntegrations(options as NodeOptions); + const opts = { + ...options, + }; + applySdkMetadata(opts, 'remix', ['remix', 'node']); - const client = nodeInit(options as NodeOptions); + const client = initWithDefaultIntegrations(opts, getRemixDefaultIntegrations); instrumentServer(); diff --git a/packages/remix/src/index.types.ts b/packages/remix/src/index.types.ts index 5cfb7114bbbc..41eb22e1fd84 100644 --- a/packages/remix/src/index.types.ts +++ b/packages/remix/src/index.types.ts @@ -11,6 +11,10 @@ import type { RemixOptions } from './utils/remixOptions'; /** Initializes Sentry Remix SDK */ export declare function init(options: RemixOptions): Client | undefined; +export declare function initWithDefaultIntegrations( + options: RemixOptions, + getDefaultIntegrations: (options: Options) => Integration[], +): Client | undefined; export declare const linkedErrorsIntegration: typeof clientSdk.linkedErrorsIntegration; export declare const contextLinesIntegration: typeof clientSdk.contextLinesIntegration; diff --git a/packages/remix/test/index.server.test.ts b/packages/remix/test/index.server.test.ts index b710e295ed1e..f909ea9895c1 100644 --- a/packages/remix/test/index.server.test.ts +++ b/packages/remix/test/index.server.test.ts @@ -9,7 +9,7 @@ jest.mock('@sentry/node', () => { }; }); -const nodeInit = jest.spyOn(SentryNode, 'init'); +const nodeInit = jest.spyOn(SentryNode, 'initWithDefaultIntegrations'); describe('Server init()', () => { afterEach(() => { @@ -44,6 +44,7 @@ describe('Server init()', () => { }, }, }), + expect.any(Function), ); }); diff --git a/packages/solid/src/index.ts b/packages/solid/src/index.ts index 8a05327bcf0b..22e459c33517 100644 --- a/packages/solid/src/index.ts +++ b/packages/solid/src/index.ts @@ -1,5 +1,5 @@ export * from '@sentry/browser'; -export { init } from './sdk'; +export { init, initWithDefaultIntegrations } from './sdk'; export * from './errorboundary'; diff --git a/packages/solid/src/sdk.ts b/packages/solid/src/sdk.ts index b17a42051474..ea0c49d13eb7 100644 --- a/packages/solid/src/sdk.ts +++ b/packages/solid/src/sdk.ts @@ -1,17 +1,35 @@ -import type { BrowserOptions } from '@sentry/browser'; -import { init as browserInit } from '@sentry/browser'; +import type { BrowserClient, BrowserOptions } from '@sentry/browser'; +import { + getDefaultIntegrations, + initWithDefaultIntegrations as browserInitWithDefaultIntegrations, +} from '@sentry/browser'; +import type { Integration } from '@sentry/core'; import { applySdkMetadata } from '@sentry/core'; -import type { Client } from '@sentry/core'; /** - * Initializes the Solid SDK + * Initializes the Solid SDK. */ -export function init(options: BrowserOptions): Client | undefined { +export function init(options: BrowserOptions): BrowserClient | undefined { + return initWithDefaultIntegrations(options, getDefaultIntegrations); +} + +/** + * Initialize a Solid client with the provided options and default integrations getter function. + * This is an internal method the SDK uses under the hood to set up things - you should not use this as a user! + * Instead, use `init()` to initialize the SDK. + * + * @hidden + * @internal + */ +export function initWithDefaultIntegrations( + options: BrowserOptions, + getDefaultIntegrations: (options: BrowserOptions) => Integration[], +): BrowserClient | undefined { const opts = { ...options, }; applySdkMetadata(opts, 'solid'); - return browserInit(opts); + return browserInitWithDefaultIntegrations(opts, getDefaultIntegrations); } diff --git a/packages/solid/test/sdk.test.ts b/packages/solid/test/sdk.test.ts index 7177dd8c2a64..44258edd14ef 100644 --- a/packages/solid/test/sdk.test.ts +++ b/packages/solid/test/sdk.test.ts @@ -5,7 +5,7 @@ import * as SentryBrowser from '@sentry/browser'; import { init as solidInit } from '../src/sdk'; -const browserInit = vi.spyOn(SentryBrowser, 'init'); +const browserInit = vi.spyOn(SentryBrowser, 'initWithDefaultIntegrations'); describe('Initialize Solid SDK', () => { beforeEach(() => { @@ -29,6 +29,6 @@ describe('Initialize Solid SDK', () => { expect(client).not.toBeUndefined(); expect(browserInit).toHaveBeenCalledTimes(1); - expect(browserInit).toHaveBeenLastCalledWith(expect.objectContaining(expectedMetadata)); + expect(browserInit).toHaveBeenLastCalledWith(expect.objectContaining(expectedMetadata), expect.any(Function)); }); }); diff --git a/packages/solidstart/src/client/sdk.ts b/packages/solidstart/src/client/sdk.ts index e656fdcba921..bcf1b7ac8139 100644 --- a/packages/solidstart/src/client/sdk.ts +++ b/packages/solidstart/src/client/sdk.ts @@ -1,10 +1,10 @@ -import { applySdkMetadata } from '@sentry/core'; import type { Client, Integration } from '@sentry/core'; +import { applySdkMetadata } from '@sentry/core'; import type { BrowserOptions } from '@sentry/solid'; import { browserTracingIntegration, getDefaultIntegrations as getDefaultSolidIntegrations, - init as initSolidSDK, + initWithDefaultIntegrations, } from '@sentry/solid'; // Treeshakable guard to remove all code related to tracing @@ -15,13 +15,12 @@ declare const __SENTRY_TRACING__: boolean; */ export function init(options: BrowserOptions): Client | undefined { const opts = { - defaultIntegrations: getDefaultIntegrations(options), ...options, }; applySdkMetadata(opts, 'solidstart', ['solidstart', 'solid']); - return initSolidSDK(opts); + return initWithDefaultIntegrations(opts, getDefaultIntegrations); } function getDefaultIntegrations(options: BrowserOptions): Integration[] { diff --git a/packages/solidstart/src/index.types.ts b/packages/solidstart/src/index.types.ts index 54a5ec6d6a3c..a2a4d619e6ce 100644 --- a/packages/solidstart/src/index.types.ts +++ b/packages/solidstart/src/index.types.ts @@ -12,6 +12,10 @@ import type * as serverSdk from './server'; /** Initializes Sentry Solid Start SDK */ export declare function init(options: Options | clientSdk.BrowserOptions | serverSdk.NodeOptions): Client | undefined; +export declare function initWithDefaultIntegrations( + options: Options | clientSdk.BrowserOptions | serverSdk.NodeOptions, + getDefaultIntegrations: (options: Options) => Integration[], +): Client | undefined; export declare const linkedErrorsIntegration: typeof clientSdk.linkedErrorsIntegration; export declare const contextLinesIntegration: typeof clientSdk.contextLinesIntegration; diff --git a/packages/solidstart/test/client/sdk.test.ts b/packages/solidstart/test/client/sdk.test.ts index dc0df6993b81..b3be048de978 100644 --- a/packages/solidstart/test/client/sdk.test.ts +++ b/packages/solidstart/test/client/sdk.test.ts @@ -5,7 +5,7 @@ import { vi } from 'vitest'; import { init as solidStartInit } from '../../src/client'; import { solidRouterBrowserTracingIntegration } from '../../src/client/solidrouter'; -const browserInit = vi.spyOn(SentrySolid, 'init'); +const browserInit = vi.spyOn(SentrySolid, 'initWithDefaultIntegrations'); describe('Initialize Solid Start SDK', () => { beforeEach(() => { @@ -32,7 +32,7 @@ describe('Initialize Solid Start SDK', () => { expect(client).not.toBeUndefined(); expect(browserInit).toHaveBeenCalledTimes(1); - expect(browserInit).toHaveBeenLastCalledWith(expect.objectContaining(expectedMetadata)); + expect(browserInit).toHaveBeenLastCalledWith(expect.objectContaining(expectedMetadata), expect.any(Function)); }); }); diff --git a/packages/solidstart/test/server/sdk.test.ts b/packages/solidstart/test/server/sdk.test.ts index b700b43a067a..1e44e1ce9a98 100644 --- a/packages/solidstart/test/server/sdk.test.ts +++ b/packages/solidstart/test/server/sdk.test.ts @@ -4,7 +4,7 @@ import * as SentryNode from '@sentry/node'; import { beforeEach, describe, expect, it, vi } from 'vitest'; import { init as solidStartInit } from '../../src/server'; -const browserInit = vi.spyOn(SentryNode, 'init'); +const nodeInit = vi.spyOn(SentryNode, 'init'); describe('Initialize Solid Start SDK', () => { beforeEach(() => { @@ -30,8 +30,8 @@ describe('Initialize Solid Start SDK', () => { }; expect(client).not.toBeUndefined(); - expect(browserInit).toHaveBeenCalledTimes(1); - expect(browserInit).toHaveBeenLastCalledWith(expect.objectContaining(expectedMetadata)); + expect(nodeInit).toHaveBeenCalledTimes(1); + expect(nodeInit).toHaveBeenLastCalledWith(expect.objectContaining(expectedMetadata)); }); it('filters out low quality transactions', async () => { diff --git a/packages/svelte/src/index.ts b/packages/svelte/src/index.ts index 8db23384897e..cd05ed11f6e5 100644 --- a/packages/svelte/src/index.ts +++ b/packages/svelte/src/index.ts @@ -5,7 +5,7 @@ export type { export * from '@sentry/browser'; -export { init } from './sdk'; +export { init, initWithDefaultIntegrations } from './sdk'; export { trackComponent } from './performance'; diff --git a/packages/svelte/src/sdk.ts b/packages/svelte/src/sdk.ts index b46a09bfdfa9..9ae80ace0c4f 100644 --- a/packages/svelte/src/sdk.ts +++ b/packages/svelte/src/sdk.ts @@ -1,16 +1,35 @@ import type { BrowserOptions } from '@sentry/browser'; -import { init as browserInit } from '@sentry/browser'; -import type { Client } from '@sentry/core'; +import { + getDefaultIntegrations, + initWithDefaultIntegrations as browserInitWithDefaultIntegrations, +} from '@sentry/browser'; +import type { Client, Integration } from '@sentry/core'; import { applySdkMetadata } from '@sentry/core'; + /** * Inits the Svelte SDK */ export function init(options: BrowserOptions): Client | undefined { + return initWithDefaultIntegrations(options, getDefaultIntegrations); +} + +/** + * Initialize a Svelte client with the provided options and default integrations getter function. + * This is an internal method the SDK uses under the hood to set up things - you should not use this as a user! + * Instead, use `init()` to initialize the SDK. + * + * @hidden + * @internal + */ +export function initWithDefaultIntegrations( + options: BrowserOptions, + getDefaultIntegrations: (options: BrowserOptions) => Integration[], +): Client | undefined { const opts = { ...options, }; applySdkMetadata(opts, 'svelte'); - return browserInit(opts); + return browserInitWithDefaultIntegrations(opts, getDefaultIntegrations); } diff --git a/packages/svelte/test/sdk.test.ts b/packages/svelte/test/sdk.test.ts index 6cc2ac922d45..033baf3f7d9e 100644 --- a/packages/svelte/test/sdk.test.ts +++ b/packages/svelte/test/sdk.test.ts @@ -9,7 +9,7 @@ import * as SentryBrowser from '@sentry/browser'; import { init as svelteInit } from '../src/sdk'; -const browserInit = vi.spyOn(SentryBrowser, 'init'); +const browserInit = vi.spyOn(SentryBrowser, 'initWithDefaultIntegrations'); describe('Initialize Svelte SDk', () => { beforeEach(() => { @@ -32,7 +32,7 @@ describe('Initialize Svelte SDk', () => { }; expect(browserInit).toHaveBeenCalledTimes(1); - expect(browserInit).toHaveBeenLastCalledWith(expect.objectContaining(expectedMetadata)); + expect(browserInit).toHaveBeenLastCalledWith(expect.objectContaining(expectedMetadata), expect.any(Function)); }); it("doesn't add the default svelte metadata, if metadata is already passed", () => { @@ -64,6 +64,7 @@ describe('Initialize Svelte SDk', () => { }, }, }), + expect.any(Function), ); }); diff --git a/packages/sveltekit/src/client/sdk.ts b/packages/sveltekit/src/client/sdk.ts index 35ef46118edc..7d30646fb797 100644 --- a/packages/sveltekit/src/client/sdk.ts +++ b/packages/sveltekit/src/client/sdk.ts @@ -1,8 +1,11 @@ -import { applySdkMetadata } from '@sentry/core'; import type { Client, Integration } from '@sentry/core'; +import { applySdkMetadata } from '@sentry/core'; import type { BrowserOptions } from '@sentry/svelte'; -import { getDefaultIntegrations as getDefaultSvelteIntegrations } from '@sentry/svelte'; -import { WINDOW, init as initSvelteSdk } from '@sentry/svelte'; +import { + WINDOW, + getDefaultIntegrations as getDefaultSvelteIntegrations, + initWithDefaultIntegrations, +} from '@sentry/svelte'; import { browserTracingIntegration as svelteKitBrowserTracingIntegration } from './browserTracingIntegration'; @@ -20,7 +23,6 @@ declare const __SENTRY_TRACING__: boolean; */ export function init(options: BrowserOptions): Client | undefined { const opts = { - defaultIntegrations: getDefaultIntegrations(options), ...options, }; @@ -30,7 +32,8 @@ export function init(options: BrowserOptions): Client | undefined { const actualFetch = switchToFetchProxy(); // 2. Initialize the SDK which will instrument our proxy - const client = initSvelteSdk(opts); + // Note: We initialize a Browser SDK here, we dont' actually do anything Svelte-specific in the Svelte SDK here + const client = initWithDefaultIntegrations(opts, getDefaultIntegrations); // 3. Restore the original fetch now that our proxy is instrumented if (actualFetch) { @@ -40,7 +43,7 @@ export function init(options: BrowserOptions): Client | undefined { return client; } -function getDefaultIntegrations(options: BrowserOptions): Integration[] | undefined { +function getDefaultIntegrations(options: BrowserOptions): Integration[] { // This evaluates to true unless __SENTRY_TRACING__ is text-replaced with "false", // in which case everything inside will get tree-shaken away if (typeof __SENTRY_TRACING__ === 'undefined' || __SENTRY_TRACING__) { diff --git a/packages/sveltekit/src/index.types.ts b/packages/sveltekit/src/index.types.ts index 3ad8b728bb5f..a0ac3ebf0faa 100644 --- a/packages/sveltekit/src/index.types.ts +++ b/packages/sveltekit/src/index.types.ts @@ -13,6 +13,10 @@ import type * as serverSdk from './server'; /** Initializes Sentry SvelteKit SDK */ export declare function init(options: Options | clientSdk.BrowserOptions | serverSdk.NodeOptions): Client | undefined; +export declare function initWithDefaultIntegrations( + options: Options | clientSdk.BrowserOptions | serverSdk.NodeOptions, + getDefaultIntegrations: (options: Options) => Integration[], +): Client | undefined; export declare function handleErrorWithSentry(handleError?: T): T; diff --git a/packages/sveltekit/src/server/sdk.ts b/packages/sveltekit/src/server/sdk.ts index 7f3acbf57fbd..aafd4f3f2360 100644 --- a/packages/sveltekit/src/server/sdk.ts +++ b/packages/sveltekit/src/server/sdk.ts @@ -1,7 +1,7 @@ +import type { Integration } from '@sentry/core'; import { applySdkMetadata } from '@sentry/core'; import type { NodeClient, NodeOptions } from '@sentry/node'; -import { getDefaultIntegrations as getDefaultNodeIntegrations } from '@sentry/node'; -import { init as initNodeSdk } from '@sentry/node'; +import { getDefaultIntegrations as getDefaultNodeIntegrations, initWithDefaultIntegrations } from '@sentry/node'; import { rewriteFramesIntegration } from './rewriteFramesIntegration'; @@ -11,11 +11,14 @@ import { rewriteFramesIntegration } from './rewriteFramesIntegration'; */ export function init(options: NodeOptions): NodeClient | undefined { const opts = { - defaultIntegrations: [...getDefaultNodeIntegrations(options), rewriteFramesIntegration()], ...options, }; applySdkMetadata(opts, 'sveltekit', ['sveltekit', 'node']); - return initNodeSdk(opts); + return initWithDefaultIntegrations(opts, getDefaultIntegrations); +} + +function getDefaultIntegrations(options: NodeOptions): Integration[] { + return [...getDefaultNodeIntegrations(options), rewriteFramesIntegration()]; } diff --git a/packages/sveltekit/test/client/sdk.test.ts b/packages/sveltekit/test/client/sdk.test.ts index d0d563c44a4b..8d187f2d3673 100644 --- a/packages/sveltekit/test/client/sdk.test.ts +++ b/packages/sveltekit/test/client/sdk.test.ts @@ -1,12 +1,11 @@ import { afterEach, describe, expect, it, vi } from 'vitest'; -import type { BrowserClient } from '@sentry/svelte'; import * as SentrySvelte from '@sentry/svelte'; -import { SDK_VERSION, getClient, getCurrentScope, getGlobalScope, getIsolationScope } from '@sentry/svelte'; +import { SDK_VERSION, getCurrentScope, getGlobalScope, getIsolationScope } from '@sentry/svelte'; import { init } from '../../src/client'; -const svelteInit = vi.spyOn(SentrySvelte, 'init'); +const svelteInit = vi.spyOn(SentrySvelte, 'initWithDefaultIntegrations'); describe('Sentry client SDK', () => { describe('init', () => { @@ -38,6 +37,7 @@ describe('Sentry client SDK', () => { }, }, }), + expect.any(Function), ); }); @@ -47,12 +47,12 @@ describe('Sentry client SDK', () => { ['tracesSampler', { tracesSampler: () => 1.0 }], ['no tracing option set', {}], ])('adds a browserTracingIntegration if tracing is enabled via %s', (_, tracingOptions) => { - init({ + const client = init({ dsn: 'https://public@dsn.ingest.sentry.io/1337', ...tracingOptions, }); - const browserTracing = getClient()?.getIntegrationByName('BrowserTracing'); + const browserTracing = client?.getIntegrationByName('BrowserTracing'); expect(browserTracing).toBeDefined(); }); @@ -62,12 +62,12 @@ describe('Sentry client SDK', () => { globalThis.__SENTRY_TRACING__ = false; - init({ + const client = init({ dsn: 'https://public@dsn.ingest.sentry.io/1337', tracesSampleRate: 1, }); - const browserTracing = getClient()?.getIntegrationByName('BrowserTracing'); + const browserTracing = client?.getIntegrationByName('BrowserTracing'); expect(browserTracing).toBeUndefined(); delete globalThis.__SENTRY_TRACING__; diff --git a/packages/sveltekit/test/server/sdk.test.ts b/packages/sveltekit/test/server/sdk.test.ts index 4c6c9917c572..535d225679f3 100644 --- a/packages/sveltekit/test/server/sdk.test.ts +++ b/packages/sveltekit/test/server/sdk.test.ts @@ -6,7 +6,7 @@ import { SDK_VERSION, getClient } from '@sentry/node'; import { init } from '../../src/server/sdk'; -const nodeInit = vi.spyOn(SentryNode, 'init'); +const nodeInit = vi.spyOn(SentryNode, 'initWithDefaultIntegrations'); describe('Sentry server SDK', () => { describe('init', () => { @@ -38,6 +38,7 @@ describe('Sentry server SDK', () => { }, }, }), + expect.any(Function), ); }); diff --git a/packages/vercel-edge/src/sdk.ts b/packages/vercel-edge/src/sdk.ts index a816b02e27a9..64501c3d4567 100644 --- a/packages/vercel-edge/src/sdk.ts +++ b/packages/vercel-edge/src/sdk.ts @@ -66,10 +66,6 @@ export function init(options: VercelEdgeOptions = {}): Client | undefined { const scope = getCurrentScope(); scope.update(options.initialScope); - if (options.defaultIntegrations === undefined) { - options.defaultIntegrations = getDefaultIntegrations(options); - } - if (options.dsn === undefined && process.env.SENTRY_DSN) { options.dsn = process.env.SENTRY_DSN; } @@ -91,10 +87,12 @@ export function init(options: VercelEdgeOptions = {}): Client | undefined { options.environment = options.environment || process.env.SENTRY_ENVIRONMENT || getVercelEnv(false) || process.env.NODE_ENV; + const defaultIntegrations = getDefaultIntegrations(options); + const client = new VercelEdgeClient({ ...options, stackParser: stackParserFromStackParserOptions(options.stackParser || nodeStackParser), - integrations: getIntegrationsToSetup(options), + integrations: getIntegrationsToSetup(options, defaultIntegrations), transport: options.transport || makeEdgeTransport, }); // The client is on the current scope, from where it generally is inherited diff --git a/packages/vue/src/sdk.ts b/packages/vue/src/sdk.ts index 689a17dacbc4..58b7765abb2a 100644 --- a/packages/vue/src/sdk.ts +++ b/packages/vue/src/sdk.ts @@ -1,5 +1,6 @@ -import { getDefaultIntegrations, init as browserInit } from '@sentry/browser'; -import type { Client } from '@sentry/core'; +import type { BrowserOptions } from '@sentry/browser'; +import { getDefaultIntegrations, initWithDefaultIntegrations } from '@sentry/browser'; +import type { Client, Integration } from '@sentry/core'; import { applySdkMetadata } from '@sentry/core'; import { vueIntegration } from './integration'; import type { Options } from './types'; @@ -9,11 +10,14 @@ import type { Options } from './types'; */ export function init(options: Partial> = {}): Client | undefined { const opts = { - defaultIntegrations: [...getDefaultIntegrations(options), vueIntegration()], ...options, }; applySdkMetadata(opts, 'vue'); - return browserInit(opts); + return initWithDefaultIntegrations(opts, getVueDefaultIntegrations); +} + +function getVueDefaultIntegrations(options: BrowserOptions): Integration[] { + return [...getDefaultIntegrations(options), vueIntegration()]; }