diff --git a/dev-packages/browser-integration-tests/suites/manual-client/skip-init-browser-extension/init.js b/dev-packages/browser-integration-tests/suites/manual-client/skip-init-browser-extension/init.js new file mode 100644 index 000000000000..7494359a0a8a --- /dev/null +++ b/dev-packages/browser-integration-tests/suites/manual-client/skip-init-browser-extension/init.js @@ -0,0 +1,10 @@ +import * as Sentry from '@sentry/browser'; + +window.Sentry = Sentry; + +// We mock this here to simulate a Firefox/Safari browser extension +window.browser = { runtime: { id: 'mock-extension-id' } }; + +Sentry.init({ + dsn: 'https://public@dsn.ingest.sentry.io/1337', +}); diff --git a/dev-packages/browser-integration-tests/suites/manual-client/skip-init-browser-extension/test.ts b/dev-packages/browser-integration-tests/suites/manual-client/skip-init-browser-extension/test.ts new file mode 100644 index 000000000000..41d2da3e9e9d --- /dev/null +++ b/dev-packages/browser-integration-tests/suites/manual-client/skip-init-browser-extension/test.ts @@ -0,0 +1,34 @@ +import { expect } from '@playwright/test'; +import { sentryTest } from '../../../utils/fixtures'; + +sentryTest( + 'should not initialize when inside a Firefox/Safari browser extension', + async ({ getLocalTestUrl, page }) => { + await page.route('https://dsn.ingest.sentry.io/**/*', route => { + return route.fulfill({ + status: 200, + contentType: 'application/json', + body: JSON.stringify({ id: 'test-id' }), + }); + }); + + const errorLogs: string[] = []; + + page.on('console', message => { + if (message.type() === 'error') errorLogs.push(message.text()); + }); + + const url = await getLocalTestUrl({ testDir: __dirname }); + await page.goto(url); + + const isInitialized = await page.evaluate(() => { + return !!(window as any).Sentry.isInitialized(); + }); + + expect(isInitialized).toEqual(false); + expect(errorLogs.length).toEqual(1); + expect(errorLogs[0]).toEqual( + '[Sentry] You cannot run Sentry this way in a browser extension, check: https://docs.sentry.io/platforms/javascript/troubleshooting/#setting-up-sentry-in-shared-environments-eg-browser-extensions', + ); + }, +); diff --git a/dev-packages/browser-integration-tests/suites/manual-client/skip-init-chrome-extension/init.js b/dev-packages/browser-integration-tests/suites/manual-client/skip-init-chrome-extension/init.js new file mode 100644 index 000000000000..6cb3b49ceb53 --- /dev/null +++ b/dev-packages/browser-integration-tests/suites/manual-client/skip-init-chrome-extension/init.js @@ -0,0 +1,10 @@ +import * as Sentry from '@sentry/browser'; + +window.Sentry = Sentry; + +// We mock this here to simulate a Chrome browser extension +window.chrome = { runtime: { id: 'mock-extension-id' } }; + +Sentry.init({ + dsn: 'https://public@dsn.ingest.sentry.io/1337', +}); diff --git a/dev-packages/browser-integration-tests/suites/manual-client/skip-init-chrome-extension/test.ts b/dev-packages/browser-integration-tests/suites/manual-client/skip-init-chrome-extension/test.ts new file mode 100644 index 000000000000..401788b588a9 --- /dev/null +++ b/dev-packages/browser-integration-tests/suites/manual-client/skip-init-chrome-extension/test.ts @@ -0,0 +1,31 @@ +import { expect } from '@playwright/test'; +import { sentryTest } from '../../../utils/fixtures'; + +sentryTest('should not initialize when inside a Chrome browser extension', async ({ getLocalTestUrl, page }) => { + await page.route('https://dsn.ingest.sentry.io/**/*', route => { + return route.fulfill({ + status: 200, + contentType: 'application/json', + body: JSON.stringify({ id: 'test-id' }), + }); + }); + + const errorLogs: string[] = []; + + page.on('console', message => { + if (message.type() === 'error') errorLogs.push(message.text()); + }); + + const url = await getLocalTestUrl({ testDir: __dirname }); + await page.goto(url); + + const isInitialized = await page.evaluate(() => { + return !!(window as any).Sentry.isInitialized(); + }); + + expect(isInitialized).toEqual(false); + expect(errorLogs.length).toEqual(1); + expect(errorLogs[0]).toEqual( + '[Sentry] You cannot run Sentry this way in a browser extension, check: https://docs.sentry.io/platforms/javascript/troubleshooting/#setting-up-sentry-in-shared-environments-eg-browser-extensions', + ); +}); diff --git a/packages/astro/test/client/sdk.test.ts b/packages/astro/test/client/sdk.test.ts index 50f4e3c9e354..a344cd326bb6 100644 --- a/packages/astro/test/client/sdk.test.ts +++ b/packages/astro/test/client/sdk.test.ts @@ -78,7 +78,7 @@ describe('Sentry client SDK', () => { ...tracingOptions, }); - const integrationsToInit = browserInit.mock.calls[0][0]?.defaultIntegrations; + const integrationsToInit = browserInit.mock.calls[0][0]?.defaultIntegrations || []; const browserTracing = getClient()?.getIntegrationByName('BrowserTracing'); expect(integrationsToInit).not.toContainEqual(expect.objectContaining({ name: 'BrowserTracing' })); @@ -93,7 +93,7 @@ describe('Sentry client SDK', () => { enableTracing: true, }); - const integrationsToInit = browserInit.mock.calls[0][0]?.defaultIntegrations; + const integrationsToInit = browserInit.mock.calls[0][0]?.defaultIntegrations || []; const browserTracing = getClient()?.getIntegrationByName('BrowserTracing'); expect(integrationsToInit).not.toContainEqual(expect.objectContaining({ name: 'BrowserTracing' })); diff --git a/packages/browser/src/sdk.ts b/packages/browser/src/sdk.ts index 254276af335c..e747bd533699 100644 --- a/packages/browser/src/sdk.ts +++ b/packages/browser/src/sdk.ts @@ -11,6 +11,7 @@ import { import type { DsnLike, Integration, Options, UserFeedback } from '@sentry/types'; import { addHistoryInstrumentationHandler, + consoleSandbox, logger, stackParserFromStackParserOptions, supportsFetch, @@ -43,6 +44,40 @@ export function getDefaultIntegrations(_options: Options): Integration[] { ]; } +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 && WINDOW.SENTRY_RELEASE.id // This supports the variable that sentry-webpack-plugin injects + ? WINDOW.SENTRY_RELEASE.id + : undefined, + autoSessionTracking: true, + sendClientReports: true, + }; + + return { ...defaultOptions, ...optionsArg }; +} + +function shouldShowBrowserExtensionError(): boolean { + const windowWithMaybeChrome = WINDOW as typeof WINDOW & { chrome?: { runtime?: { id?: string } } }; + const isInsideChromeExtension = + windowWithMaybeChrome && + windowWithMaybeChrome.chrome && + windowWithMaybeChrome.chrome.runtime && + windowWithMaybeChrome.chrome.runtime.id; + + const windowWithMaybeBrowser = WINDOW as typeof WINDOW & { browser?: { runtime?: { id?: string } } }; + const isInsideBrowserExtension = + windowWithMaybeBrowser && + windowWithMaybeBrowser.browser && + windowWithMaybeBrowser.browser.runtime && + windowWithMaybeBrowser.browser.runtime.id; + + return !!isInsideBrowserExtension || !!isInsideChromeExtension; +} + /** * A magic string that build tooling can leverage in order to inject a release value into the SDK. */ @@ -94,26 +129,17 @@ declare const __SENTRY_RELEASE__: string | undefined; * * @see {@link BrowserOptions} for documentation on configuration options. */ -export function init(options: BrowserOptions = {}): void { - if (options.defaultIntegrations === undefined) { - options.defaultIntegrations = getDefaultIntegrations(options); - } - if (options.release === undefined) { - // This allows build tooling to find-and-replace __SENTRY_RELEASE__ to inject a release value - if (typeof __SENTRY_RELEASE__ === 'string') { - options.release = __SENTRY_RELEASE__; - } +export function init(browserOptions: BrowserOptions = {}): void { + const options = applyDefaultOptions(browserOptions); - // This supports the variable that sentry-webpack-plugin injects - if (WINDOW.SENTRY_RELEASE && WINDOW.SENTRY_RELEASE.id) { - options.release = WINDOW.SENTRY_RELEASE.id; - } - } - if (options.autoSessionTracking === undefined) { - options.autoSessionTracking = true; - } - if (options.sendClientReports === undefined) { - options.sendClientReports = true; + if (shouldShowBrowserExtensionError()) { + consoleSandbox(() => { + // eslint-disable-next-line no-console + console.error( + '[Sentry] You cannot run Sentry this way in a browser extension, check: https://docs.sentry.io/platforms/javascript/troubleshooting/#setting-up-sentry-in-shared-environments-eg-browser-extensions', + ); + }); + return; } if (DEBUG_BUILD) { diff --git a/packages/browser/test/unit/sdk.test.ts b/packages/browser/test/unit/sdk.test.ts index 75cb238d35bb..f4a04d088135 100644 --- a/packages/browser/test/unit/sdk.test.ts +++ b/packages/browser/test/unit/sdk.test.ts @@ -4,6 +4,7 @@ import type { Client, Integration } from '@sentry/types'; import { resolvedSyncPromise } from '@sentry/utils'; import type { BrowserOptions } from '../../src'; +import { WINDOW } from '../../src'; import { init } from '../../src/sdk'; const PUBLIC_DSN = 'https://username@domain/123'; @@ -127,4 +128,61 @@ describe('init', () => { expect(newIntegration.setupOnce as jest.Mock).toHaveBeenCalledTimes(1); expect(DEFAULT_INTEGRATIONS[1].setupOnce as jest.Mock).toHaveBeenCalledTimes(0); }); + + describe('initialization error in browser extension', () => { + const DEFAULT_INTEGRATIONS: Integration[] = [ + new MockIntegration('MockIntegration 0.1'), + new MockIntegration('MockIntegration 0.2'), + ]; + + const options = getDefaultBrowserOptions({ dsn: PUBLIC_DSN, defaultIntegrations: DEFAULT_INTEGRATIONS }); + + afterEach(() => { + Object.defineProperty(WINDOW, 'chrome', { value: undefined, writable: true }); + Object.defineProperty(WINDOW, 'browser', { value: undefined, writable: true }); + }); + + it('should log a browser extension error if executed inside a Chrome extension', () => { + const consoleErrorSpy = jest.spyOn(console, 'error').mockImplementation(() => {}); + + Object.defineProperty(WINDOW, 'chrome', { + value: { runtime: { id: 'mock-extension-id' } }, + writable: true, + }); + + init(options); + + expect(consoleErrorSpy).toBeCalledTimes(1); + expect(consoleErrorSpy).toHaveBeenCalledWith( + '[Sentry] You cannot run Sentry this way in a browser extension, check: https://docs.sentry.io/platforms/javascript/troubleshooting/#setting-up-sentry-in-shared-environments-eg-browser-extensions', + ); + + consoleErrorSpy.mockRestore(); + }); + + it('should log a browser extension error if executed inside a Firefox/Safari extension', () => { + const consoleErrorSpy = jest.spyOn(console, 'error').mockImplementation(() => {}); + + Object.defineProperty(WINDOW, 'browser', { value: { runtime: { id: 'mock-extension-id' } }, writable: true }); + + init(options); + + expect(consoleErrorSpy).toBeCalledTimes(1); + expect(consoleErrorSpy).toHaveBeenCalledWith( + '[Sentry] You cannot run Sentry this way in a browser extension, check: https://docs.sentry.io/platforms/javascript/troubleshooting/#setting-up-sentry-in-shared-environments-eg-browser-extensions', + ); + + consoleErrorSpy.mockRestore(); + }); + + it('should not log a browser extension error if executed inside regular browser environment', () => { + const consoleErrorSpy = jest.spyOn(console, 'error').mockImplementation(() => {}); + + init(options); + + expect(consoleErrorSpy).toBeCalledTimes(0); + + consoleErrorSpy.mockRestore(); + }); + }); });