Skip to content

feat(browser): Disable client when browser extension is detected in init() #16354

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 3 commits into from
May 26, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,12 @@ sentryTest(
return !!(window as any).Sentry.isInitialized();
});

expect(isInitialized).toEqual(false);
const isEnabled = await page.evaluate(() => {
return !!(window as any).Sentry.getClient()?.getOptions().enabled;
});

expect(isInitialized).toEqual(true);
expect(isEnabled).toEqual(false);

if (hasDebugLogs()) {
expect(errorLogs.length).toEqual(1);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,12 @@ sentryTest('should not initialize when inside a Chrome browser extension', async
return !!(window as any).Sentry.isInitialized();
});

expect(isInitialized).toEqual(false);
const isEnabled = await page.evaluate(() => {
return !!(window as any).Sentry.getClient()?.getOptions().enabled;
});

expect(isInitialized).toEqual(true);
expect(isEnabled).toEqual(false);

if (hasDebugLogs()) {
expect(errorLogs.length).toEqual(1);
Expand Down
40 changes: 27 additions & 13 deletions packages/browser/src/client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,15 +21,24 @@ import { eventFromException, eventFromMessage } from './eventbuilder';
import { WINDOW } from './helpers';
import type { BrowserTransportOptions } from './transports/types';

/**
* A magic string that build tooling can leverage in order to inject a release value into the SDK.
*/
declare const __SENTRY_RELEASE__: string | undefined;

const DEFAULT_FLUSH_INTERVAL = 5000;

type BrowserSpecificOptions = BrowserClientReplayOptions &
BrowserClientProfilingOptions & {
/** 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<BrowserTransportOptions> &
BrowserClientReplayOptions &
BrowserClientProfilingOptions & {
BrowserSpecificOptions & {
/**
* Important: Only set this option if you know what you are doing!
*
Expand All @@ -54,12 +63,7 @@ export type BrowserOptions = Options<BrowserTransportOptions> &
* Configuration options for the Sentry Browser SDK Client class
* @see BrowserClient for more information.
*/
export type BrowserClientOptions = ClientOptions<BrowserTransportOptions> &
BrowserClientReplayOptions &
BrowserClientProfilingOptions & {
/** If configured, this URL will be used as base URL for lazy loading integration. */
cdnBaseUrl?: string;
};
export type BrowserClientOptions = ClientOptions<BrowserTransportOptions> & BrowserSpecificOptions;

/**
* The Sentry Browser SDK Client.
Expand All @@ -75,11 +79,7 @@ export class BrowserClient extends Client<BrowserClientOptions> {
* @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);

Expand Down Expand Up @@ -155,3 +155,17 @@ export class BrowserClient extends Client<BrowserClientOptions> {
return super._prepareEvent(event, hint, currentScope, isolationScope);
}
}

/** Exported only for tests. */
export function applyDefaultOptions<T extends Partial<BrowserClientOptions>>(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,
};
}
116 changes: 10 additions & 106 deletions packages/browser/src/sdk.ts
Original file line number Diff line number Diff line change
@@ -1,18 +1,14 @@
import type { Client, Integration, Options } from '@sentry/core';
import {
consoleSandbox,
dedupeIntegration,
functionToStringIntegration,
getIntegrationsToSetup,
getLocationHref,
inboundFiltersIntegration,
initAndBind,
stackParserFromStackParserOptions,
} from '@sentry/core';
import type { BrowserClientOptions, 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';
Expand All @@ -21,22 +17,7 @@ import { httpContextIntegration } from './integrations/httpcontext';
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;
import { checkAndWarnIfIsEmbeddedBrowserExtension } from './utils/detectBrowserExtension';

/** Get the default integrations for the browser SDK. */
export function getDefaultIntegrations(_options: Options): Integration[] {
Expand All @@ -59,40 +40,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<T extends object>(obj: T): Partial<T> {
const mutatetedObj: Partial<T> = {};

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.
*
Expand Down Expand Up @@ -139,19 +86,21 @@ function dropTopLevelUndefinedKeys<T extends object>(obj: T): Partial<T> {
*
* @see {@link BrowserOptions} for documentation on configuration options.
*/
export function init(browserOptions: BrowserOptions = {}): Client | undefined {
if (!browserOptions.skipBrowserExtensionCheck && _checkForBrowserExtension()) {
return;
}
export function init(options: BrowserOptions = {}): Client | undefined {
const shouldDisableBecauseIsBrowserExtenstion =
!options.skipBrowserExtensionCheck && checkAndWarnIfIsEmbeddedBrowserExtension();

const options = applyDefaultOptions(browserOptions);
const clientOptions: BrowserClientOptions = {
...options,
enabled: shouldDisableBecauseIsBrowserExtenstion ? false : options.enabled,
stackParser: stackParserFromStackParserOptions(options.stackParser || defaultStackParser),
integrations: getIntegrationsToSetup(options),
integrations: getIntegrationsToSetup({
integrations: options.integrations,
defaultIntegrations:
options.defaultIntegrations == null ? getDefaultIntegrations(options) : options.defaultIntegrations,
}),
transport: options.transport || makeFetchTransport,
};

return initAndBind(BrowserClient, clientOptions);
}

Expand All @@ -170,48 +119,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;
}
}
65 changes: 65 additions & 0 deletions packages/browser/src/utils/detectBrowserExtension.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
import { consoleSandbox, getLocationHref } from '@sentry/core';
import { DEBUG_BUILD } from '../debug-build';
import { WINDOW } from '../helpers';

type ExtensionRuntime = {
runtime?: {
id?: string;
};
};
type ExtensionProperties = {
chrome?: ExtensionRuntime;
browser?: ExtensionRuntime;
nw?: unknown;
};

/**
* 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.
*/
export function checkAndWarnIfIsEmbeddedBrowserExtension(): boolean {
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;
}

return false;
}

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;
}
Loading
Loading