diff --git a/dev-packages/browser-integration-tests/suites/manual-client/skip-init-browser-extension/test.ts b/dev-packages/browser-integration-tests/suites/manual-client/skip-init-browser-extension/test.ts index e997e08f8257..5098b4aa6552 100644 --- a/dev-packages/browser-integration-tests/suites/manual-client/skip-init-browser-extension/test.ts +++ b/dev-packages/browser-integration-tests/suites/manual-client/skip-init-browser-extension/test.ts @@ -23,7 +23,7 @@ sentryTest( if (hasDebugLogs()) { expect(errorLogs.length).toEqual(1); expect(errorLogs[0]).toEqual( - '[Sentry] You cannot run Sentry this way in a browser extension, check: https://docs.sentry.io/platforms/javascript/best-practices/browser-extensions/', + '[Sentry] You cannot use Sentry.init() in a browser extension, see: https://docs.sentry.io/platforms/javascript/best-practices/browser-extensions/', ); } else { expect(errorLogs.length).toEqual(0); diff --git a/dev-packages/browser-integration-tests/suites/manual-client/skip-init-chrome-extension/test.ts b/dev-packages/browser-integration-tests/suites/manual-client/skip-init-chrome-extension/test.ts index 8d3ea4f27faf..411fbd2f9db8 100644 --- a/dev-packages/browser-integration-tests/suites/manual-client/skip-init-chrome-extension/test.ts +++ b/dev-packages/browser-integration-tests/suites/manual-client/skip-init-chrome-extension/test.ts @@ -21,7 +21,7 @@ sentryTest('should not initialize when inside a Chrome browser extension', async if (hasDebugLogs()) { expect(errorLogs.length).toEqual(1); expect(errorLogs[0]).toEqual( - '[Sentry] You cannot run Sentry this way in a browser extension, check: https://docs.sentry.io/platforms/javascript/best-practices/browser-extensions/', + '[Sentry] You cannot use Sentry.init() in a browser extension, see: https://docs.sentry.io/platforms/javascript/best-practices/browser-extensions/', ); } else { expect(errorLogs.length).toEqual(0); 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 5a4ffbbde7ee..b3c3bbf20907 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'; @@ -50,14 +50,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 f04725d1ef1e..80c6500437c4 100644 --- a/packages/astro/src/client/sdk.ts +++ b/packages/astro/src/client/sdk.ts @@ -2,7 +2,7 @@ import type { BrowserOptions } from '@sentry/browser'; import { browserTracingIntegration, getDefaultIntegrations as getBrowserDefaultIntegrations, - init as initBrowserSdk, + initWithDefaultIntegrations, } from '@sentry/browser'; import type { Client, Integration } from '@sentry/core'; import { applySdkMetadata } from '@sentry/core'; @@ -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 d74a885c2b37..b4feb63ded62 100644 --- a/packages/astro/src/index.types.ts +++ b/packages/astro/src/index.types.ts @@ -15,6 +15,10 @@ export * from '@sentry/node'; /** 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 a537013f7c22..ebf940b5c331 100644 --- a/packages/astro/test/client/sdk.test.ts +++ b/packages/astro/test/client/sdk.test.ts @@ -1,8 +1,6 @@ -import type { BrowserClient } from '@sentry/browser'; import { browserTracingIntegration, getActiveSpan, - getClient, getCurrentScope, getGlobalScope, getIsolationScope, @@ -12,7 +10,7 @@ import * as SentryBrowser from '@sentry/browser'; import { afterEach, describe, expect, it, vi } from 'vitest'; import { init } from '../../src/client/sdk'; -const browserInit = vi.spyOn(SentryBrowser, 'init'); +const browserInit = vi.spyOn(SentryBrowser, 'initWithDefaultIntegrations'); describe('Sentry client SDK', () => { describe('init', () => { @@ -44,6 +42,7 @@ describe('Sentry client SDK', () => { }, }, }), + expect.any(Function), ); }); @@ -53,37 +52,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 }), @@ -91,7 +84,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 7d278414c5be..84a3d9130d80 100644 --- a/packages/aws-serverless/src/sdk.ts +++ b/packages/aws-serverless/src/sdk.ts @@ -13,7 +13,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 f05deee57d88..ecc8fe76c69b 100644 --- a/packages/aws-serverless/test/sdk.test.ts +++ b/packages/aws-serverless/test/sdk.test.ts @@ -25,7 +25,7 @@ vi.mock('@sentry/node', async () => { const original = (await vi.importActual('@sentry/node')) as typeof import('@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 2b2279c099b3..22375404a508 100644 --- a/packages/browser/src/index.ts +++ b/packages/browser/src/index.ts @@ -6,6 +6,8 @@ export * from './exports'; export { logger }; +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 1c7e6fbe95ad..b75aae7a5f48 100644 --- a/packages/browser/src/sdk.ts +++ b/packages/browser/src/sdk.ts @@ -3,6 +3,7 @@ import { consoleSandbox, dedupeIntegration, functionToStringIntegration, + getClient, getCurrentScope, getIntegrationsToSetup, getLocationHref, @@ -12,7 +13,6 @@ import { lastEventId, logger, stackParserFromStackParserOptions, - supportsFetch, } from '@sentry/core'; import type { BrowserClientOptions, BrowserOptions } from './client'; import { BrowserClient } from './client'; @@ -27,6 +27,22 @@ import { linkedErrorsIntegration } from './integrations/linkederrors'; import { defaultStackParser } from './stack-parsers'; import { makeFetchTransport } from './transports/fetch'; +type ExtensionProperties = { + chrome?: Runtime; + browser?: Runtime; + nw?: unknown; +}; +type Runtime = { + runtime?: { + id?: string; + }; +}; + +/** + * A magic string that build tooling can leverage in order to inject a release value into the SDK. + */ +declare const __SENTRY_RELEASE__: string | undefined; + /** Get the default integrations for the browser SDK. */ export function getDefaultIntegrations(_options: Options): Integration[] { /** @@ -49,9 +65,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__ @@ -61,70 +76,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; - nw?: unknown; -}; -type Runtime = { - runtime?: { - id?: string; - }; -}; - -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. - */ -declare const __SENTRY_RELEASE__: string | undefined; - /** * The Sentry Browser SDK Client. * @@ -172,32 +127,37 @@ declare const __SENTRY_RELEASE__: string | undefined; * @see {@link BrowserOptions} for documentation on configuration options. */ export function init(browserOptions: BrowserOptions = {}): Client | undefined { - const options = applyDefaultOptions(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/', - ); - }); - } + if (!browserOptions.skipBrowserExtensionCheck && _checkForBrowserExtension()) { return; } + return _init(browserOptions, getDefaultIntegrations(browserOptions)); +} - 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.', - ); +/** + * 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 { + if (!browserOptions.skipBrowserExtensionCheck && _checkForBrowserExtension()) { + return; } - const clientOptions: BrowserClientOptions = { - ...options, - stackParser: stackParserFromStackParserOptions(options.stackParser || defaultStackParser), - integrations: getIntegrationsToSetup(options), - transport: options.transport || makeFetchTransport, - }; + return _init(browserOptions, getDefaultIntegrationsImpl(browserOptions)); +} + +/** + * Acutal implementation shared by init and initWithDefaultIntegrations. + */ +function _init(browserOptions: BrowserOptions = {}, defaultIntegrations: Integration[]): BrowserClient { + const options = applyDefaultOptions(browserOptions); + const clientOptions = getClientOptions(options, defaultIntegrations); return initAndBind(BrowserClient, clientOptions); } @@ -214,7 +174,7 @@ export function showReportDialog(options: ReportDialogOptions = {}): void { } const scope = getCurrentScope(); - const client = scope.getClient(); + const client = getClient(); const dsn = client?.getDsn(); if (!dsn) { @@ -222,24 +182,23 @@ export function showReportDialog(options: ReportDialogOptions = {}): void { return; } - if (scope) { - options.user = { + options.user = { + ...scope.getUser(), + ...options.user, + }; + + const mergedOptions = { + user: { ...scope.getUser(), ...options.user, - }; - } - - if (!options.eventId) { - const eventId = lastEventId(); - if (eventId) { - options.eventId = eventId; - } - } + }, + eventId: options.eventId || lastEventId(), + }; const script = WINDOW.document.createElement('script'); script.async = true; script.crossOrigin = 'anonymous'; - script.src = getReportDialogEndpoint(dsn, options); + script.src = getReportDialogEndpoint(dsn, mergedOptions); if (options.onLoad) { script.onload = options.onLoad; @@ -282,3 +241,57 @@ export function forceLoad(): void { export function onLoad(callback: () => void): void { callback(); } + +function _isEmbeddedBrowserExtension(): boolean { + if (typeof WINDOW.window === 'undefined') { + // No need to show the error if we're not in a browser window environment (e.g. service workers) + return false; + } + + const _window = WINDOW as typeof WINDOW & ExtensionProperties; + + // 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 + if (_window.nw) { + return false; + } + + const extensionObject = _window['chrome'] || _window['browser']; + + if (!extensionObject?.runtime?.id) { + return false; + } + + 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 = + WINDOW === WINDOW.top && extensionProtocols.some(protocol => href.startsWith(`${protocol}://`)); + + return !isDedicatedExtensionPage; +} + +function _checkForBrowserExtension(): true | void { + if (_isEmbeddedBrowserExtension()) { + if (DEBUG_BUILD) { + consoleSandbox(() => { + // eslint-disable-next-line no-console + console.error( + '[Sentry] You cannot use Sentry.init() in a browser extension, see: https://docs.sentry.io/platforms/javascript/best-practices/browser-extensions/', + ); + }); + } + + return true; + } +} + +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 4761a41228fe..b2c4be8bfd9a 100644 --- a/packages/browser/test/sdk.test.ts +++ b/packages/browser/test/sdk.test.ts @@ -7,10 +7,10 @@ import type { Integration } from '@sentry/core'; import * as SentryCore from '@sentry/core'; import { createTransport, resolvedSyncPromise } from '@sentry/core'; import type { Mock } from 'vitest'; -import { afterAll, afterEach, beforeEach, describe, expect, it, test, vi } from 'vitest'; +import { afterEach, describe, expect, it, test, vi } from 'vitest'; 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'; @@ -32,15 +32,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'), @@ -53,28 +49,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', () => { @@ -134,7 +143,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', () => { @@ -149,10 +158,8 @@ describe('init', () => { expect(consoleErrorSpy).toBeCalledTimes(1); 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/', + '[Sentry] You cannot use Sentry.init() in a browser extension, see: 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', () => { @@ -164,10 +171,8 @@ describe('init', () => { expect(consoleErrorSpy).toBeCalledTimes(1); 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/', + '[Sentry] You cannot use Sentry.init() in a browser extension, see: https://docs.sentry.io/platforms/javascript/best-practices/browser-extensions/', ); - - consoleErrorSpy.mockRestore(); }); it.each(['chrome-extension', 'moz-extension', 'ms-browser-extension', 'safari-web-extension'])( @@ -246,20 +251,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', () => { @@ -270,15 +296,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', () => { @@ -306,7 +327,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 641567504818..3c5868e5839f 100644 --- a/packages/bun/src/sdk.ts +++ b/packages/bun/src/sdk.ts @@ -14,7 +14,7 @@ import { contextLinesIntegration, getAutoPerformanceIntegrations, httpIntegration, - init as initNode, + initWithDefaultIntegrations, modulesIntegration, nativeNodeFetchIntegration, nodeContextIntegration, @@ -109,9 +109,5 @@ export function init(userOptions: BunOptions = {}): NodeClient | undefined { options.transport = options.transport || makeFetchTransport; - if (options.defaultIntegrations === undefined) { - options.defaultIntegrations = getDefaultIntegrations(options); - } - - return initNode(options); + return initWithDefaultIntegrations(options, getDefaultIntegrations); } diff --git a/packages/bun/test/init.test.ts b/packages/bun/test/init.test.ts index 4b2ddd452713..658fab68bdad 100644 --- a/packages/bun/test/init.test.ts +++ b/packages/bun/test/init.test.ts @@ -19,7 +19,6 @@ describe('init()', () => { let mockAutoPerformanceIntegrations: Mock<() => Integration[]>; beforeEach(() => { - // @ts-expect-error weird mockAutoPerformanceIntegrations = spyOn(sentryNode, 'getAutoPerformanceIntegrations'); }); diff --git a/packages/cloudflare/src/sdk.ts b/packages/cloudflare/src/sdk.ts index dee32b856eb0..7ef0a2ad1b8a 100644 --- a/packages/cloudflare/src/sdk.ts +++ b/packages/cloudflare/src/sdk.ts @@ -37,14 +37,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/index.ts b/packages/core/src/index.ts index 6d281fde0ac9..34498f588217 100644 --- a/packages/core/src/index.ts +++ b/packages/core/src/index.ts @@ -206,6 +206,7 @@ export { supportsFetch, supportsHistory, supportsNativeFetch, + // eslint-disable-next-line deprecation/deprecation supportsReferrerPolicy, supportsReportingObserver, } from './utils-hoist/supports'; diff --git a/packages/core/src/integration.ts b/packages/core/src/integration.ts index c5636d002cc1..7dd7a4dbc126 100644 --- a/packages/core/src/integration.ts +++ b/packages/core/src/integration.ts @@ -42,24 +42,40 @@ 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]; - } else if (typeof userIntegrations === 'function') { - const resolvedUserIntegrations = userIntegrations(defaultIntegrations); + if (typeof userIntegrations === 'function') { + const resolvedUserIntegrations = userIntegrations(defaultIntegrationsToUse); integrations = Array.isArray(resolvedUserIntegrations) ? resolvedUserIntegrations : [resolvedUserIntegrations]; } else { - integrations = defaultIntegrations; + integrations = [...defaultIntegrationsToUse, ...(userIntegrations || [])]; } return filterDuplicates(integrations); diff --git a/packages/core/src/sdk.ts b/packages/core/src/sdk.ts index fa3194d1ebc5..5803c6e6b015 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 4b0010f2b7d7..9827aecb19f4 100644 --- a/packages/core/src/types-hoist/options.ts +++ b/packages/core/src/types-hoist/options.ts @@ -395,9 +395,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/src/utils-hoist/supports.ts b/packages/core/src/utils-hoist/supports.ts index 2336c41b0672..8c5e55d7cd26 100644 --- a/packages/core/src/utils-hoist/supports.ts +++ b/packages/core/src/utils-hoist/supports.ts @@ -76,9 +76,7 @@ export function supportsFetch(): boolean { } try { - new Headers(); new Request('http://www.example.com'); - new Response(); return true; } catch (e) { return false; @@ -153,6 +151,8 @@ export function supportsReportingObserver(): boolean { * {@link supportsReferrerPolicy}. * * @returns Answer to the given question. + * + * @deprecated This method will be removed in a future version. */ export function supportsReferrerPolicy(): boolean { // Despite all stars in the sky saying that Edge supports old draft syntax, aka 'never', 'always', 'origin' and 'default' diff --git a/packages/core/test/lib/integration.test.ts b/packages/core/test/lib/integration.test.ts index 27ca88049b2a..358eef466c4a 100644 --- a/packages/core/test/lib/integration.test.ts +++ b/packages/core/test/lib/integration.test.ts @@ -34,7 +34,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 ]; @@ -47,31 +48,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); }); }); @@ -114,10 +133,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'], @@ -127,6 +147,7 @@ describe('getIntegrationsToSetup', () => { [ 'duplicate user integrations, user-provided array', defaultIntegrations, + undefined, duplicateUserIntegrationsArray, [ ['ChaseSquirrels', 'defaultA'], @@ -136,6 +157,7 @@ describe('getIntegrationsToSetup', () => { [ 'duplicate user integrations, user-provided function with defaults first', defaultIntegrations, + undefined, duplicateUserIntegrationsFunctionDefaultsFirst, [ ['ChaseSquirrels', 'defaultA'], @@ -145,6 +167,7 @@ describe('getIntegrationsToSetup', () => { [ 'duplicate user integrations, user-provided function with defaults second', defaultIntegrations, + undefined, duplicateUserIntegrationsFunctionDefaultsSecond, [ ['CatchTreats', 'userB'], @@ -154,6 +177,7 @@ describe('getIntegrationsToSetup', () => { [ 'same integration in default and user integrations, user-provided array', defaultIntegrations, + undefined, userIntegrationsMatchingDefaultsArray, [ ['ChaseSquirrels', 'userA'], @@ -163,6 +187,7 @@ describe('getIntegrationsToSetup', () => { [ 'same integration in default and user integrations, user-provided function with defaults first', defaultIntegrations, + undefined, userIntegrationsMatchingDefaultsFunctionDefaultsFirst, [ ['ChaseSquirrels', 'userA'], @@ -172,6 +197,7 @@ describe('getIntegrationsToSetup', () => { [ 'same integration in default and user integrations, user-provided function with defaults second', defaultIntegrations, + undefined, userIntegrationsMatchingDefaultsFunctionDefaultsSecond, [ ['ChaseSquirrels', 'userA'], @@ -180,11 +206,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 588d417f5ed9..7172d7060bec 100644 --- a/packages/deno/src/sdk.ts +++ b/packages/deno/src/sdk.ts @@ -86,14 +86,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 2699eb4f9e2f..634a17a81da7 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'; @@ -28,11 +28,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 ca7a3f4d6c60..f1a826001b91 100644 --- a/packages/google-cloud-serverless/test/gcpfunction/http.test.ts +++ b/packages/google-cloud-serverless/test/gcpfunction/http.test.ts @@ -1,6 +1,6 @@ import type { Integration } from '@sentry/core'; import { SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN, SEMANTIC_ATTRIBUTE_SENTRY_SOURCE } from '@sentry/core'; -import { type MockInstance, beforeEach, describe, expect, test, vi } from 'vitest'; +import { beforeEach, describe, expect, test, vi } from 'vitest'; import type { HttpFunction, Request, Response } from '../../src/gcpfunction/general'; import { wrapHttpFunction } from '../../src/gcpfunction/http'; import { init } from '../../src/sdk'; @@ -23,8 +23,8 @@ vi.mock('@sentry/node', async () => { const original = (await vi.importActual('@sentry/node')) as typeof import('@sentry/node'); return { ...original, - init: (options: unknown) => { - mockInit(options); + initWithDefaultIntegrations: (options: unknown, getDefaultIntergations: unknown) => { + mockInit(options, getDefaultIntergations); }, startSpanManual: (...args: unknown[]) => { mockStartSpanManual(...args); @@ -169,10 +169,9 @@ describe('GCPFunction', () => { await handleHttp(wrappedHandler); - const initOptions = (mockInit as unknown as MockInstance).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 9759ac2a5a43..0c210223e08d 100644 --- a/packages/google-cloud-serverless/test/sdk.test.ts +++ b/packages/google-cloud-serverless/test/sdk.test.ts @@ -8,7 +8,7 @@ vi.mock('@sentry/node', async () => { const original = (await vi.importActual('@sentry/node')) as typeof import('@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 733cb935003c..534fbbac1bfc 100644 --- a/packages/nestjs/src/sdk.ts +++ b/packages/nestjs/src/sdk.ts @@ -6,7 +6,7 @@ import { spanToJSON, } 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 1692c9be6fdd..caaae6249388 100644 --- a/packages/nestjs/test/sdk.test.ts +++ b/packages/nestjs/test/sdk.test.ts @@ -3,7 +3,7 @@ import * as SentryNode from '@sentry/node'; import { beforeEach, describe, expect, it, vi } from 'vitest'; 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', () => { @@ -28,6 +28,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 e650c4e23a10..b0b70653f452 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 { addEventProcessor, applySdkMetadata, consoleSandbox, getGlobalScope, GLOBAL_OBJ } 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'; @@ -43,7 +43,6 @@ export function init(options: BrowserOptions): Client | undefined { const opts = { environment: getVercelEnv(true) || process.env.NODE_ENV, - defaultIntegrations: getDefaultIntegrations(options), release: process.env._sentryRelease || globalWithInjectedValues._sentryRelease, ...options, } satisfies BrowserOptions; @@ -51,7 +50,7 @@ export function init(options: BrowserOptions): Client | undefined { 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 7982667f0c3f..e39d1dca48d2 100644 --- a/packages/nextjs/src/edge/index.ts +++ b/packages/nextjs/src/edge/index.ts @@ -48,7 +48,6 @@ export function init(options: VercelEdgeOptions = {}): void { } const opts = { - defaultIntegrations: customDefaultIntegrations, release: process.env._sentryRelease || globalWithInjectedValues._sentryRelease, ...options, }; diff --git a/packages/nextjs/src/index.types.ts b/packages/nextjs/src/index.types.ts index c630d545061c..d2426f6a94bf 100644 --- a/packages/nextjs/src/index.types.ts +++ b/packages/nextjs/src/index.types.ts @@ -18,6 +18,10 @@ export * from './edge'; 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 e4e437ebd691..bc67f8502642 100644 --- a/packages/nextjs/src/server/index.ts +++ b/packages/nextjs/src/server/index.ts @@ -6,7 +6,7 @@ import { SEMATTRS_HTTP_METHOD, SEMATTRS_HTTP_TARGET, } from '@opentelemetry/semantic-conventions'; -import type { EventProcessor } from '@sentry/core'; +import type { EventProcessor, Integration } from '@sentry/core'; import { applySdkMetadata, extractTraceparentData, @@ -26,7 +26,7 @@ import { stripUrlQueryAndFragment, } 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'; @@ -40,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; @@ -97,30 +98,9 @@ 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, release: process.env._sentryRelease || globalWithInjectedValues._sentryRelease, - defaultIntegrations: customDefaultIntegrations, ...options, }; @@ -137,7 +117,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 @@ -381,6 +365,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 b1e7884d5ad2..a4f7c13d7174 100644 --- a/packages/nextjs/test/clientSdk.test.ts +++ b/packages/nextjs/test/clientSdk.test.ts @@ -6,7 +6,7 @@ import { JSDOM } from 'jsdom'; import { afterAll, afterEach, describe, expect, it, vi } from 'vitest'; import { breadcrumbsIntegration, browserTracingIntegration, init } from '../src/client'; -const reactInit = vi.spyOn(SentryReact, 'init'); +const reactInit = vi.spyOn(SentryReact, 'initWithDefaultIntegrations'); const loggerLogSpy = vi.spyOn(logger, 'log'); // We're setting up JSDom here because the Next.js routing instrumentations requires a few things to be present on pageload: @@ -68,13 +68,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 8ea0b060155e..e4cd0cebcba4 100644 --- a/packages/nextjs/test/serverSdk.test.ts +++ b/packages/nextjs/test/serverSdk.test.ts @@ -1,4 +1,3 @@ -import type { Integration } from '@sentry/core'; import { GLOBAL_OBJ } 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 = vi.spyOn(SentryNode, 'init'); - -function findIntegrationByName(integrations: Integration[] = [], name: string): Integration | undefined { - return integrations.find(integration => integration.name === name); -} +const nodeInit = vi.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 03705bda89ba..f73e97e4692a 100644 --- a/packages/node/src/index.ts +++ b/packages/node/src/index.ts @@ -44,7 +44,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 1536242cfdcb..4160e98b5e42 100644 --- a/packages/node/src/sdk/index.ts +++ b/packages/node/src/sdk/index.ts @@ -74,7 +74,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 @@ -89,22 +89,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); @@ -217,15 +222,12 @@ function getClientOptions( debug: envToBool(options.debug ?? process.env.SENTRY_DEBUG), }; - const integrations = options.integrations; - const defaultIntegrations = options.defaultIntegrations ?? getDefaultIntegrationsImpl(mergedOptions); + const defaultIntegrations = getDefaultIntegrationsImpl(mergedOptions); return { ...mergedOptions, - integrations: getIntegrationsToSetup({ - defaultIntegrations, - integrations, - }), + stackParser: stackParserFromStackParserOptions(options.stackParser || defaultStackParser), + integrations: getIntegrationsToSetup(mergedOptions, defaultIntegrations), }; } diff --git a/packages/nuxt/src/client/sdk.ts b/packages/nuxt/src/client/sdk.ts index 5db856dae689..436b2610052d 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 type { Client } from '@sentry/core'; import { applySdkMetadata } 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 c6cdb01d280e..077c3a35a241 100644 --- a/packages/nuxt/src/index.types.ts +++ b/packages/nuxt/src/index.types.ts @@ -12,6 +12,10 @@ export * from './index.server'; // re-export colliding types export declare function init(options: Options | SentryNuxtClientOptions | SentryNuxtServerOptions): Client | undefined; +export declare function initWithDefaultIntegrations( + options: Options | SentryNuxtClientOptions | SentryNuxtServerOptions, + 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 7139b82e30ec..0e16c6740698 100644 --- a/packages/nuxt/test/server/sdk.test.ts +++ b/packages/nuxt/test/server/sdk.test.ts @@ -6,7 +6,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', () => { @@ -35,7 +35,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-router/src/index.types.ts b/packages/react-router/src/index.types.ts index 45f4fe10fa31..6ec8508596ee 100644 --- a/packages/react-router/src/index.types.ts +++ b/packages/react-router/src/index.types.ts @@ -1,6 +1,6 @@ /* eslint-disable import/export */ -import type { Integration, Options, StackParser } from '@sentry/core'; +import type { Client, Integration, Options, StackParser } from '@sentry/core'; import type * as clientSdk from './client'; import type * as serverSdk from './server'; @@ -10,7 +10,11 @@ export * from './server'; export * from './vite'; /** Initializes Sentry React Router SDK */ -export declare function init(options: Options | clientSdk.BrowserOptions | serverSdk.NodeOptions): void; +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 contextLinesIntegration: typeof clientSdk.contextLinesIntegration; export declare const linkedErrorsIntegration: typeof clientSdk.linkedErrorsIntegration; diff --git a/packages/react/src/index.ts b/packages/react/src/index.ts index 4638bc5c4c98..cf7ec1b37f66 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 { captureReactException, 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 844bc30f1785..233fe2c18025 100644 --- a/packages/react/src/sdk.ts +++ b/packages/react/src/sdk.ts @@ -1,6 +1,10 @@ import type { BrowserOptions } from '@sentry/browser'; -import { init as browserInit, setContext } from '@sentry/browser'; -import type { Client } from '@sentry/core'; +import { + init as browserInit, + initWithDefaultIntegrations as browserInitWithDefaultIntegrations, + setContext, +} from '@sentry/browser'; +import type { Client, Integration } from '@sentry/core'; import { applySdkMetadata } from '@sentry/core'; import { version } from 'react'; @@ -16,3 +20,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.types.ts b/packages/remix/src/index.types.ts index 697fc3813045..38ec12f37408 100644 --- a/packages/remix/src/index.types.ts +++ b/packages/remix/src/index.types.ts @@ -12,6 +12,10 @@ export * from './index.server'; /** 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 browserTracingIntegration: typeof clientSdk.browserTracingIntegration; export declare const linkedErrorsIntegration: typeof clientSdk.linkedErrorsIntegration; diff --git a/packages/remix/src/server/sdk.ts b/packages/remix/src/server/sdk.ts index 816e5083aa26..b7eecf7d93b5 100644 --- a/packages/remix/src/server/sdk.ts +++ b/packages/remix/src/server/sdk.ts @@ -1,7 +1,11 @@ import type { Integration } from '@sentry/core'; import { applySdkMetadata, logger } from '@sentry/core'; import type { NodeClient, NodeOptions } from '@sentry/node'; -import { getDefaultIntegrations as getDefaultNodeIntegrations, init as nodeInit, isInitialized } from '@sentry/node'; +import { + getDefaultIntegrations as getDefaultNodeIntegrations, + initWithDefaultIntegrations, + isInitialized, +} from '@sentry/node'; import { DEBUG_BUILD } from '../utils/debug-build'; import type { RemixOptions } from '../utils/remixOptions'; import { instrumentServer } from './instrumentServer'; @@ -23,17 +27,18 @@ 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/test/index.server.test.ts b/packages/remix/test/index.server.test.ts index 8e61ebd77e06..19e98f57b168 100644 --- a/packages/remix/test/index.server.test.ts +++ b/packages/remix/test/index.server.test.ts @@ -1,10 +1,10 @@ import * as SentryNode from '@sentry/node'; -import { type Mock, afterEach, describe, expect, it, vi } from 'vitest'; +import { afterEach, describe, expect, it, vi } from 'vitest'; import { init } from '../src/index.server'; vi.mock('@sentry/node', { spy: true }); -const nodeInit = SentryNode.init as Mock; +const nodeInit = vi.spyOn(SentryNode, 'initWithDefaultIntegrations'); describe('Server init()', () => { afterEach(() => { @@ -39,6 +39,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 9968b0ace8f0..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 { Client } from '@sentry/core'; +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'; /** - * 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 dec8220668a8..ba9ea775e0c3 100644 --- a/packages/solid/test/sdk.test.ts +++ b/packages/solid/test/sdk.test.ts @@ -3,7 +3,7 @@ import * as SentryBrowser from '@sentry/browser'; import { beforeEach, describe, expect, it, vi } from 'vitest'; import { init as solidInit } from '../src/sdk'; -const browserInit = vi.spyOn(SentryBrowser, 'init'); +const browserInit = vi.spyOn(SentryBrowser, 'initWithDefaultIntegrations'); describe('Initialize Solid SDK', () => { beforeEach(() => { @@ -27,6 +27,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 06ee8f092094..bcf1b7ac8139 100644 --- a/packages/solidstart/src/client/sdk.ts +++ b/packages/solidstart/src/client/sdk.ts @@ -4,7 +4,7 @@ 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 e4cd974ed00e..c40e488c3f3e 100644 --- a/packages/solidstart/src/index.types.ts +++ b/packages/solidstart/src/index.types.ts @@ -13,6 +13,10 @@ export * from './config'; /** 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 73bb412d1909..efa1e413e386 100644 --- a/packages/solidstart/test/client/sdk.test.ts +++ b/packages/solidstart/test/client/sdk.test.ts @@ -4,7 +4,7 @@ import { beforeEach, describe, expect, it, 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(() => { @@ -31,7 +31,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 725d9bc66898..9be37dec7bd8 100644 --- a/packages/svelte/test/sdk.test.ts +++ b/packages/svelte/test/sdk.test.ts @@ -7,7 +7,7 @@ import * as SentryBrowser from '@sentry/browser'; import { beforeEach, describe, expect, it, vi } from 'vitest'; import { init as svelteInit } from '../src/sdk'; -const browserInit = vi.spyOn(SentryBrowser, 'init'); +const browserInit = vi.spyOn(SentryBrowser, 'initWithDefaultIntegrations'); describe('Initialize Svelte SDk', () => { beforeEach(() => { @@ -30,7 +30,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", () => { @@ -62,6 +62,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 5c3f482cb7d0..32a6dbdde6a2 100644 --- a/packages/sveltekit/src/client/sdk.ts +++ b/packages/sveltekit/src/client/sdk.ts @@ -1,7 +1,11 @@ import type { Client, Integration } from '@sentry/core'; import { applySdkMetadata } from '@sentry/core'; import type { BrowserOptions } from '@sentry/svelte'; -import { getDefaultIntegrations as getDefaultSvelteIntegrations, init as initSvelteSdk, WINDOW } from '@sentry/svelte'; +import { + getDefaultIntegrations as getDefaultSvelteIntegrations, + initWithDefaultIntegrations, + WINDOW, +} from '@sentry/svelte'; import { browserTracingIntegration as svelteKitBrowserTracingIntegration } from './browserTracingIntegration'; type WindowWithSentryFetchProxy = typeof WINDOW & { @@ -18,7 +22,6 @@ declare const __SENTRY_TRACING__: boolean; */ export function init(options: BrowserOptions): Client | undefined { const opts = { - defaultIntegrations: getDefaultIntegrations(options), ...options, }; @@ -28,7 +31,7 @@ export function init(options: BrowserOptions): Client | undefined { const actualFetch = switchToFetchProxy(); // 2. Initialize the SDK which will instrument our proxy - const client = initSvelteSdk(opts); + const client = initWithDefaultIntegrations(opts, getDefaultIntegrations); // 3. Restore the original fetch now that our proxy is instrumented if (actualFetch) { @@ -38,7 +41,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 03c63041e726..8f43f6966584 100644 --- a/packages/sveltekit/src/index.types.ts +++ b/packages/sveltekit/src/index.types.ts @@ -20,6 +20,10 @@ export { initCloudflareSentryHandle } from './worker'; /** 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 19a0a8f9f5ad..6422d5250c6f 100644 --- a/packages/sveltekit/src/server/sdk.ts +++ b/packages/sveltekit/src/server/sdk.ts @@ -1,7 +1,7 @@ -import { applySdkMetadata } from '@sentry/core'; +import type { Integration } from '@sentry/core'; +import { applySdkMetadata, rewriteFramesIntegration } from '@sentry/core'; import type { NodeClient, NodeOptions } from '@sentry/node'; -import { getDefaultIntegrations as getDefaultNodeIntegrations, init as initNodeSdk } from '@sentry/node'; -import { rewriteFramesIntegration } from '../server-common/rewriteFramesIntegration'; +import { getDefaultIntegrations as getDefaultNodeIntegrations, initWithDefaultIntegrations } from '@sentry/node'; /** * Initialize the Server-side Sentry SDK @@ -9,11 +9,14 @@ import { rewriteFramesIntegration } from '../server-common/rewriteFramesIntegrat */ 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 1bbd2e2bc81f..442b5a793ccd 100644 --- a/packages/sveltekit/test/client/sdk.test.ts +++ b/packages/sveltekit/test/client/sdk.test.ts @@ -1,10 +1,9 @@ -import type { BrowserClient } from '@sentry/svelte'; import * as SentrySvelte from '@sentry/svelte'; -import { getClient, getCurrentScope, getGlobalScope, getIsolationScope, SDK_VERSION } from '@sentry/svelte'; +import { getCurrentScope, getGlobalScope, getIsolationScope, SDK_VERSION } from '@sentry/svelte'; import { afterEach, describe, expect, it, vi } from 'vitest'; import { init } from '../../src/client'; -const svelteInit = vi.spyOn(SentrySvelte, 'init'); +const svelteInit = vi.spyOn(SentrySvelte, 'initWithDefaultIntegrations'); describe('Sentry client SDK', () => { describe('init', () => { @@ -36,6 +35,7 @@ describe('Sentry client SDK', () => { }, }, }), + expect.any(Function), ); }); @@ -45,12 +45,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(); }); @@ -60,12 +60,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-common/sdk.test.ts b/packages/sveltekit/test/server-common/sdk.test.ts index 714828304d35..1e4bbfce9d8d 100644 --- a/packages/sveltekit/test/server-common/sdk.test.ts +++ b/packages/sveltekit/test/server-common/sdk.test.ts @@ -4,7 +4,7 @@ import { getClient, SDK_VERSION } from '@sentry/node'; import { afterEach, describe, expect, it, vi } from 'vitest'; import { init } from '../../src/server/sdk'; -const nodeInit = vi.spyOn(SentryNode, 'init'); +const nodeInit = vi.spyOn(SentryNode, 'initWithDefaultIntegrations'); describe('Sentry server SDK', () => { describe('init', () => { @@ -36,6 +36,7 @@ describe('Sentry server SDK', () => { }, }, }), + expect.any(Function), ); }); diff --git a/packages/sveltekit/test/server/handle.test.ts b/packages/sveltekit/test/server/handle.test.ts index 8b53196bb14e..a25daad14b97 100644 --- a/packages/sveltekit/test/server/handle.test.ts +++ b/packages/sveltekit/test/server/handle.test.ts @@ -11,8 +11,7 @@ describe('Node handle hooks', () => { describe('initCloudflareSentryHandle', () => { it('inits Sentry on the first call but not on subsequent calls', async () => { - // @ts-expect-error - no need for an actual init call - vi.spyOn(NodeSDK, 'init').mockImplementationOnce(() => {}); + const initSpy = vi.spyOn(NodeSDK, 'initWithDefaultIntegrations').mockImplementationOnce(() => ({}) as any); const handle = initCloudflareSentryHandle({ dsn: 'https://public@dsn.ingest.sentry.io/1337' }); expect(handle).toBeDefined(); @@ -20,12 +19,12 @@ describe('Node handle hooks', () => { // @ts-expect-error - no need to call with actual params await handle({ event: {}, resolve: () => Promise.resolve({}) }); - expect(NodeSDK.init).toHaveBeenCalledTimes(1); + expect(initSpy).toHaveBeenCalledTimes(1); // @ts-expect-error - no need to call with actual params await handle({ event: {}, resolve: () => Promise.resolve({}) }); - expect(NodeSDK.init).toHaveBeenCalledTimes(1); + expect(initSpy).toHaveBeenCalledTimes(1); }); }); }); diff --git a/packages/tanstackstart-react/src/index.types.ts b/packages/tanstackstart-react/src/index.types.ts index 85bbe9df63fd..7487cec1d14e 100644 --- a/packages/tanstackstart-react/src/index.types.ts +++ b/packages/tanstackstart-react/src/index.types.ts @@ -13,6 +13,10 @@ export * from './common'; /** Initializes Sentry TanStack 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/vercel-edge/src/sdk.ts b/packages/vercel-edge/src/sdk.ts index bebce0935adf..27a1ba19c632 100644 --- a/packages/vercel-edge/src/sdk.ts +++ b/packages/vercel-edge/src/sdk.ts @@ -71,10 +71,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; } @@ -96,10 +92,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()]; }