diff --git a/packages/angular/src/sdk.ts b/packages/angular/src/sdk.ts index 5a4ffbbde7ee..a5e2d77d4e13 100755 --- a/packages/angular/src/sdk.ts +++ b/packages/angular/src/sdk.ts @@ -2,11 +2,13 @@ import { VERSION } from '@angular/core'; import type { BrowserOptions } from '@sentry/browser'; import { breadcrumbsIntegration, + BrowserClient, browserSessionIntegration, + defaultStackParser, globalHandlersIntegration, httpContextIntegration, - init as browserInit, linkedErrorsIntegration, + makeFetchTransport, setContext, } from '@sentry/browser'; import type { Client, Integration } from '@sentry/core'; @@ -14,7 +16,9 @@ import { applySdkMetadata, dedupeIntegration, functionToStringIntegration, + getClientOptions, inboundFiltersIntegration, + initAndBind, logger, } from '@sentry/core'; import { IS_DEBUG_BUILD } from './flags'; @@ -49,15 +53,19 @@ export function getDefaultIntegrations(_options: BrowserOptions = {}): Integrati * Inits the Angular SDK */ export function init(options: BrowserOptions): Client | undefined { - const opts = { - defaultIntegrations: getDefaultIntegrations(), - ...options, - }; + const clientOptions = getClientOptions(options, { + integrations: getDefaultIntegrations(options), + stackParser: defaultStackParser, + transport: makeFetchTransport, + }); - applySdkMetadata(opts, 'angular'); + applySdkMetadata(clientOptions, 'angular'); + + const client = initAndBind(BrowserClient, clientOptions); checkAndSetAngularVersion(); - return browserInit(opts); + + return client; } function checkAndSetAngularVersion(): void { diff --git a/packages/astro/src/client/sdk.ts b/packages/astro/src/client/sdk.ts index f04725d1ef1e..fb7161c5d10c 100644 --- a/packages/astro/src/client/sdk.ts +++ b/packages/astro/src/client/sdk.ts @@ -1,11 +1,13 @@ import type { BrowserOptions } from '@sentry/browser'; import { + BrowserClient, browserTracingIntegration, + defaultStackParser, getDefaultIntegrations as getBrowserDefaultIntegrations, - init as initBrowserSdk, + makeFetchTransport, } from '@sentry/browser'; import type { Client, Integration } from '@sentry/core'; -import { applySdkMetadata } from '@sentry/core'; +import { applySdkMetadata, getClientOptions, initAndBind } from '@sentry/core'; // Tree-shakable guard to remove all code related to tracing declare const __SENTRY_TRACING__: boolean; @@ -16,14 +18,15 @@ declare const __SENTRY_TRACING__: boolean; * @param options Configuration options for the SDK. */ export function init(options: BrowserOptions): Client | undefined { - const opts = { - defaultIntegrations: getDefaultIntegrations(options), - ...options, - }; + const clientOptions = getClientOptions(options, { + integrations: getDefaultIntegrations(options), + stackParser: defaultStackParser, + transport: makeFetchTransport, + }); - applySdkMetadata(opts, 'astro', ['astro', 'browser']); + applySdkMetadata(clientOptions, 'astro', ['astro', 'browser']); - return initBrowserSdk(opts); + return initAndBind(BrowserClient, clientOptions); } function getDefaultIntegrations(options: BrowserOptions): Integration[] { diff --git a/packages/astro/test/client/sdk.test.ts b/packages/astro/test/client/sdk.test.ts index a537013f7c22..92ec0e86fdf3 100644 --- a/packages/astro/test/client/sdk.test.ts +++ b/packages/astro/test/client/sdk.test.ts @@ -1,18 +1,27 @@ -import type { BrowserClient } from '@sentry/browser'; import { + BrowserClient, browserTracingIntegration, getActiveSpan, - getClient, getCurrentScope, getGlobalScope, getIsolationScope, SDK_VERSION, } from '@sentry/browser'; import * as SentryBrowser from '@sentry/browser'; +import * as SentryCore from '@sentry/core'; import { afterEach, describe, expect, it, vi } from 'vitest'; import { init } from '../../src/client/sdk'; -const browserInit = vi.spyOn(SentryBrowser, 'init'); +const initAndBind = vi.spyOn(SentryCore, 'initAndBind'); + +// Mock this to avoid the "duplicate integration" error message +vi.spyOn(SentryBrowser, 'browserTracingIntegration').mockImplementation(() => { + return { + name: 'BrowserTracing', + setupOnce: vi.fn(), + afterAllSetup: vi.fn(), + }; +}); describe('Sentry client SDK', () => { describe('init', () => { @@ -26,12 +35,13 @@ describe('Sentry client SDK', () => { }); it('adds Astro metadata to the SDK options', () => { - expect(browserInit).not.toHaveBeenCalled(); + expect(initAndBind).not.toHaveBeenCalled(); init({}); - expect(browserInit).toHaveBeenCalledTimes(1); - expect(browserInit).toHaveBeenCalledWith( + expect(initAndBind).toHaveBeenCalledTimes(1); + expect(initAndBind).toHaveBeenCalledWith( + BrowserClient, expect.objectContaining({ _metadata: { sdk: { @@ -53,37 +63,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 +95,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..0dbcae665fb3 100644 --- a/packages/aws-serverless/src/sdk.ts +++ b/packages/aws-serverless/src/sdk.ts @@ -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 initWithoutDefaultIntegrations(opts, getDefaultIntegrations); } /** */ diff --git a/packages/browser/src/client.ts b/packages/browser/src/client.ts index 73cbd55d42db..5153d1ea2fd6 100644 --- a/packages/browser/src/client.ts +++ b/packages/browser/src/client.ts @@ -15,20 +15,34 @@ import { addAutoIpAddressToUser, applySdkMetadata, Client, + consoleSandbox, + getLocationHref, getSDKSource, } from '@sentry/core'; +import { DEBUG_BUILD } from './debug-build'; import { eventFromException, eventFromMessage } from './eventbuilder'; import { WINDOW } from './helpers'; import type { BrowserTransportOptions } from './transports/types'; -const DEFAULT_FLUSH_INTERVAL = 5000; +type ExtensionRuntime = { + runtime?: { + id?: string; + }; +}; +type ExtensionProperties = { + chrome?: ExtensionRuntime; + browser?: ExtensionRuntime; + nw?: unknown; +}; /** - * Configuration options for the Sentry Browser SDK. - * @see @sentry/core Options for more information. + * A magic string that build tooling can leverage in order to inject a release value into the SDK. */ -export type BrowserOptions = Options & - BrowserClientReplayOptions & +declare const __SENTRY_RELEASE__: string | undefined; + +const DEFAULT_FLUSH_INTERVAL = 5000; + +type BrowserSpecificOptions = BrowserClientReplayOptions & BrowserClientProfilingOptions & { /** * Important: Only set this option if you know what you are doing! @@ -48,18 +62,21 @@ export type BrowserOptions = Options & * @default false */ skipBrowserExtensionCheck?: boolean; + + /** If configured, this URL will be used as base URL for lazy loading integration. */ + cdnBaseUrl?: string; }; +/** + * Configuration options for the Sentry Browser SDK. + * @see @sentry/core Options for more information. + */ +export type BrowserOptions = Options & BrowserSpecificOptions; /** * Configuration options for the Sentry Browser SDK Client class * @see BrowserClient for more information. */ -export type BrowserClientOptions = ClientOptions & - BrowserClientReplayOptions & - BrowserClientProfilingOptions & { - /** If configured, this URL will be used as base URL for lazy loading integration. */ - cdnBaseUrl?: string; - }; +export type BrowserClientOptions = ClientOptions & BrowserSpecificOptions; /** * The Sentry Browser SDK Client. @@ -75,14 +92,14 @@ export class BrowserClient extends Client { * @param options Configuration options for this SDK. */ public constructor(options: BrowserClientOptions) { - const opts = { - // We default this to true, as it is the safer scenario - parentSpanIsAlwaysRootSpan: true, - ...options, - }; + const opts = applyDefaultOptions(options); const sdkSource = WINDOW.SENTRY_SDK_SOURCE || getSDKSource(); applySdkMetadata(opts, 'browser', ['browser'], sdkSource); + if (!opts.skipBrowserExtensionCheck && checkIfEmbeddedBrowserExtension()) { + opts.enabled = false; + } + super(opts); // eslint-disable-next-line @typescript-eslint/no-this-alias @@ -155,3 +172,66 @@ export class BrowserClient extends Client { return super._prepareEvent(event, hint, currentScope, isolationScope); } } + +/** Exported only for tests. */ +export function applyDefaultOptions>(optionsArg: T): T { + return { + release: + typeof __SENTRY_RELEASE__ === 'string' // This allows build tooling to find-and-replace __SENTRY_RELEASE__ to inject a release value + ? __SENTRY_RELEASE__ + : WINDOW.SENTRY_RELEASE?.id, // This supports the variable that sentry-webpack-plugin injects + sendClientReports: true, + // We default this to true, as it is the safer scenario + parentSpanIsAlwaysRootSpan: true, + ...optionsArg, + }; +} + +/** + * Returns true if the SDK is running in an embedded browser extension. + * Stand-alone browser extensions (which do not share the same data as the main browser page) are fine. + */ +function checkIfEmbeddedBrowserExtension(): 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 _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; +} diff --git a/packages/browser/src/sdk.ts b/packages/browser/src/sdk.ts index 56f3ace8f193..e82d85c7131a 100644 --- a/packages/browser/src/sdk.ts +++ b/packages/browser/src/sdk.ts @@ -1,18 +1,13 @@ import type { Client, Integration, Options } from '@sentry/core'; import { - consoleSandbox, dedupeIntegration, functionToStringIntegration, - getIntegrationsToSetup, - getLocationHref, + getClientOptions, inboundFiltersIntegration, initAndBind, - stackParserFromStackParserOptions, } from '@sentry/core'; -import type { BrowserClientOptions, BrowserOptions } from './client'; +import type { BrowserOptions } from './client'; import { BrowserClient } from './client'; -import { DEBUG_BUILD } from './debug-build'; -import { WINDOW } from './helpers'; import { breadcrumbsIntegration } from './integrations/breadcrumbs'; import { browserApiErrorsIntegration } from './integrations/browserapierrors'; import { browserSessionIntegration } from './integrations/browsersession'; @@ -22,22 +17,6 @@ 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[] { /** @@ -59,40 +38,6 @@ export function getDefaultIntegrations(_options: Options): Integration[] { ]; } -/** Exported only for tests. */ -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__ - : WINDOW.SENTRY_RELEASE?.id, // This supports the variable that sentry-webpack-plugin injects - sendClientReports: true, - }; - - return { - ...defaultOptions, - ...dropTopLevelUndefinedKeys(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; -} - /** * The Sentry Browser SDK Client. * @@ -139,19 +84,12 @@ function dropTopLevelUndefinedKeys(obj: T): Partial { * * @see {@link BrowserOptions} for documentation on configuration options. */ -export function init(browserOptions: BrowserOptions = {}): Client | undefined { - if (!browserOptions.skipBrowserExtensionCheck && _checkForBrowserExtension()) { - return; - } - - const options = applyDefaultOptions(browserOptions); - const clientOptions: BrowserClientOptions = { - ...options, - stackParser: stackParserFromStackParserOptions(options.stackParser || defaultStackParser), - integrations: getIntegrationsToSetup(options), - transport: options.transport || makeFetchTransport, - }; - +export function init(options: BrowserOptions = {}): Client | undefined { + const clientOptions = getClientOptions(options, { + integrations: getDefaultIntegrations(options), + stackParser: defaultStackParser, + transport: makeFetchTransport, + }); return initAndBind(BrowserClient, clientOptions); } @@ -170,48 +108,3 @@ 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; - } -} diff --git a/packages/browser/test/client.test.ts b/packages/browser/test/client.test.ts index a90f8cdbc388..c0f4e649501a 100644 --- a/packages/browser/test/client.test.ts +++ b/packages/browser/test/client.test.ts @@ -4,7 +4,7 @@ import * as sentryCore from '@sentry/core'; import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; -import { BrowserClient } from '../src/client'; +import { applyDefaultOptions, BrowserClient } from '../src/client'; import { WINDOW } from '../src/helpers'; import { getDefaultBrowserClientOptions } from './helper/browser-client-options'; @@ -118,3 +118,70 @@ describe('BrowserClient', () => { }); }); }); + +describe('applyDefaultOptions', () => { + it('works with empty options', () => { + const options = {}; + const actual = applyDefaultOptions(options); + + expect(actual).toEqual({ + release: undefined, + sendClientReports: true, + parentSpanIsAlwaysRootSpan: true, + }); + }); + + it('works with options', () => { + const options = { + tracesSampleRate: 0.5, + release: '1.0.0', + }; + const actual = applyDefaultOptions(options); + + expect(actual).toEqual({ + release: '1.0.0', + sendClientReports: true, + tracesSampleRate: 0.5, + parentSpanIsAlwaysRootSpan: true, + }); + }); + + it('picks up release from WINDOW.SENTRY_RELEASE.id', () => { + const releaseBefore = WINDOW.SENTRY_RELEASE; + + WINDOW.SENTRY_RELEASE = { id: '1.0.0' }; + const options = { + tracesSampleRate: 0.5, + }; + const actual = applyDefaultOptions(options); + + expect(actual).toEqual({ + release: '1.0.0', + sendClientReports: true, + tracesSampleRate: 0.5, + parentSpanIsAlwaysRootSpan: true, + }); + + WINDOW.SENTRY_RELEASE = releaseBefore; + }); + + it('passed in release takes precedence over WINDOW.SENTRY_RELEASE.id', () => { + const releaseBefore = WINDOW.SENTRY_RELEASE; + + WINDOW.SENTRY_RELEASE = { id: '1.0.0' }; + const options = { + release: '2.0.0', + tracesSampleRate: 0.5, + }; + const actual = applyDefaultOptions(options); + + expect(actual).toEqual({ + release: '2.0.0', + sendClientReports: true, + tracesSampleRate: 0.5, + parentSpanIsAlwaysRootSpan: true, + }); + + WINDOW.SENTRY_RELEASE = releaseBefore; + }); +}); diff --git a/packages/browser/test/sdk.test.ts b/packages/browser/test/sdk.test.ts index 342b008bfc18..183ae13eab06 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 { init } 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,53 @@ 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 spyGetClientOptions = vi.spyOn(SentryCore, 'getClientOptions').mockImplementation(options => { + return { + ...options, + integrations: [], + } as unknown as SentryCore.ClientOptions; + }); + + const options = getDefaultBrowserOptions({ dsn: PUBLIC_DSN }); + init(options); + + expect(spyGetClientOptions).toHaveBeenCalledTimes(1); + expect(spyGetClientOptions).toHaveBeenCalledWith(expect.objectContaining(options), { + integrations: expect.arrayContaining([expect.objectContaining({ name: 'InboundFilters' })]), + stackParser: expect.any(Function), + transport: expect.any(Function), + }); + }); + 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 spyGetClientOptions = vi.spyOn(SentryCore, 'getClientOptions').mockImplementation(options => { + return { + ...options, + integrations: [], + } as unknown as SentryCore.ClientOptions; + }); + 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(spyGetClientOptions).toHaveBeenCalledTimes(1); + expect(spyGetClientOptions).toHaveBeenCalledWith(expect.objectContaining(options), { + integrations: expect.arrayContaining([expect.objectContaining({ name: 'InboundFilters' })]), + stackParser: expect.any(Function), + transport: expect.any(Function), + }); }); - 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 +155,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', () => { @@ -151,8 +172,6 @@ describe('init', () => { expect(consoleErrorSpy).toHaveBeenCalledWith( '[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', () => { @@ -166,8 +185,6 @@ describe('init', () => { expect(consoleErrorSpy).toHaveBeenCalledWith( '[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'])( @@ -224,7 +241,7 @@ describe('init', () => { consoleErrorSpy.mockRestore(); }); - it("doesn't return a client on initialization error", () => { + it('returns a disabled client on initialization error', () => { const consoleErrorSpy = vi.spyOn(console, 'error').mockImplementation(() => {}); Object.defineProperty(WINDOW, 'chrome', { @@ -234,7 +251,9 @@ describe('init', () => { const client = init(options); - expect(client).toBeUndefined(); + expect(client).toBeDefined(); + expect(SentryCore.isEnabled()).toBe(false); + expect(client!['_isEnabled']()).toBe(false); consoleErrorSpy.mockRestore(); }); @@ -245,97 +264,3 @@ describe('init', () => { expect(client).not.toBeUndefined(); }); }); - -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', () => { - const options = { - tracesSampleRate: 0.5, - release: '1.0.0', - }; - 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', () => { - const options = { - defaultIntegrations: false, - } as const; - const actual = applyDefaultOptions(options); - - expect(actual.defaultIntegrations).toStrictEqual(false); - }); - - test('it works with defaultIntegrations=[]', () => { - const options = { - defaultIntegrations: [], - }; - const actual = applyDefaultOptions(options); - - expect(actual.defaultIntegrations).toEqual([]); - }); - - test('it works with tracesSampleRate=undefined', () => { - const options = { - tracesSampleRate: undefined, - } as const; - const actual = applyDefaultOptions(options); - - // Not defined, not even undefined - expect('tracesSampleRate' in actual).toBe(false); - }); - - test('it works with tracesSampleRate=null', () => { - const options = { - tracesSampleRate: null, - } as any; - const actual = applyDefaultOptions(options); - - expect(actual.tracesSampleRate).toStrictEqual(null); - }); - - test('it works with tracesSampleRate=0', () => { - const options = { - tracesSampleRate: 0, - } as const; - const actual = applyDefaultOptions(options); - - expect(actual.tracesSampleRate).toStrictEqual(0); - }); - - test('it does not deep-drop undefined keys', () => { - const options = { - obj: { - prop: undefined, - }, - } as any; - const actual = applyDefaultOptions(options) as any; - - expect('prop' in actual.obj).toBe(true); - expect(actual.obj.prop).toStrictEqual(undefined); - }); -}); diff --git a/packages/bun/src/sdk.ts b/packages/bun/src/sdk.ts index 641567504818..d983564fa543 100644 --- a/packages/bun/src/sdk.ts +++ b/packages/bun/src/sdk.ts @@ -14,7 +14,7 @@ import { contextLinesIntegration, getAutoPerformanceIntegrations, httpIntegration, - init as initNode, + initWithoutDefaultIntegrations, 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 initWithoutDefaultIntegrations(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 a67f003aac56..948bc8010602 100644 --- a/packages/core/src/index.ts +++ b/packages/core/src/index.ts @@ -55,7 +55,7 @@ export { BaseClient, } from './client'; export { ServerRuntimeClient } from './server-runtime-client'; -export { initAndBind, setCurrentClient } from './sdk'; +export { initAndBind, setCurrentClient, getClientOptions } from './sdk'; export { createTransport } from './transports/base'; export { makeOfflineTransport } from './transports/offline'; export { makeMultiplexedTransport } from './transports/multiplexed'; @@ -160,7 +160,7 @@ export { isVueViewModel, } from './utils-hoist/is'; export { isBrowser } from './utils-hoist/isBrowser'; -export { CONSOLE_LEVELS, consoleSandbox, logger, originalConsoleMethods } from './utils-hoist/logger'; +export { CONSOLE_LEVELS, consoleSandbox, logger, originalConsoleMethods, enableLogger } from './utils-hoist/logger'; export type { Logger } from './utils-hoist/logger'; export { addContextToFrame, 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..255ef29494c0 100644 --- a/packages/core/src/sdk.ts +++ b/packages/core/src/sdk.ts @@ -1,8 +1,12 @@ import type { Client } from './client'; import { getCurrentScope } from './currentScopes'; -import { DEBUG_BUILD } from './debug-build'; -import type { ClientOptions } from './types-hoist/options'; -import { consoleSandbox, logger } from './utils-hoist/logger'; +import { getIntegrationsToSetup } from './integration'; +import type { Integration } from './types-hoist/integration'; +import type { ClientOptions, Options } from './types-hoist/options'; +import type { StackParser } from './types-hoist/stacktrace'; +import type { BaseTransportOptions, Transport } from './types-hoist/transport'; +import { enableLogger } from './utils-hoist/logger'; +import { stackParserFromStackParserOptions } from './utils-hoist/stacktrace'; /** A class object that can instantiate Client objects. */ export type ClientClass = new (options: O) => F; @@ -14,30 +18,40 @@ 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 { - if (options.debug === true) { - if (DEBUG_BUILD) { - logger.enable(); - } else { - // use `console.warn` rather than `logger.warn` since by non-debug bundles have all `logger.x` statements stripped - consoleSandbox(() => { - // eslint-disable-next-line no-console - console.warn('[Sentry] Cannot initialize SDK with `debug` option using a non-debug bundle.'); - }); - } +export function initAndBind(ClientClass: ClientClass, options: O): F { + if (options.debug) { + enableLogger(); } const scope = getCurrentScope(); scope.update(options.initialScope); - const client = new clientClass(options); + const client = new ClientClass(options); setCurrentClient(client); client.init(); return client; } +/** Get client options with defaults. */ +export function getClientOptions< + O extends Options, + CO extends ClientOptions, + TO extends BaseTransportOptions = BaseTransportOptions, +>( + options: O, + defaultOptions: { + stackParser: StackParser; + integrations: Integration[]; + transport: (transportOptions: TO) => Transport; + }, +): CO { + return { + ...options, + stackParser: stackParserFromStackParserOptions(options.stackParser || defaultOptions.stackParser), + integrations: getIntegrationsToSetup(options, defaultOptions.integrations), + transport: options.transport || defaultOptions.transport, + } as unknown as CO; +} + /** * Make the given client the current client. */ diff --git a/packages/core/src/types-hoist/options.ts b/packages/core/src/types-hoist/options.ts index 09dab550be4c..14618ca29584 100644 --- a/packages/core/src/types-hoist/options.ts +++ b/packages/core/src/types-hoist/options.ts @@ -403,9 +403,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/logger.ts b/packages/core/src/utils-hoist/logger.ts index 0c1e8f4d169b..d02299885ad1 100644 --- a/packages/core/src/utils-hoist/logger.ts +++ b/packages/core/src/utils-hoist/logger.ts @@ -100,3 +100,16 @@ function makeLogger(): Logger { * The logger is a singleton on the carrier, to ensure that a consistent logger is used throughout the SDK. */ export const logger = getGlobalSingleton('logger', makeLogger); + +/** Enables the logger, or log a warning if DEBUG_BUILD is false. */ +export function enableLogger(): void { + if (DEBUG_BUILD) { + logger.enable(); + } else { + // use `console.warn` rather than `logger.warn` since by non-debug bundles have all `logger.x` statements stripped + consoleSandbox(() => { + // eslint-disable-next-line no-console + console.warn('[Sentry] Cannot initialize SDK with `debug` option using a non-debug bundle.'); + }); + } +} diff --git a/packages/core/src/utils-hoist/supports.ts b/packages/core/src/utils-hoist/supports.ts index 9cb6a71d8058..68377dd92910 100644 --- a/packages/core/src/utils-hoist/supports.ts +++ b/packages/core/src/utils-hoist/supports.ts @@ -79,9 +79,7 @@ function _isFetchSupported(): boolean { } try { - new Headers(); new Request('http://www.example.com'); - new Response(); return true; } catch (e) { return false; 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..8fcd73f0c3ab 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, initWithoutDefaultIntegrations } 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 initWithoutDefaultIntegrations(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..ce729f7f6a41 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); + initWithoutDefaultIntegrations: (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..8b9ab53be760 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) => { + initWithoutDefaultIntegrations: (options: unknown) => { mockInit(options); }, }; diff --git a/packages/nestjs/src/sdk.ts b/packages/nestjs/src/sdk.ts index 733cb935003c..bb4b6ad5308d 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, initWithoutDefaultIntegrations } 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 = initWithoutDefaultIntegrations(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..eeaef11ab4de 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, 'initWithoutDefaultIntegrations'); 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..57f6841a72cf 100644 --- a/packages/nextjs/src/client/index.ts +++ b/packages/nextjs/src/client/index.ts @@ -1,7 +1,19 @@ import type { Client, EventProcessor, Integration } from '@sentry/core'; -import { addEventProcessor, applySdkMetadata, consoleSandbox, getGlobalScope, GLOBAL_OBJ } from '@sentry/core'; +import { + applySdkMetadata, + consoleSandbox, + getClientOptions, + getGlobalScope, + GLOBAL_OBJ, + initAndBind, +} from '@sentry/core'; import type { BrowserOptions } from '@sentry/react'; -import { getDefaultIntegrations as getReactDefaultIntegrations, init as reactInit } from '@sentry/react'; +import { + BrowserClient, + defaultStackParser, + getDefaultIntegrations as getReactDefaultIntegrations, + makeFetchTransport, +} from '@sentry/react'; import { devErrorSymbolicationEventProcessor } from '../common/devErrorSymbolicationEventProcessor'; import { getVercelEnv } from '../common/getVercelEnv'; import { isRedirectNavigationError } from '../common/nextNavigationErrorUtils'; @@ -43,37 +55,42 @@ 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; + }; - applyTunnelRouteOption(opts); - applySdkMetadata(opts, 'nextjs', ['nextjs', 'react']); + const clientOptions = getClientOptions(opts, { + integrations: getDefaultIntegrations(options), + stackParser: defaultStackParser, + transport: makeFetchTransport, + }); - const client = reactInit(opts); + applyTunnelRouteOption(clientOptions); + applySdkMetadata(clientOptions, 'nextjs', ['nextjs', 'react']); + + const client = initAndBind(BrowserClient, clientOptions); const filterTransactions: EventProcessor = event => event.type === 'transaction' && event.transaction === '/404' ? null : event; filterTransactions.id = 'NextClient404Filter'; - addEventProcessor(filterTransactions); + client.addEventProcessor(filterTransactions); const filterIncompleteNavigationTransactions: EventProcessor = event => event.type === 'transaction' && event.transaction === INCOMPLETE_APP_ROUTER_INSTRUMENTATION_TRANSACTION_NAME ? null : event; filterIncompleteNavigationTransactions.id = 'IncompleteTransactionFilter'; - addEventProcessor(filterIncompleteNavigationTransactions); + client.addEventProcessor(filterIncompleteNavigationTransactions); const filterNextRedirectError: EventProcessor = (event, hint) => isRedirectNavigationError(hint?.originalException) || event.exception?.values?.[0]?.value === 'NEXT_REDIRECT' ? null : event; filterNextRedirectError.id = 'NextRedirectErrorFilter'; - addEventProcessor(filterNextRedirectError); + client.addEventProcessor(filterNextRedirectError); if (process.env.NODE_ENV === 'development') { - addEventProcessor(devErrorSymbolicationEventProcessor); + client.addEventProcessor(devErrorSymbolicationEventProcessor); } try { 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/server/index.ts b/packages/nextjs/src/server/index.ts index a6594e7fae1e..1c763322f86a 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, initWithoutDefaultIntegrations } 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 = initWithoutDefaultIntegrations(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 @@ -386,6 +370,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..7e998befd73e 100644 --- a/packages/nextjs/test/clientSdk.test.ts +++ b/packages/nextjs/test/clientSdk.test.ts @@ -1,14 +1,23 @@ -import type { Integration } from '@sentry/core'; -import { getGlobalScope, getIsolationScope, logger } from '@sentry/core'; +import { getGlobalScope, getIsolationScope, logger, startInactiveSpan, withActiveSpan } from '@sentry/core'; +import * as SentryCore from '@sentry/core'; +import { BrowserClient, getClient, getCurrentScope, httpClientIntegration, WINDOW } from '@sentry/react'; import * as SentryReact from '@sentry/react'; -import { getClient, getCurrentScope, WINDOW } from '@sentry/react'; import { JSDOM } from 'jsdom'; import { afterAll, afterEach, describe, expect, it, vi } from 'vitest'; -import { breadcrumbsIntegration, browserTracingIntegration, init } from '../src/client'; +import { browserTracingIntegration, init } from '../src/client'; -const reactInit = vi.spyOn(SentryReact, 'init'); +const initAndBind = vi.spyOn(SentryCore, 'initAndBind'); const loggerLogSpy = vi.spyOn(logger, 'log'); +// Mock this to avoid the "duplicate integration" error message +vi.spyOn(SentryReact, 'browserTracingIntegration').mockImplementation(() => { + return { + name: 'BrowserTracing', + setupOnce: vi.fn(), + afterAllSetup: vi.fn(), + }; +}); + // We're setting up JSDom here because the Next.js routing instrumentations requires a few things to be present on pageload: // 1. Access to window.document API for `window.document.getElementById` // 2. Access to window.location API for `window.location.pathname` @@ -29,10 +38,6 @@ afterAll(() => { Object.defineProperty(WINDOW, 'addEventListener', { value: originalGlobalAddEventListener }); }); -function findIntegrationByName(integrations: Integration[] = [], name: string): Integration | undefined { - return integrations.find(integration => integration.name === name); -} - const TEST_DSN = 'https://public@dsn.ingest.sentry.io/1337'; describe('Client init()', () => { @@ -46,11 +51,15 @@ describe('Client init()', () => { }); it('inits the React SDK', () => { - expect(reactInit).toHaveBeenCalledTimes(0); + expect(initAndBind).toHaveBeenCalledTimes(0); init({}); - expect(reactInit).toHaveBeenCalledTimes(1); - expect(reactInit).toHaveBeenCalledWith( + expect(initAndBind).toHaveBeenCalledTimes(1); + expect(initAndBind).toHaveBeenCalledWith( + BrowserClient, expect.objectContaining({ + integrations: expect.arrayContaining([ + expect.objectContaining({ name: 'NextjsClientStackFrameNormalization' }), + ]), _metadata: { sdk: { name: 'sentry.javascript.nextjs', @@ -68,11 +77,6 @@ describe('Client init()', () => { }, }, environment: 'test', - defaultIntegrations: expect.arrayContaining([ - expect.objectContaining({ - name: 'NextjsClientStackFrameNormalization', - }), - ]), }), ); }); @@ -85,8 +89,8 @@ describe('Client init()', () => { const transportSend = vi.spyOn(getClient()!.getTransport()!, 'send'); // Ensure we have no current span, so our next span is a transaction - SentryReact.withActiveSpan(null, () => { - SentryReact.startInactiveSpan({ name: '/404' })?.end(); + withActiveSpan(null, () => { + startInactiveSpan({ name: '/404' })?.end(); }); expect(transportSend).not.toHaveBeenCalled(); @@ -94,16 +98,11 @@ describe('Client init()', () => { }); describe('integrations', () => { - // Options passed by `@sentry/nextjs`'s `init` to `@sentry/react`'s `init` after modifying them - type ModifiedInitOptionsIntegrationArray = { defaultIntegrations: Integration[]; integrations: Integration[] }; - it('supports passing unrelated integrations through options', () => { - init({ integrations: [breadcrumbsIntegration({ console: false })] }); - - const reactInitOptions = reactInit.mock.calls[0]![0] as ModifiedInitOptionsIntegrationArray; - const installedBreadcrumbsIntegration = findIntegrationByName(reactInitOptions.integrations, 'Breadcrumbs'); + const client = init({ dsn: TEST_DSN, integrations: [httpClientIntegration()] }); - expect(installedBreadcrumbsIntegration).toBeDefined(); + const integration = client?.getIntegrationByName('HttpClient'); + expect(integration).toBeDefined(); }); it('forces correct router instrumentation if user provides `browserTracingIntegration` in an array', () => { diff --git a/packages/nextjs/test/serverSdk.test.ts b/packages/nextjs/test/serverSdk.test.ts index 8ea0b060155e..9e81bd4a70cc 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, 'initWithoutDefaultIntegrations'); 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/sdk/client.ts b/packages/node/src/sdk/client.ts index 0e5718b30207..d60a57471300 100644 --- a/packages/node/src/sdk/client.ts +++ b/packages/node/src/sdk/client.ts @@ -3,12 +3,23 @@ import type { Tracer } from '@opentelemetry/api'; import { trace } from '@opentelemetry/api'; import { registerInstrumentations } from '@opentelemetry/instrumentation'; import type { BasicTracerProvider } from '@opentelemetry/sdk-trace-base'; -import type { DynamicSamplingContext, Scope, ServerRuntimeClientOptions, TraceContext } from '@sentry/core'; -import { _INTERNAL_flushLogsBuffer, applySdkMetadata, logger, SDK_VERSION, ServerRuntimeClient } from '@sentry/core'; +import type { DynamicSamplingContext, Scope, TraceContext } from '@sentry/core'; +import { + _INTERNAL_flushLogsBuffer, + applySdkMetadata, + logger, + propagationContextFromHeaders, + SDK_VERSION, + ServerRuntimeClient, +} from '@sentry/core'; import { getTraceContextForScope } from '@sentry/opentelemetry'; import { isMainThread, threadId } from 'worker_threads'; +import { getCurrentScope } from '..'; import { DEBUG_BUILD } from '../debug-build'; import type { NodeClientOptions } from '../types'; +import { isCjs } from '../utils/commonjs'; +import { envToBool } from '../utils/envToBool'; +import { getSentryRelease } from './api'; const DEFAULT_CLIENT_REPORT_FLUSH_INTERVAL_MS = 60_000; // 60s was chosen arbitrarily @@ -21,15 +32,9 @@ export class NodeClient extends ServerRuntimeClient { private _logOnExitFlushListener: (() => void) | undefined; public constructor(options: NodeClientOptions) { - const serverName = options.serverName || global.process.env.SENTRY_NAME || os.hostname(); - const clientOptions: ServerRuntimeClientOptions = { - ...options, - platform: 'node', - runtime: { name: 'node', version: global.process.version }, - serverName, - }; - - if (options.openTelemetryInstrumentations) { + const clientOptions = applyDefaultOptions(options); + + if (clientOptions.openTelemetryInstrumentations) { registerInstrumentations({ instrumentations: options.openTelemetryInstrumentations, }); @@ -40,19 +45,24 @@ export class NodeClient extends ServerRuntimeClient { logger.log( `Initializing Sentry: process: ${process.pid}, thread: ${isMainThread ? 'main' : `worker-${threadId}`}.`, ); + logger.log(`Running in ${isCjs() ? 'CommonJS' : 'ESM'} mode.`); super(clientOptions); + updateScopeFromEnvVariables(); + + this.startClientReportTracking(); + if (this.getOptions()._experiments?.enableLogs) { this._logOnExitFlushListener = () => { _INTERNAL_flushLogsBuffer(this); }; - if (serverName) { + if (clientOptions.serverName) { this.on('beforeCaptureLog', log => { log.attributes = { ...log.attributes, - 'server.address': serverName, + 'server.address': clientOptions.serverName, }; }); } @@ -154,3 +164,70 @@ export class NodeClient extends ServerRuntimeClient { return getTraceContextForScope(this, scope); } } + +function applyDefaultOptions>(options: T): T { + const release = getRelease(options.release); + const spotlight = + options.spotlight ?? envToBool(process.env.SENTRY_SPOTLIGHT, { strict: true }) ?? process.env.SENTRY_SPOTLIGHT; + const tracesSampleRate = getTracesSampleRate(options.tracesSampleRate); + const serverName = options.serverName || global.process.env.SENTRY_NAME || os.hostname(); + + return { + platform: 'node', + runtime: { name: 'node', version: global.process.version }, + serverName, + ...options, + dsn: options.dsn ?? process.env.SENTRY_DSN, + environment: options.environment ?? process.env.SENTRY_ENVIRONMENT, + sendClientReports: options.sendClientReports ?? true, + release, + tracesSampleRate, + spotlight, + debug: envToBool(options.debug ?? process.env.SENTRY_DEBUG), + }; +} + +function getRelease(release: NodeClientOptions['release']): string | undefined { + if (release !== undefined) { + return release; + } + + const detectedRelease = getSentryRelease(); + if (detectedRelease !== undefined) { + return detectedRelease; + } + + return undefined; +} + +/** + * Tries to get a `tracesSampleRate`, possibly extracted from the environment variables. + */ +export function getTracesSampleRate(tracesSampleRate: NodeClientOptions['tracesSampleRate']): number | undefined { + if (tracesSampleRate !== undefined) { + return tracesSampleRate; + } + + const sampleRateFromEnv = process.env.SENTRY_TRACES_SAMPLE_RATE; + if (!sampleRateFromEnv) { + return undefined; + } + + const parsed = parseFloat(sampleRateFromEnv); + return isFinite(parsed) ? parsed : undefined; +} + +/** + * Update scope and propagation context based on environmental variables. + * + * See https://github.com/getsentry/rfcs/blob/main/text/0071-continue-trace-over-process-boundaries.md + * for more details. + */ +function updateScopeFromEnvVariables(): void { + if (envToBool(process.env.SENTRY_USE_ENVIRONMENT) !== false) { + const sentryTraceEnv = process.env.SENTRY_TRACE; + const baggageEnv = process.env.SENTRY_BAGGAGE; + const propagationContext = propagationContextFromHeaders(sentryTraceEnv, baggageEnv); + getCurrentScope().setPropagationContext(propagationContext); + } +} diff --git a/packages/node/src/sdk/index.ts b/packages/node/src/sdk/index.ts index 1536242cfdcb..28161202c94a 100644 --- a/packages/node/src/sdk/index.ts +++ b/packages/node/src/sdk/index.ts @@ -1,17 +1,14 @@ -import type { Integration, Options } from '@sentry/core'; +import type { Integration } from '@sentry/core'; import { consoleIntegration, - consoleSandbox, functionToStringIntegration, - getCurrentScope, - getIntegrationsToSetup, + getClientOptions, hasSpansEnabled, inboundFiltersIntegration, + initAndBind, linkedErrorsIntegration, logger, - propagationContextFromHeaders, requestDataIntegration, - stackParserFromStackParserOptions, } from '@sentry/core'; import { enhanceDscWithOpenTelemetryRootSpanName, @@ -30,14 +27,13 @@ import { nativeNodeFetchIntegration } from '../integrations/node-fetch'; import { onUncaughtExceptionIntegration } from '../integrations/onuncaughtexception'; import { onUnhandledRejectionIntegration } from '../integrations/onunhandledrejection'; import { processSessionIntegration } from '../integrations/processSession'; -import { INTEGRATION_NAME as SPOTLIGHT_INTEGRATION_NAME, spotlightIntegration } from '../integrations/spotlight'; +import { spotlightIntegration } from '../integrations/spotlight'; import { getAutoPerformanceIntegrations } from '../integrations/tracing'; import { makeNodeTransport } from '../transports'; import type { NodeClientOptions, NodeOptions } from '../types'; import { isCjs } from '../utils/commonjs'; -import { envToBool } from '../utils/envToBool'; -import { defaultStackParser, getSentryRelease } from './api'; -import { NodeClient } from './client'; +import { defaultStackParser } from './api'; +import { getTracesSampleRate, NodeClient } from './client'; import { initOpenTelemetry, maybeInitializeEsmLoader } from './initOtel'; function getCjsOnlyIntegrations(): Integration[] { @@ -74,9 +70,12 @@ 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(), + ...(options.spotlight + ? [spotlightIntegration({ sidecarUrl: typeof options.spotlight === 'string' ? options.spotlight : undefined })] + : []), // We only add performance integrations if tracing is enabled // Note that this means that without tracing enabled, e.g. `expressIntegration()` will not be added // This means that generally request isolation will work (because that is done by httpIntegration) @@ -88,66 +87,54 @@ export function getDefaultIntegrations(options: Options): Integration[] { /** * Initialize Sentry for Node. */ -export function init(options: NodeOptions | undefined = {}): NodeClient | undefined { +export function init(options: NodeOptions = {}): NodeClient | undefined { return _init(options, getDefaultIntegrations); } /** * Initialize Sentry for Node, without any integrations added by default. + * This allows to optionally pass a function that returns default integrations that should be used instead. */ -export function initWithoutDefaultIntegrations(options: NodeOptions | undefined = {}): NodeClient { - return _init(options, () => []); +export function initWithoutDefaultIntegrations( + options: NodeOptions = {}, + getDefaultIntegrationsImpl?: (options: NodeOptions) => Integration[], +): NodeClient { + return _init(options, getDefaultIntegrationsImpl || (() => [])); } /** - * 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( - _options: NodeOptions | undefined = {}, - getDefaultIntegrationsImpl: (options: Options) => Integration[], + options: NodeOptions = {}, + getDefaultIntegrationsImpl: (options: NodeOptions) => Integration[], ): NodeClient { - const options = getClientOptions(_options, getDefaultIntegrationsImpl); - - if (options.debug === true) { - if (DEBUG_BUILD) { - logger.enable(); - } else { - // use `console.warn` rather than `logger.warn` since by non-debug bundles have all `logger.x` statements stripped - consoleSandbox(() => { - // eslint-disable-next-line no-console - console.warn('[Sentry] Cannot initialize SDK with `debug` option using a non-debug bundle.'); - }); - } - } - if (!isCjs() && options.registerEsmLoaderHooks !== false) { maybeInitializeEsmLoader(); } setOpenTelemetryContextAsyncContextStrategy(); - const scope = getCurrentScope(); - scope.update(options.initialScope); - - if (options.spotlight && !options.integrations.some(({ name }) => name === SPOTLIGHT_INTEGRATION_NAME)) { - options.integrations.push( - spotlightIntegration({ - sidecarUrl: typeof options.spotlight === 'string' ? options.spotlight : undefined, - }), - ); - } - - const client = new NodeClient(options); - // The client is on the current scope, from where it generally is inherited - getCurrentScope().setClient(client); - - client.init(); - - logger.log(`Running in ${isCjs() ? 'CommonJS' : 'ESM'} mode.`); + // We need to make sure to extract the tracesSampleRate already here, before we pass it to `getDefaultIntegrationsImpl` + // As otherwise, the check for `hasSpansEnabled` may not work in all scenarios + const optionsWithTracesSampleRate = { + ...options, + tracesSampleRate: getTracesSampleRate(options.tracesSampleRate), + }; + const defaultIntegrations = getDefaultIntegrationsImpl(optionsWithTracesSampleRate); - client.startClientReportTracking(); + const clientOptions = getClientOptions(options, { + integrations: defaultIntegrations, + stackParser: defaultStackParser, + transport: makeNodeTransport, + }); - updateScopeFromEnvVariables(); + const client = initAndBind(NodeClient, clientOptions); // If users opt-out of this, they _have_ to set up OpenTelemetry themselves // There is no way to use this SDK without OpenTelemetry! @@ -194,79 +181,3 @@ export function validateOpenTelemetrySetup(): void { ); } } - -function getClientOptions( - options: NodeOptions, - getDefaultIntegrationsImpl: (options: Options) => Integration[], -): NodeClientOptions { - const release = getRelease(options.release); - const spotlight = - options.spotlight ?? envToBool(process.env.SENTRY_SPOTLIGHT, { strict: true }) ?? process.env.SENTRY_SPOTLIGHT; - const tracesSampleRate = getTracesSampleRate(options.tracesSampleRate); - - const mergedOptions = { - ...options, - dsn: options.dsn ?? process.env.SENTRY_DSN, - environment: options.environment ?? process.env.SENTRY_ENVIRONMENT, - sendClientReports: options.sendClientReports ?? true, - transport: options.transport ?? makeNodeTransport, - stackParser: stackParserFromStackParserOptions(options.stackParser || defaultStackParser), - release, - tracesSampleRate, - spotlight, - debug: envToBool(options.debug ?? process.env.SENTRY_DEBUG), - }; - - const integrations = options.integrations; - const defaultIntegrations = options.defaultIntegrations ?? getDefaultIntegrationsImpl(mergedOptions); - - return { - ...mergedOptions, - integrations: getIntegrationsToSetup({ - defaultIntegrations, - integrations, - }), - }; -} - -function getRelease(release: NodeOptions['release']): string | undefined { - if (release !== undefined) { - return release; - } - - const detectedRelease = getSentryRelease(); - if (detectedRelease !== undefined) { - return detectedRelease; - } - - return undefined; -} - -function getTracesSampleRate(tracesSampleRate: NodeOptions['tracesSampleRate']): number | undefined { - if (tracesSampleRate !== undefined) { - return tracesSampleRate; - } - - const sampleRateFromEnv = process.env.SENTRY_TRACES_SAMPLE_RATE; - if (!sampleRateFromEnv) { - return undefined; - } - - const parsed = parseFloat(sampleRateFromEnv); - return isFinite(parsed) ? parsed : undefined; -} - -/** - * Update scope and propagation context based on environmental variables. - * - * See https://github.com/getsentry/rfcs/blob/main/text/0071-continue-trace-over-process-boundaries.md - * for more details. - */ -function updateScopeFromEnvVariables(): void { - if (envToBool(process.env.SENTRY_USE_ENVIRONMENT) !== false) { - const sentryTraceEnv = process.env.SENTRY_TRACE; - const baggageEnv = process.env.SENTRY_BAGGAGE; - const propagationContext = propagationContextFromHeaders(sentryTraceEnv, baggageEnv); - getCurrentScope().setPropagationContext(propagationContext); - } -} diff --git a/packages/node/src/sdk/initOtel.ts b/packages/node/src/sdk/initOtel.ts index 26b9cfa71f72..6838e0fc476d 100644 --- a/packages/node/src/sdk/initOtel.ts +++ b/packages/node/src/sdk/initOtel.ts @@ -7,7 +7,7 @@ import { ATTR_SERVICE_VERSION, SEMRESATTRS_SERVICE_NAMESPACE, } from '@opentelemetry/semantic-conventions'; -import { consoleSandbox, GLOBAL_OBJ, logger, SDK_VERSION } from '@sentry/core'; +import { consoleSandbox, enableLogger, GLOBAL_OBJ, logger, SDK_VERSION } from '@sentry/core'; import { SentryPropagator, SentrySampler, SentrySpanProcessor } from '@sentry/opentelemetry'; import { createAddHookMessageChannel } from 'import-in-the-middle'; import moduleModule from 'module'; @@ -52,7 +52,10 @@ export function maybeInitializeEsmLoader(): void { transferList: [addHookMessagePort], }); } catch (error) { - logger.warn('Failed to register ESM hook', error); + consoleSandbox(() => { + // eslint-disable-next-line no-console + console.warn('Failed to register ESM hook', error); + }); } } } else { @@ -79,7 +82,7 @@ export function preloadOpenTelemetry(options: NodePreloadOptions = {}): void { const { debug } = options; if (debug) { - logger.enable(); + enableLogger(); setupOpenTelemetryLogger(); } 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..55e982f14f69 100644 --- a/packages/nuxt/src/index.types.ts +++ b/packages/nuxt/src/index.types.ts @@ -12,6 +12,7 @@ export * from './index.server'; // re-export colliding types export declare function init(options: Options | SentryNuxtClientOptions | SentryNuxtServerOptions): 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..5cf69b9c5f24 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, + initWithoutDefaultIntegrations, } 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 = initWithoutDefaultIntegrations(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..f0fe2c1bc954 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, 'initWithoutDefaultIntegrations'); 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..60a889aee4f7 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,7 @@ 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 const contextLinesIntegration: typeof clientSdk.contextLinesIntegration; 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..e59f938e85bb 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, + initWithoutDefaultIntegrations, + 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 = initWithoutDefaultIntegrations(opts, getRemixDefaultIntegrations); instrumentServer(); diff --git a/packages/remix/test/index.server.test.ts b/packages/remix/test/index.server.test.ts index 8e61ebd77e06..b551eb9ee770 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, 'initWithoutDefaultIntegrations'); describe('Server init()', () => { afterEach(() => { @@ -39,6 +39,7 @@ describe('Server init()', () => { }, }, }), + expect.any(Function), ); }); diff --git a/packages/solid/src/sdk.ts b/packages/solid/src/sdk.ts index 9968b0ace8f0..efcdde3b59bc 100644 --- a/packages/solid/src/sdk.ts +++ b/packages/solid/src/sdk.ts @@ -4,7 +4,7 @@ import type { Client } from '@sentry/core'; import { applySdkMetadata } from '@sentry/core'; /** - * Initializes the Solid SDK + * Initializes the Solid SDK. */ export function init(options: BrowserOptions): Client | undefined { const opts = { @@ -12,6 +12,5 @@ export function init(options: BrowserOptions): Client | undefined { }; applySdkMetadata(opts, 'solid'); - return browserInit(opts); } diff --git a/packages/solid/test/sdk.test.ts b/packages/solid/test/sdk.test.ts index dec8220668a8..eb1557c2637a 100644 --- a/packages/solid/test/sdk.test.ts +++ b/packages/solid/test/sdk.test.ts @@ -1,9 +1,19 @@ -import { SDK_VERSION } from '@sentry/browser'; +import { BrowserClient, SDK_VERSION } from '@sentry/browser'; import * as SentryBrowser from '@sentry/browser'; +import * as SentryCore from '@sentry/core'; import { beforeEach, describe, expect, it, vi } from 'vitest'; -import { init as solidInit } from '../src/sdk'; +import { init } from '../src/sdk'; -const browserInit = vi.spyOn(SentryBrowser, 'init'); +const initAndBind = vi.spyOn(SentryCore, 'initAndBind'); + +// Mock this to avoid the "duplicate integration" error message +vi.spyOn(SentryBrowser, 'browserTracingIntegration').mockImplementation(() => { + return { + name: 'BrowserTracing', + setupOnce: vi.fn(), + afterAllSetup: vi.fn(), + }; +}); describe('Initialize Solid SDK', () => { beforeEach(() => { @@ -11,7 +21,7 @@ describe('Initialize Solid SDK', () => { }); it('has the correct metadata', () => { - const client = solidInit({ + const client = init({ dsn: 'https://public@dsn.ingest.sentry.io/1337', }); @@ -26,7 +36,7 @@ describe('Initialize Solid SDK', () => { }; expect(client).not.toBeUndefined(); - expect(browserInit).toHaveBeenCalledTimes(1); - expect(browserInit).toHaveBeenLastCalledWith(expect.objectContaining(expectedMetadata)); + expect(initAndBind).toHaveBeenCalledTimes(1); + expect(initAndBind).toHaveBeenLastCalledWith(BrowserClient, expect.objectContaining(expectedMetadata)); }); }); diff --git a/packages/solidstart/src/client/sdk.ts b/packages/solidstart/src/client/sdk.ts index 06ee8f092094..5e7346fb6b39 100644 --- a/packages/solidstart/src/client/sdk.ts +++ b/packages/solidstart/src/client/sdk.ts @@ -1,10 +1,12 @@ import type { Client, Integration } from '@sentry/core'; -import { applySdkMetadata } from '@sentry/core'; +import { applySdkMetadata, getClientOptions, initAndBind } from '@sentry/core'; +import { defaultStackParser } from '@sentry/node'; import type { BrowserOptions } from '@sentry/solid'; import { + BrowserClient, browserTracingIntegration, getDefaultIntegrations as getDefaultSolidIntegrations, - init as initSolidSDK, + makeFetchTransport, } from '@sentry/solid'; // Treeshakable guard to remove all code related to tracing @@ -14,14 +16,15 @@ declare const __SENTRY_TRACING__: boolean; * Initializes the client side of the Solid Start SDK. */ export function init(options: BrowserOptions): Client | undefined { - const opts = { - defaultIntegrations: getDefaultIntegrations(options), - ...options, - }; + const clientOptions = getClientOptions(options, { + integrations: getDefaultIntegrations(options), + stackParser: defaultStackParser, + transport: makeFetchTransport, + }); - applySdkMetadata(opts, 'solidstart', ['solidstart', 'solid']); + applySdkMetadata(clientOptions, 'solidstart', ['solidstart', 'solid']); - return initSolidSDK(opts); + return initAndBind(BrowserClient, clientOptions); } function getDefaultIntegrations(options: BrowserOptions): Integration[] { diff --git a/packages/solidstart/test/client/sdk.test.ts b/packages/solidstart/test/client/sdk.test.ts index 73bb412d1909..6cc04ce179a1 100644 --- a/packages/solidstart/test/client/sdk.test.ts +++ b/packages/solidstart/test/client/sdk.test.ts @@ -1,10 +1,20 @@ -import { SDK_VERSION } from '@sentry/solid'; +import * as SentryCore from '@sentry/core'; +import { BrowserClient, SDK_VERSION } from '@sentry/solid'; import * as SentrySolid from '@sentry/solid'; 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 initAndBind = vi.spyOn(SentryCore, 'initAndBind'); + +// Mock this to avoid the "duplicate integration" error message +vi.spyOn(SentrySolid, 'browserTracingIntegration').mockImplementation(() => { + return { + name: 'BrowserTracing', + setupOnce: vi.fn(), + afterAllSetup: vi.fn(), + }; +}); describe('Initialize Solid Start SDK', () => { beforeEach(() => { @@ -30,8 +40,8 @@ describe('Initialize Solid Start SDK', () => { }; expect(client).not.toBeUndefined(); - expect(browserInit).toHaveBeenCalledTimes(1); - expect(browserInit).toHaveBeenLastCalledWith(expect.objectContaining(expectedMetadata)); + expect(initAndBind).toHaveBeenCalledTimes(1); + expect(initAndBind).toHaveBeenLastCalledWith(BrowserClient, expect.objectContaining(expectedMetadata)); }); }); 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/sdk.ts b/packages/svelte/src/sdk.ts index b46a09bfdfa9..51f529b90b85 100644 --- a/packages/svelte/src/sdk.ts +++ b/packages/svelte/src/sdk.ts @@ -2,6 +2,7 @@ import type { BrowserOptions } from '@sentry/browser'; import { init as browserInit } from '@sentry/browser'; import type { Client } from '@sentry/core'; import { applySdkMetadata } from '@sentry/core'; + /** * Inits the Svelte SDK */ diff --git a/packages/svelte/test/sdk.test.ts b/packages/svelte/test/sdk.test.ts index 725d9bc66898..27260e0d9b77 100644 --- a/packages/svelte/test/sdk.test.ts +++ b/packages/svelte/test/sdk.test.ts @@ -2,12 +2,22 @@ * @vitest-environment jsdom */ -import { SDK_VERSION } from '@sentry/browser'; +import { BrowserClient, SDK_VERSION } from '@sentry/browser'; import * as SentryBrowser from '@sentry/browser'; +import * as SentryCore from '@sentry/core'; import { beforeEach, describe, expect, it, vi } from 'vitest'; import { init as svelteInit } from '../src/sdk'; -const browserInit = vi.spyOn(SentryBrowser, 'init'); +const initAndBind = vi.spyOn(SentryCore, 'initAndBind'); + +// Mock this to avoid the "duplicate integration" error message +vi.spyOn(SentryBrowser, 'browserTracingIntegration').mockImplementation(() => { + return { + name: 'BrowserTracing', + setupOnce: vi.fn(), + afterAllSetup: vi.fn(), + }; +}); describe('Initialize Svelte SDk', () => { beforeEach(() => { @@ -29,8 +39,8 @@ describe('Initialize Svelte SDk', () => { }, }; - expect(browserInit).toHaveBeenCalledTimes(1); - expect(browserInit).toHaveBeenLastCalledWith(expect.objectContaining(expectedMetadata)); + expect(initAndBind).toHaveBeenCalledTimes(1); + expect(initAndBind).toHaveBeenLastCalledWith(BrowserClient, expect.objectContaining(expectedMetadata)); }); it("doesn't add the default svelte metadata, if metadata is already passed", () => { @@ -48,8 +58,9 @@ describe('Initialize Svelte SDk', () => { }, }); - expect(browserInit).toHaveBeenCalledTimes(1); - expect(browserInit).toHaveBeenLastCalledWith( + expect(initAndBind).toHaveBeenCalledTimes(1); + expect(initAndBind).toHaveBeenLastCalledWith( + BrowserClient, expect.objectContaining({ _metadata: { sdk: { diff --git a/packages/sveltekit/src/client/sdk.ts b/packages/sveltekit/src/client/sdk.ts index 5c3f482cb7d0..e339fa0f0443 100644 --- a/packages/sveltekit/src/client/sdk.ts +++ b/packages/sveltekit/src/client/sdk.ts @@ -1,7 +1,13 @@ import type { Client, Integration } from '@sentry/core'; -import { applySdkMetadata } from '@sentry/core'; +import { applySdkMetadata, getClientOptions, initAndBind } from '@sentry/core'; import type { BrowserOptions } from '@sentry/svelte'; -import { getDefaultIntegrations as getDefaultSvelteIntegrations, init as initSvelteSdk, WINDOW } from '@sentry/svelte'; +import { + BrowserClient, + defaultStackParser, + getDefaultIntegrations as getDefaultSvelteIntegrations, + makeFetchTransport, + WINDOW, +} from '@sentry/svelte'; import { browserTracingIntegration as svelteKitBrowserTracingIntegration } from './browserTracingIntegration'; type WindowWithSentryFetchProxy = typeof WINDOW & { @@ -17,18 +23,18 @@ declare const __SENTRY_TRACING__: boolean; * @param options Configuration options for the SDK. */ export function init(options: BrowserOptions): Client | undefined { - const opts = { - defaultIntegrations: getDefaultIntegrations(options), - ...options, - }; - - applySdkMetadata(opts, 'sveltekit', ['sveltekit', 'svelte']); - // 1. Switch window.fetch to our fetch proxy we injected earlier const actualFetch = switchToFetchProxy(); // 2. Initialize the SDK which will instrument our proxy - const client = initSvelteSdk(opts); + const clientOptions = getClientOptions(options, { + integrations: getDefaultIntegrations(options), + stackParser: defaultStackParser, + transport: makeFetchTransport, + }); + applySdkMetadata(clientOptions, 'sveltekit', ['sveltekit', 'svelte']); + + const client = initAndBind(BrowserClient, clientOptions); // 3. Restore the original fetch now that our proxy is instrumented if (actualFetch) { @@ -38,7 +44,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/server/sdk.ts b/packages/sveltekit/src/server/sdk.ts index 19a0a8f9f5ad..a4f4826ea155 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, initWithoutDefaultIntegrations } 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 initWithoutDefaultIntegrations(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..de590ef2cdf9 100644 --- a/packages/sveltekit/test/client/sdk.test.ts +++ b/packages/sveltekit/test/client/sdk.test.ts @@ -1,10 +1,19 @@ -import type { BrowserClient } from '@sentry/svelte'; +import * as SentryCore from '@sentry/core'; import * as SentrySvelte from '@sentry/svelte'; -import { getClient, getCurrentScope, getGlobalScope, getIsolationScope, SDK_VERSION } from '@sentry/svelte'; +import { BrowserClient,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 initAndBind = vi.spyOn(SentryCore, 'initAndBind'); + +// Mock this to avoid the "duplicate integration" error message +vi.spyOn(SentrySvelte, 'browserTracingIntegration').mockImplementation(() => { + return { + name: 'BrowserTracing', + setupOnce: vi.fn(), + afterAllSetup: vi.fn(), + }; +}); describe('Sentry client SDK', () => { describe('init', () => { @@ -18,12 +27,13 @@ describe('Sentry client SDK', () => { }); it('adds SvelteKit metadata to the SDK options', () => { - expect(svelteInit).not.toHaveBeenCalled(); + expect(initAndBind).not.toHaveBeenCalled(); init({}); - expect(svelteInit).toHaveBeenCalledTimes(1); - expect(svelteInit).toHaveBeenCalledWith( + expect(initAndBind).toHaveBeenCalledTimes(1); + expect(initAndBind).toHaveBeenCalledWith( + BrowserClient, expect.objectContaining({ _metadata: { sdk: { @@ -45,12 +55,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 +70,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..27e250f1569b 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, 'initWithoutDefaultIntegrations'); 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..42914c1b9f4a 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, 'initWithoutDefaultIntegrations').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/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..11dd32a4d26e 100644 --- a/packages/vue/src/sdk.ts +++ b/packages/vue/src/sdk.ts @@ -1,6 +1,7 @@ -import { getDefaultIntegrations, init as browserInit } from '@sentry/browser'; -import type { Client } from '@sentry/core'; -import { applySdkMetadata } from '@sentry/core'; +import type { BrowserOptions } from '@sentry/browser'; +import { BrowserClient, defaultStackParser, getDefaultIntegrations, makeFetchTransport } from '@sentry/browser'; +import type { Client, Integration } from '@sentry/core'; +import { applySdkMetadata, getClientOptions, initAndBind } from '@sentry/core'; import { vueIntegration } from './integration'; import type { Options } from './types'; @@ -8,12 +9,17 @@ import type { Options } from './types'; * Inits the Vue SDK */ export function init(options: Partial> = {}): Client | undefined { - const opts = { - defaultIntegrations: [...getDefaultIntegrations(options), vueIntegration()], - ...options, - }; + const clientOptions = getClientOptions(options, { + integrations: getVueDefaultIntegrations(options), + stackParser: defaultStackParser, + transport: makeFetchTransport, + }); - applySdkMetadata(opts, 'vue'); + applySdkMetadata(clientOptions, 'vue'); - return browserInit(opts); + return initAndBind(BrowserClient, clientOptions); +} + +function getVueDefaultIntegrations(options: BrowserOptions): Integration[] { + return [...getDefaultIntegrations(options), vueIntegration()]; }