Skip to content

Commit 784b485

Browse files
authored
feat(browser): Prevent initialization in browser extensions (#10844)
Prevents initialization inside chrome.* and browser.* extension environments. Also refactored init() in browser because of eslint warning about too much complexity. Fixes #10632
1 parent 60f483e commit 784b485

File tree

7 files changed

+190
-21
lines changed

7 files changed

+190
-21
lines changed
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
import * as Sentry from '@sentry/browser';
2+
3+
window.Sentry = Sentry;
4+
5+
// We mock this here to simulate a Firefox/Safari browser extension
6+
window.browser = { runtime: { id: 'mock-extension-id' } };
7+
8+
Sentry.init({
9+
dsn: 'https://[email protected]/1337',
10+
});
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
import { expect } from '@playwright/test';
2+
import { sentryTest } from '../../../utils/fixtures';
3+
4+
sentryTest(
5+
'should not initialize when inside a Firefox/Safari browser extension',
6+
async ({ getLocalTestUrl, page }) => {
7+
await page.route('https://dsn.ingest.sentry.io/**/*', route => {
8+
return route.fulfill({
9+
status: 200,
10+
contentType: 'application/json',
11+
body: JSON.stringify({ id: 'test-id' }),
12+
});
13+
});
14+
15+
const errorLogs: string[] = [];
16+
17+
page.on('console', message => {
18+
if (message.type() === 'error') errorLogs.push(message.text());
19+
});
20+
21+
const url = await getLocalTestUrl({ testDir: __dirname });
22+
await page.goto(url);
23+
24+
const isInitialized = await page.evaluate(() => {
25+
return !!(window as any).Sentry.isInitialized();
26+
});
27+
28+
expect(isInitialized).toEqual(false);
29+
expect(errorLogs.length).toEqual(1);
30+
expect(errorLogs[0]).toEqual(
31+
'[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',
32+
);
33+
},
34+
);
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
import * as Sentry from '@sentry/browser';
2+
3+
window.Sentry = Sentry;
4+
5+
// We mock this here to simulate a Chrome browser extension
6+
window.chrome = { runtime: { id: 'mock-extension-id' } };
7+
8+
Sentry.init({
9+
dsn: 'https://[email protected]/1337',
10+
});
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
import { expect } from '@playwright/test';
2+
import { sentryTest } from '../../../utils/fixtures';
3+
4+
sentryTest('should not initialize when inside a Chrome browser extension', async ({ getLocalTestUrl, page }) => {
5+
await page.route('https://dsn.ingest.sentry.io/**/*', route => {
6+
return route.fulfill({
7+
status: 200,
8+
contentType: 'application/json',
9+
body: JSON.stringify({ id: 'test-id' }),
10+
});
11+
});
12+
13+
const errorLogs: string[] = [];
14+
15+
page.on('console', message => {
16+
if (message.type() === 'error') errorLogs.push(message.text());
17+
});
18+
19+
const url = await getLocalTestUrl({ testDir: __dirname });
20+
await page.goto(url);
21+
22+
const isInitialized = await page.evaluate(() => {
23+
return !!(window as any).Sentry.isInitialized();
24+
});
25+
26+
expect(isInitialized).toEqual(false);
27+
expect(errorLogs.length).toEqual(1);
28+
expect(errorLogs[0]).toEqual(
29+
'[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',
30+
);
31+
});

packages/astro/test/client/sdk.test.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -78,7 +78,7 @@ describe('Sentry client SDK', () => {
7878
...tracingOptions,
7979
});
8080

81-
const integrationsToInit = browserInit.mock.calls[0][0]?.defaultIntegrations;
81+
const integrationsToInit = browserInit.mock.calls[0][0]?.defaultIntegrations || [];
8282
const browserTracing = getClient<BrowserClient>()?.getIntegrationByName('BrowserTracing');
8383

8484
expect(integrationsToInit).not.toContainEqual(expect.objectContaining({ name: 'BrowserTracing' }));
@@ -93,7 +93,7 @@ describe('Sentry client SDK', () => {
9393
enableTracing: true,
9494
});
9595

96-
const integrationsToInit = browserInit.mock.calls[0][0]?.defaultIntegrations;
96+
const integrationsToInit = browserInit.mock.calls[0][0]?.defaultIntegrations || [];
9797
const browserTracing = getClient<BrowserClient>()?.getIntegrationByName('BrowserTracing');
9898

9999
expect(integrationsToInit).not.toContainEqual(expect.objectContaining({ name: 'BrowserTracing' }));

packages/browser/src/sdk.ts

Lines changed: 45 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import {
1111
import type { DsnLike, Integration, Options, UserFeedback } from '@sentry/types';
1212
import {
1313
addHistoryInstrumentationHandler,
14+
consoleSandbox,
1415
logger,
1516
stackParserFromStackParserOptions,
1617
supportsFetch,
@@ -43,6 +44,40 @@ export function getDefaultIntegrations(_options: Options): Integration[] {
4344
];
4445
}
4546

47+
function applyDefaultOptions(optionsArg: BrowserOptions = {}): BrowserOptions {
48+
const defaultOptions: BrowserOptions = {
49+
defaultIntegrations: getDefaultIntegrations(optionsArg),
50+
release:
51+
typeof __SENTRY_RELEASE__ === 'string' // This allows build tooling to find-and-replace __SENTRY_RELEASE__ to inject a release value
52+
? __SENTRY_RELEASE__
53+
: WINDOW.SENTRY_RELEASE && WINDOW.SENTRY_RELEASE.id // This supports the variable that sentry-webpack-plugin injects
54+
? WINDOW.SENTRY_RELEASE.id
55+
: undefined,
56+
autoSessionTracking: true,
57+
sendClientReports: true,
58+
};
59+
60+
return { ...defaultOptions, ...optionsArg };
61+
}
62+
63+
function shouldShowBrowserExtensionError(): boolean {
64+
const windowWithMaybeChrome = WINDOW as typeof WINDOW & { chrome?: { runtime?: { id?: string } } };
65+
const isInsideChromeExtension =
66+
windowWithMaybeChrome &&
67+
windowWithMaybeChrome.chrome &&
68+
windowWithMaybeChrome.chrome.runtime &&
69+
windowWithMaybeChrome.chrome.runtime.id;
70+
71+
const windowWithMaybeBrowser = WINDOW as typeof WINDOW & { browser?: { runtime?: { id?: string } } };
72+
const isInsideBrowserExtension =
73+
windowWithMaybeBrowser &&
74+
windowWithMaybeBrowser.browser &&
75+
windowWithMaybeBrowser.browser.runtime &&
76+
windowWithMaybeBrowser.browser.runtime.id;
77+
78+
return !!isInsideBrowserExtension || !!isInsideChromeExtension;
79+
}
80+
4681
/**
4782
* A magic string that build tooling can leverage in order to inject a release value into the SDK.
4883
*/
@@ -94,26 +129,17 @@ declare const __SENTRY_RELEASE__: string | undefined;
94129
*
95130
* @see {@link BrowserOptions} for documentation on configuration options.
96131
*/
97-
export function init(options: BrowserOptions = {}): void {
98-
if (options.defaultIntegrations === undefined) {
99-
options.defaultIntegrations = getDefaultIntegrations(options);
100-
}
101-
if (options.release === undefined) {
102-
// This allows build tooling to find-and-replace __SENTRY_RELEASE__ to inject a release value
103-
if (typeof __SENTRY_RELEASE__ === 'string') {
104-
options.release = __SENTRY_RELEASE__;
105-
}
132+
export function init(browserOptions: BrowserOptions = {}): void {
133+
const options = applyDefaultOptions(browserOptions);
106134

107-
// This supports the variable that sentry-webpack-plugin injects
108-
if (WINDOW.SENTRY_RELEASE && WINDOW.SENTRY_RELEASE.id) {
109-
options.release = WINDOW.SENTRY_RELEASE.id;
110-
}
111-
}
112-
if (options.autoSessionTracking === undefined) {
113-
options.autoSessionTracking = true;
114-
}
115-
if (options.sendClientReports === undefined) {
116-
options.sendClientReports = true;
135+
if (shouldShowBrowserExtensionError()) {
136+
consoleSandbox(() => {
137+
// eslint-disable-next-line no-console
138+
console.error(
139+
'[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',
140+
);
141+
});
142+
return;
117143
}
118144

119145
if (DEBUG_BUILD) {

packages/browser/test/unit/sdk.test.ts

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import type { Client, Integration } from '@sentry/types';
44
import { resolvedSyncPromise } from '@sentry/utils';
55

66
import type { BrowserOptions } from '../../src';
7+
import { WINDOW } from '../../src';
78
import { init } from '../../src/sdk';
89

910
const PUBLIC_DSN = 'https://username@domain/123';
@@ -127,4 +128,61 @@ describe('init', () => {
127128
expect(newIntegration.setupOnce as jest.Mock).toHaveBeenCalledTimes(1);
128129
expect(DEFAULT_INTEGRATIONS[1].setupOnce as jest.Mock).toHaveBeenCalledTimes(0);
129130
});
131+
132+
describe('initialization error in browser extension', () => {
133+
const DEFAULT_INTEGRATIONS: Integration[] = [
134+
new MockIntegration('MockIntegration 0.1'),
135+
new MockIntegration('MockIntegration 0.2'),
136+
];
137+
138+
const options = getDefaultBrowserOptions({ dsn: PUBLIC_DSN, defaultIntegrations: DEFAULT_INTEGRATIONS });
139+
140+
afterEach(() => {
141+
Object.defineProperty(WINDOW, 'chrome', { value: undefined, writable: true });
142+
Object.defineProperty(WINDOW, 'browser', { value: undefined, writable: true });
143+
});
144+
145+
it('should log a browser extension error if executed inside a Chrome extension', () => {
146+
const consoleErrorSpy = jest.spyOn(console, 'error').mockImplementation(() => {});
147+
148+
Object.defineProperty(WINDOW, 'chrome', {
149+
value: { runtime: { id: 'mock-extension-id' } },
150+
writable: true,
151+
});
152+
153+
init(options);
154+
155+
expect(consoleErrorSpy).toBeCalledTimes(1);
156+
expect(consoleErrorSpy).toHaveBeenCalledWith(
157+
'[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',
158+
);
159+
160+
consoleErrorSpy.mockRestore();
161+
});
162+
163+
it('should log a browser extension error if executed inside a Firefox/Safari extension', () => {
164+
const consoleErrorSpy = jest.spyOn(console, 'error').mockImplementation(() => {});
165+
166+
Object.defineProperty(WINDOW, 'browser', { value: { runtime: { id: 'mock-extension-id' } }, writable: true });
167+
168+
init(options);
169+
170+
expect(consoleErrorSpy).toBeCalledTimes(1);
171+
expect(consoleErrorSpy).toHaveBeenCalledWith(
172+
'[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',
173+
);
174+
175+
consoleErrorSpy.mockRestore();
176+
});
177+
178+
it('should not log a browser extension error if executed inside regular browser environment', () => {
179+
const consoleErrorSpy = jest.spyOn(console, 'error').mockImplementation(() => {});
180+
181+
init(options);
182+
183+
expect(consoleErrorSpy).toBeCalledTimes(0);
184+
185+
consoleErrorSpy.mockRestore();
186+
});
187+
});
130188
});

0 commit comments

Comments
 (0)