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
5 changes: 5 additions & 0 deletions packages/browser/src/helpers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -106,6 +106,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 {
// Track depth on GLOBAL_OBJ so the thirdPartyErrorFilterIntegration (in @sentry/core) can detect
// that processEvent is running inside a sentryWrapped call, even with minified/bundled code.
GLOBAL_OBJ._sentryWrappedDepth = (GLOBAL_OBJ._sentryWrappedDepth || 0) + 1;
try {
// Also wrap arguments that are themselves functions
const wrappedArguments = args.map(arg => wrap(arg, options));
Expand Down Expand Up @@ -138,6 +141,8 @@ export function wrap<T extends WrappableFunction, NonFunction>(
});

throw ex;
} finally {
GLOBAL_OBJ._sentryWrappedDepth = (GLOBAL_OBJ._sentryWrappedDepth || 0) - 1;
}
Comment thread
chargome marked this conversation as resolved.
} as unknown as WrappedFunction<T>;

Expand Down
55 changes: 43 additions & 12 deletions packages/core/src/integrations/third-party-errors-filter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import type { Event } from '../types-hoist/event';
import type { StackFrame } from '../types-hoist/stackframe';
import { forEachEnvelopeItem } from '../utils/envelope';
import { getFramesFromEvent } from '../utils/stacktrace';
import { GLOBAL_OBJ } from '../utils/worldwide';

interface Options {
/**
Expand Down Expand Up @@ -74,8 +75,27 @@ export const thirdPartyErrorFilterIntegration = defineIntegration((options: Opti
});
},

preprocessEvent(event) {
// Snapshot the depth counter onto the event before any event processors run.
// This is necessary because async event processors could cause the finally block
// in sentryWrapped to decrement the counter before processEvent reads it.
if (options.ignoreSentryInternalFrames && (GLOBAL_OBJ._sentryWrappedDepth ?? 0) > 0) {
event.sdkProcessingMetadata = {
...event.sdkProcessingMetadata,
insideSentryWrapped: true,
};
}
},

processEvent(event) {
const frameKeys = getBundleKeysForAllFramesWithFilenames(event, options.ignoreSentryInternalFrames);
const insideSentryWrapped = options.ignoreSentryInternalFrames
? event.sdkProcessingMetadata?.insideSentryWrapped === true
: false;
const frameKeys = getBundleKeysForAllFramesWithFilenames(
event,
options.ignoreSentryInternalFrames,
insideSentryWrapped,
);

if (frameKeys) {
const arrayMethod =
Expand Down Expand Up @@ -106,20 +126,30 @@ export const thirdPartyErrorFilterIntegration = defineIntegration((options: Opti
};
});

/**
* Checks if a stack frame is a Sentry internal frame by strictly matching:
* 1. The frame must be the last frame in the stack
* 2. The filename must indicate the internal helpers file
* 3. The context_line must contain the exact pattern "fn.apply(this, wrappedArguments)"
* 4. The comment pattern "Attempt to invoke user-land function" must be present in pre_context
*
*/
function isSentryInternalFrame(frame: StackFrame, frameIndex: number): boolean {
/** Checks if a frame is Sentry-internal via runtime depth, function name, or source patterns. */
function isSentryInternalFrame(frame: StackFrame, frameIndex: number, insideSentryWrapped: boolean): boolean {
// Only match the last frame (index 0 in reversed stack)
if (frameIndex !== 0 || !frame.context_line || !frame.filename) {
if (frameIndex !== 0) {
return false;
}

// When processEvent runs inside a sentryWrapped call, the outermost frame is always
// the sentryWrapped function. This works regardless of minification/bundling because
// it's a runtime check, not a source pattern match.
if (insideSentryWrapped) {
return true;
}
Comment on lines +136 to +141
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Bug: In non-minified builds, isSentryInternalFrame incorrectly flags user code as internal when _sentryWrappedDepth > 0, causing valid events from wrapped functions to be silently dropped.
Severity: HIGH

Suggested Fix

Remove the unconditional check in isSentryInternalFrame that treats the frame at index 0 as internal when insideSentryWrapped is true. The logic relies on the assumption that the sentryWrapped frame is always present, but it is stripped out in non-minified builds before this filter runs. The check should be removed to prevent misclassifying user code as internal.

Prompt for AI Agent
Review the code at the location below. A potential bug has been identified by an AI
agent. Verify if this is a real issue. If it is, propose a fix; if not, explain why it's
not valid.

Location: packages/core/src/integrations/third-party-errors-filter.ts#L136-L141

Potential issue: In non-minified builds, the `isSentryInternalFrame` function
incorrectly assumes the first stack frame is always `sentryWrapped` when an error occurs
inside a wrapped function. However, a preceding function, `stripSentryFramesAndReverse`,
removes the `sentryWrapped` frame by its literal name. This causes the actual user code
frame to be misclassified as an internal Sentry frame and subsequently filtered out.
When all frames are filtered out, an `every()` check on the empty frame list becomes
vacuously true, causing the entire event to be silently dropped, even though it
originates from first-party code.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Chained errors cause wrong frame filtered as internal

Medium Severity

When insideSentryWrapped is true, isSentryInternalFrame unconditionally returns true for frameIndex === 0. However, getFramesFromEvent concatenates frames from all exception.values entries in order — for chained errors (with cause), values[0] is the root cause, so index 0 in the combined array is the outermost frame of the root cause, not the sentryWrapped frame. This causes a legitimate user/library frame to be incorrectly filtered as Sentry-internal, potentially leading to false error drops or incorrect third-party tagging.

Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit 1ae6a53. Configure here.

Copy link
Copy Markdown
Member

@logaretm logaretm May 11, 2026

Choose a reason for hiding this comment

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

I think this might be a legit issue, maybe we can narrow it down with something like:

const maybeInsideSentryWrapped = insideSentryWrapped && event.exception?.values?.length === 1;

and then gate it further to only the minified-shaped case:

function isLikelyMinifiedSentryWrappedFrame(frame: StackFrame): boolean {
  return !frame.context_line && !frame.pre_context && !!frame.function && frame.function.length <= 2;
}

WDYT?


// Match by function name (works when function names survive bundling but source patterns don't)
if (frame.function === 'sentryWrapped') {
return true;
}

if (!frame.context_line || !frame.filename) {
return false;
}

// Match by source code patterns (works in development / unbundled builds)
if (
!frame.filename.includes('sentry') ||
!frame.filename.includes('helpers') || // Filename would look something like this: 'node_modules/@sentry/browser/build/npm/esm/helpers.js'
Expand All @@ -144,6 +174,7 @@ function isSentryInternalFrame(frame: StackFrame, frameIndex: number): boolean {
function getBundleKeysForAllFramesWithFilenames(
event: Event,
ignoreSentryInternalFrames?: boolean,
insideSentryWrapped?: boolean,
): string[][] | undefined {
const frames = getFramesFromEvent(event);

Expand All @@ -163,7 +194,7 @@ function getBundleKeysForAllFramesWithFilenames(
return false;
}
// Optionally ignore Sentry internal frames
return !ignoreSentryInternalFrames || !isSentryInternalFrame(frame, index);
return !ignoreSentryInternalFrames || !isSentryInternalFrame(frame, index, !!insideSentryWrapped);
})
.map(frame => {
if (!frame.module_metadata) {
Expand Down
1 change: 1 addition & 0 deletions packages/core/src/utils/worldwide.ts
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,7 @@ export type InternalGlobal = {
*/
_sentryModuleMetadata?: Record<string, any>;
_sentryEsmLoaderHookRegistered?: boolean;
_sentryWrappedDepth?: number;
} & Carrier;

/** Get's the global object for the current JavaScript runtime */
Expand Down
181 changes: 177 additions & 4 deletions packages/core/test/lib/integrations/third-party-errors-filter.test.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { beforeEach, describe, expect, it } from 'vitest';
import { afterEach, beforeEach, describe, expect, it } from 'vitest';
import type { Client } from '../../../src/client';
import { thirdPartyErrorFilterIntegration } from '../../../src/integrations/third-party-errors-filter';
import { addMetadataToStackFrames } from '../../../src/metadata';
Expand Down Expand Up @@ -626,7 +626,7 @@ describe('ThirdPartyErrorFilter', () => {
expect(result).toBeDefined();
});

it('does not match when filename does not contain both helpers and sentry', async () => {
it('does not match when filename does not contain both helpers and sentry and function name is not sentryWrapped', async () => {
const eventWithWrongFilename: Event = {
exception: {
values: [
Expand All @@ -636,7 +636,7 @@ describe('ThirdPartyErrorFilter', () => {
{
colno: 2,
filename: 'some-helpers.js',
function: 'sentryWrapped',
function: 'someFunction',
lineno: 117,
context_line: ' return fn.apply(this, wrappedArguments);',
pre_context: [
Expand Down Expand Up @@ -667,7 +667,180 @@ describe('ThirdPartyErrorFilter', () => {

const event = clone(eventWithWrongFilename);
const result = await integration.processEvent?.(event, {}, MOCK_CLIENT);
// Should not drop because filename doesn't contain "sentry"
// Should not drop because filename doesn't contain "sentry" and function name is not "sentryWrapped"
expect(result).toBeDefined();
});
});

describe('minified/bundled code detection', () => {
afterEach(() => {
GLOBAL_OBJ._sentryWrappedDepth = 0;
});

it('detects Sentry internal frame when preprocessEvent snapshots depth inside a sentryWrapped call', async () => {
const eventWithMinifiedSentryFrame: Event = {
exception: {
values: [
{
stacktrace: {
frames: [
{
colno: 12345,
filename: 'https://example.com/assets/app-abc123.js',
function: 'a',
lineno: 1,
},
{
colno: 1,
filename: 'other-file.js',
function: 'function',
lineno: 1,
},
],
},
type: 'Error',
value: 'Third party error',
},
],
},
};

const integration = thirdPartyErrorFilterIntegration({
behaviour: 'drop-error-if-exclusively-contains-third-party-frames',
filterKeys: ['some-key'],
ignoreSentryInternalFrames: true,
});

// Simulate being inside a sentryWrapped call
GLOBAL_OBJ._sentryWrappedDepth = 1;
const event = clone(eventWithMinifiedSentryFrame);
// preprocessEvent snapshots the depth onto the event
integration.preprocessEvent?.(event, {}, MOCK_CLIENT);
// Even if depth resets before processEvent (async processor scenario), the snapshot survives
GLOBAL_OBJ._sentryWrappedDepth = 0;
const result = await integration.processEvent?.(event, {}, MOCK_CLIENT);
expect(result).toBe(null);
});

it('detects Sentry internal frame by function name sentryWrapped even without source patterns', async () => {
const eventWithFunctionName: Event = {
exception: {
values: [
{
stacktrace: {
frames: [
{
colno: 12345,
filename: 'https://example.com/assets/app-abc123.js',
function: 'sentryWrapped',
lineno: 1,
},
{
colno: 1,
filename: 'other-file.js',
function: 'function',
lineno: 1,
},
],
},
type: 'Error',
value: 'Third party error',
},
],
},
};

const integration = thirdPartyErrorFilterIntegration({
behaviour: 'drop-error-if-exclusively-contains-third-party-frames',
filterKeys: ['some-key'],
ignoreSentryInternalFrames: true,
});

const event = clone(eventWithFunctionName);
const result = await integration.processEvent?.(event, {}, MOCK_CLIENT);
expect(result).toBe(null);
});

it('does not detect minified frame as Sentry internal when not inside sentryWrapped and function name is mangled', async () => {
const eventWithMinifiedFrame: Event = {
exception: {
values: [
{
stacktrace: {
frames: [
{
colno: 12345,
filename: 'https://example.com/assets/app-abc123.js',
function: 'a',
lineno: 1,
},
{
colno: 1,
filename: 'other-file.js',
function: 'function',
lineno: 1,
},
],
},
type: 'Error',
value: 'Third party error',
},
],
},
};

const integration = thirdPartyErrorFilterIntegration({
behaviour: 'drop-error-if-exclusively-contains-third-party-frames',
filterKeys: ['some-key'],
ignoreSentryInternalFrames: true,
});

GLOBAL_OBJ._sentryWrappedDepth = 0;
const event = clone(eventWithMinifiedFrame);
integration.preprocessEvent?.(event, {}, MOCK_CLIENT);
const result = await integration.processEvent?.(event, {}, MOCK_CLIENT);
expect(result).toBeDefined();
});

it('does not use sentryWrappedDepth when ignoreSentryInternalFrames is false', async () => {
const eventWithMinifiedSentryFrame: Event = {
exception: {
values: [
{
stacktrace: {
frames: [
{
colno: 12345,
filename: 'https://example.com/assets/app-abc123.js',
function: 'a',
lineno: 1,
},
{
colno: 1,
filename: 'other-file.js',
function: 'function',
lineno: 1,
},
],
},
type: 'Error',
value: 'Third party error',
},
],
},
};

const integration = thirdPartyErrorFilterIntegration({
behaviour: 'drop-error-if-exclusively-contains-third-party-frames',
filterKeys: ['some-key'],
ignoreSentryInternalFrames: false,
});

GLOBAL_OBJ._sentryWrappedDepth = 1;
const event = clone(eventWithMinifiedSentryFrame);
integration.preprocessEvent?.(event, {}, MOCK_CLIENT);
GLOBAL_OBJ._sentryWrappedDepth = 0;
const result = await integration.processEvent?.(event, {}, MOCK_CLIENT);
expect(result).toBeDefined();
});
});
Expand Down
Loading