Skip to content
Open
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
11 changes: 1 addition & 10 deletions packages/browser/src/eventbuilder.ts
Original file line number Diff line number Diff line change
Expand Up @@ -401,14 +401,5 @@ function getObjectClassName(obj: unknown): string | undefined | void {

/** If a plain object has a property that is an `Error`, return this error. */
function getErrorPropertyFromObject(obj: Record<string, unknown>): Error | undefined {
for (const prop in obj) {
if (Object.prototype.hasOwnProperty.call(obj, prop)) {
const value = obj[prop];
if (value instanceof Error) {
return value;
}
}
}

return undefined;
return Object.values(obj).find((v): v is Error => v instanceof Error);
}
17 changes: 9 additions & 8 deletions packages/browser/src/helpers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -104,8 +104,9 @@ export function wrap<T extends WrappableFunction, NonFunction>(
}

// Wrap the function itself
// It is important that `sentryWrapped` is not an arrow function to preserve the context of `this`
const sentryWrapped = function (this: unknown, ...args: unknown[]): unknown {
// It is important that `sW` is not an arrow function to preserve the context of `this`
// The name `sW` is matched by the frame stripping regex in stacktrace.ts
const sW = function (this: unknown, ...args: unknown[]): unknown {
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Wrapper rename breaks minified frame stripping

High Severity

Renaming the wrapper function to sW changes the stack-frame marker, but terser still only reserves sentryWrapped. In minified CDN builds, sW can be mangled, so stripSentryFramesAndReverse no longer recognizes and removes the internal wrapper frame.

Additional Locations (1)
Fix in Cursor Fix in Web

try {
// Also wrap arguments that are themselves functions
const wrappedArguments = args.map(arg => wrap(arg, options));
Expand Down Expand Up @@ -145,7 +146,7 @@ export function wrap<T extends WrappableFunction, NonFunction>(
try {
for (const property in fn) {
if (Object.prototype.hasOwnProperty.call(fn, property)) {
sentryWrapped[property as keyof T] = fn[property as keyof T];
sW[property as keyof T] = fn[property as keyof T];
}
}
} catch {
Expand All @@ -155,16 +156,16 @@ export function wrap<T extends WrappableFunction, NonFunction>(

// Signal that this function has been wrapped/filled already
// for both debugging and to prevent it to being wrapped/filled twice
markFunctionWrapped(sentryWrapped, fn);
markFunctionWrapped(sW, fn);

addNonEnumerableProperty(fn, '__sentry_wrapped__', sentryWrapped);
addNonEnumerableProperty(fn, '__sentry_wrapped__', sW);

// Restore original function name (not all browsers allow that)
try {
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
const descriptor = Object.getOwnPropertyDescriptor(sentryWrapped, 'name')!;
const descriptor = Object.getOwnPropertyDescriptor(sW, 'name')!;
if (descriptor.configurable) {
Object.defineProperty(sentryWrapped, 'name', {
Object.defineProperty(sW, 'name', {
get(): string {
return fn.name;
},
Expand All @@ -175,7 +176,7 @@ export function wrap<T extends WrappableFunction, NonFunction>(
// to save some bytes we simply try-catch this
}

return sentryWrapped;
return sW;
}

/**
Expand Down
12 changes: 1 addition & 11 deletions packages/browser/src/integrations/breadcrumbs.ts
Original file line number Diff line number Diff line change
Expand Up @@ -287,13 +287,7 @@ function _getFetchBreadcrumbHandler(client: Client): (handlerData: HandlerDataFe
return;
}

const breadcrumbData: FetchBreadcrumbData = {
method: handlerData.fetchData.method,
url: handlerData.fetchData.url,
};

if (handlerData.error) {
const data: FetchBreadcrumbData = handlerData.fetchData;
const hint: FetchBreadcrumbHint = {
data: handlerData.error,
input: handlerData.args,
Expand All @@ -303,7 +297,7 @@ function _getFetchBreadcrumbHandler(client: Client): (handlerData: HandlerDataFe

const breadcrumb = {
category: 'fetch',
data,
data: handlerData.fetchData,
level: 'error',
type: 'http',
} satisfies Breadcrumb;
Expand All @@ -318,10 +312,6 @@ function _getFetchBreadcrumbHandler(client: Client): (handlerData: HandlerDataFe
status_code: response?.status,
};

breadcrumbData.request_body_size = handlerData.fetchData.request_body_size;
breadcrumbData.response_body_size = handlerData.fetchData.response_body_size;
breadcrumbData.status_code = response?.status;

const hint: FetchBreadcrumbHint = {
input: handlerData.args,
response,
Expand Down
38 changes: 5 additions & 33 deletions packages/browser/src/integrations/browserapierrors.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,39 +2,11 @@ import type { IntegrationFn, WrappedFunction } from '@sentry/core';
import { defineIntegration, fill, getFunctionName, getOriginalFunction } from '@sentry/core';
import { WINDOW, wrap } from '../helpers';

const DEFAULT_EVENT_TARGET = [
'EventTarget',
'Window',
'Node',
'ApplicationCache',
'AudioTrackList',
'BroadcastChannel',
'ChannelMergerNode',
'CryptoOperation',
'EventSource',
'FileReader',
'HTMLUnknownElement',
'IDBDatabase',
'IDBRequest',
'IDBTransaction',
'KeyOperation',
'MediaController',
'MessagePort',
'ModalWindow',
'Notification',
'SVGElementInstance',
'Screen',
'SharedWorker',
'TextTrack',
'TextTrackCue',
'TextTrackList',
'WebSocket',
'WebSocketWorker',
'Worker',
'XMLHttpRequest',
'XMLHttpRequestEventTarget',
'XMLHttpRequestUpload',
];
// Using a comma-separated string and split for smaller bundle size vs an array literal
const DEFAULT_EVENT_TARGET =
'EventTarget,Window,Node,ApplicationCache,AudioTrackList,BroadcastChannel,ChannelMergerNode,CryptoOperation,EventSource,FileReader,HTMLUnknownElement,IDBDatabase,IDBRequest,IDBTransaction,KeyOperation,MediaController,MessagePort,ModalWindow,Notification,SVGElementInstance,Screen,SharedWorker,TextTrack,TextTrackCue,TextTrackList,WebSocket,WebSocketWorker,Worker,XMLHttpRequest,XMLHttpRequestEventTarget,XMLHttpRequestUpload'.split(
',',
);

const INTEGRATION_NAME = 'BrowserApiErrors';

Expand Down
16 changes: 3 additions & 13 deletions packages/browser/src/integrations/globalhandlers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -162,29 +162,19 @@ function _enhanceEventWithInitialFrame(
line: number | undefined,
column: number | undefined,
): Event {
// event.exception
const e = (event.exception = event.exception || {});
// event.exception.values
const ev = (e.values = e.values || []);
// event.exception.values[0]
const ev0 = (ev[0] = ev[0] || {});
// event.exception.values[0].stacktrace
const ev0s = (ev0.stacktrace = ev0.stacktrace || {});
// event.exception.values[0].stacktrace.frames
const ev0sf = (ev0s.frames = ev0s.frames || []);

const colno = column;
const lineno = line;
const filename = getFilenameFromUrl(url) ?? getLocationHref();

// event.exception.values[0].stacktrace.frames
if (ev0sf.length === 0) {
ev0sf.push({
colno,
filename,
colno: column,
filename: getFilenameFromUrl(url) ?? getLocationHref(),
function: UNKNOWN_FUNCTION,
in_app: true,
lineno,
lineno: line,
});
}

Expand Down
4 changes: 2 additions & 2 deletions packages/browser/src/utils/detectBrowserExtension.ts
Original file line number Diff line number Diff line change
Expand Up @@ -55,11 +55,11 @@ function _isEmbeddedBrowserExtension(): boolean {
}

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}://`));
WINDOW === WINDOW.top &&
/^(?:chrome-extension|moz-extension|ms-browser-extension|safari-web-extension):\/\//.test(href);

return !isDedicatedExtensionPage;
}
66 changes: 42 additions & 24 deletions packages/browser/src/utils/lazyLoadIntegration.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,44 +3,62 @@ import { getClient, SDK_VERSION } from '@sentry/core';
import type { BrowserClient } from '../client';
import { WINDOW } from '../helpers';

// This is a map of integration function method to bundle file name.
const LazyLoadableIntegrations = {
replayIntegration: 'replay',
// Integration names that can be lazy-loaded from the CDN.
// Bundle file names are derived: strip 'Integration' suffix, lowercase.
// Exceptions (hyphenated bundle names) are listed separately.
// Using comma-separated string + split for smaller bundle size.
const LAZY_LOADABLE_NAMES =
'replayIntegration,replayCanvasIntegration,feedbackIntegration,feedbackModalIntegration,feedbackScreenshotIntegration,captureConsoleIntegration,contextLinesIntegration,linkedErrorsIntegration,dedupeIntegration,extraErrorDataIntegration,graphqlClientIntegration,httpClientIntegration,reportingObserverIntegration,rewriteFramesIntegration,browserProfilingIntegration,moduleMetadataIntegration,instrumentAnthropicAiClient,instrumentOpenAiClient,instrumentGoogleGenAIClient,instrumentLangGraph,createLangChainCallbackHandler'.split(
',',
);

// Bundle names that don't follow the simple lowercase derivation pattern
const HYPHENATED_BUNDLES: Record<string, string> = {
replayCanvasIntegration: 'replay-canvas',
feedbackIntegration: 'feedback',
feedbackModalIntegration: 'feedback-modal',
feedbackScreenshotIntegration: 'feedback-screenshot',
captureConsoleIntegration: 'captureconsole',
contextLinesIntegration: 'contextlines',
linkedErrorsIntegration: 'linkederrors',
dedupeIntegration: 'dedupe',
extraErrorDataIntegration: 'extraerrordata',
graphqlClientIntegration: 'graphqlclient',
httpClientIntegration: 'httpclient',
reportingObserverIntegration: 'reportingobserver',
rewriteFramesIntegration: 'rewriteframes',
browserProfilingIntegration: 'browserprofiling',
moduleMetadataIntegration: 'modulemetadata',
instrumentAnthropicAiClient: 'instrumentanthropicaiclient',
instrumentOpenAiClient: 'instrumentopenaiclient',
instrumentGoogleGenAIClient: 'instrumentgooglegenaiclient',
instrumentLangGraph: 'instrumentlanggraph',
createLangChainCallbackHandler: 'createlangchaincallbackhandler',
} as const;
};

type LazyLoadableIntegrationName =
| 'replayIntegration'
| 'replayCanvasIntegration'
| 'feedbackIntegration'
| 'feedbackModalIntegration'
| 'feedbackScreenshotIntegration'
| 'captureConsoleIntegration'
| 'contextLinesIntegration'
| 'linkedErrorsIntegration'
| 'dedupeIntegration'
| 'extraErrorDataIntegration'
| 'graphqlClientIntegration'
| 'httpClientIntegration'
| 'reportingObserverIntegration'
| 'rewriteFramesIntegration'
| 'browserProfilingIntegration'
| 'moduleMetadataIntegration'
| 'instrumentAnthropicAiClient'
| 'instrumentOpenAiClient'
| 'instrumentGoogleGenAIClient'
| 'instrumentLangGraph'
| 'createLangChainCallbackHandler';

function getBundleName(name: string): string {
return HYPHENATED_BUNDLES[name] || name.replace('Integration', '').toLowerCase();
}

const WindowWithMaybeIntegration = WINDOW as {
Sentry?: Partial<Record<keyof typeof LazyLoadableIntegrations, IntegrationFn>>;
Sentry?: Partial<Record<LazyLoadableIntegrationName, IntegrationFn>>;
};

/**
* Lazy load an integration from the CDN.
* Rejects if the integration cannot be loaded.
*/
export async function lazyLoadIntegration(
name: keyof typeof LazyLoadableIntegrations,
name: LazyLoadableIntegrationName,
scriptNonce?: string,
): Promise<IntegrationFn> {
const bundle = LazyLoadableIntegrations[name];
const bundle = LAZY_LOADABLE_NAMES.includes(name) ? getBundleName(name) : undefined;

// `window.Sentry` is only set when using a CDN bundle, but this method can also be used via the NPM package
const sentryOnWindow = (WindowWithMaybeIntegration.Sentry = WindowWithMaybeIntegration.Sentry || {});
Expand Down
2 changes: 1 addition & 1 deletion packages/core/src/utils/stacktrace.ts
Original file line number Diff line number Diff line change
Expand Up @@ -88,7 +88,7 @@ export function stripSentryFramesAndReverse(stack: ReadonlyArray<StackFrame>): S
const localStack = Array.from(stack);

// If stack starts with one of our API calls, remove it (starts, meaning it's the top of the stack - aka last call)
if (/sentryWrapped/.test(getLastStackFrame(localStack).function || '')) {
if (/sentryWrapped|^sW$/.test(getLastStackFrame(localStack).function || '')) {
localStack.pop();
}

Expand Down
Loading