Skip to content

Commit dd41c62

Browse files
timfishlforst
authored andcommitted
feat: Add thirdPartyErrorFilterIntegration (#12267)
Co-authored-by: Luca Forstner <luca.forstner@sentry.io>
1 parent 8bdf16e commit dd41c62

File tree

6 files changed

+392
-19
lines changed

6 files changed

+392
-19
lines changed

packages/core/src/index.ts

+1
Original file line numberDiff line numberDiff line change
@@ -90,6 +90,7 @@ export { extraErrorDataIntegration } from './integrations/extraerrordata';
9090
export { rewriteFramesIntegration } from './integrations/rewriteframes';
9191
export { sessionTimingIntegration } from './integrations/sessiontiming';
9292
export { zodErrorsIntegration } from './integrations/zoderrors';
93+
export { thirdPartyErrorFilterIntegration } from './integrations/third-party-errors-filter';
9394
export { metrics } from './metrics/exports';
9495
export type { MetricData } from '@sentry/types';
9596
export { metricsDefault } from './metrics/exports-default';

packages/core/src/integrations/dedupe.ts

+3-17
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import type { Event, Exception, IntegrationFn, StackFrame } from '@sentry/types';
2-
import { logger } from '@sentry/utils';
2+
import { getFramesFromEvent, logger } from '@sentry/utils';
33
import { defineIntegration } from '../integration';
44

55
import { DEBUG_BUILD } from '../debug-build';
@@ -106,8 +106,8 @@ function _isSameExceptionEvent(currentEvent: Event, previousEvent: Event): boole
106106
}
107107

108108
function _isSameStacktrace(currentEvent: Event, previousEvent: Event): boolean {
109-
let currentFrames = _getFramesFromEvent(currentEvent);
110-
let previousFrames = _getFramesFromEvent(previousEvent);
109+
let currentFrames = getFramesFromEvent(currentEvent);
110+
let previousFrames = getFramesFromEvent(previousEvent);
111111

112112
// If neither event has a stacktrace, they are assumed to be the same
113113
if (!currentFrames && !previousFrames) {
@@ -173,17 +173,3 @@ function _isSameFingerprint(currentEvent: Event, previousEvent: Event): boolean
173173
function _getExceptionFromEvent(event: Event): Exception | undefined {
174174
return event.exception && event.exception.values && event.exception.values[0];
175175
}
176-
177-
function _getFramesFromEvent(event: Event): StackFrame[] | undefined {
178-
const exception = event.exception;
179-
180-
if (exception) {
181-
try {
182-
// @ts-expect-error Object could be undefined
183-
return exception.values[0].stacktrace.frames;
184-
} catch (_oO) {
185-
return undefined;
186-
}
187-
}
188-
return undefined;
189-
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,114 @@
1+
import type { Event, EventItem } from '@sentry/types';
2+
import { forEachEnvelopeItem, getFramesFromEvent } from '@sentry/utils';
3+
import { defineIntegration } from '../integration';
4+
import { addMetadataToStackFrames, stripMetadataFromStackFrames } from '../metadata';
5+
6+
interface Options {
7+
/**
8+
* Keys that have been provided in the Sentry bundler plugin via the the `applicationKey` option, identifying your bundles.
9+
*
10+
* - Webpack plugin: https://www.npmjs.com/package/@sentry/webpack-plugin#applicationkey
11+
* - Vite plugin: https://www.npmjs.com/package/@sentry/vite-plugin#applicationkey
12+
* - Esbuild plugin: https://www.npmjs.com/package/@sentry/esbuild-plugin#applicationkey
13+
* - Rollup plugin: https://www.npmjs.com/package/@sentry/rollup-plugin#applicationkey
14+
*/
15+
filterKeys: string[];
16+
17+
/**
18+
* Defines how the integration should behave. "Third-Party Stack Frames" are stack frames that did not come from files marked with a matching bundle key.
19+
*
20+
* You can define the behaviour with one of 4 modes:
21+
* - `drop-error-if-contains-third-party-frames`: Drop error events that contain at least one third-party stack frame.
22+
* - `drop-error-if-exclusively-contains-third-party-frames`: Drop error events that exclusively contain third-party stack frames.
23+
* - `apply-tag-if-contains-third-party-frames`: Keep all error events, but apply a `third_party_code: true` tag in case the error contains at least one third-party stack frame.
24+
* - `apply-tag-if-exclusively-contains-third-party-frames`: Keep all error events, but apply a `third_party_code: true` tag in case the error contains exclusively third-party stack frames.
25+
*
26+
* If you chose the mode to only apply tags, the tags can then be used in Sentry to filter your issue stream by entering `!third_party_code:True` in the search bar.
27+
*/
28+
behaviour:
29+
| 'drop-error-if-contains-third-party-frames'
30+
| 'drop-error-if-exclusively-contains-third-party-frames'
31+
| 'apply-tag-if-contains-third-party-frames'
32+
| 'apply-tag-if-exclusively-contains-third-party-frames';
33+
}
34+
35+
/**
36+
* This integration allows you to filter out, or tag error events that do not come from user code marked with a bundle key via the Sentry bundler plugins.
37+
*/
38+
export const thirdPartyErrorFilterIntegration = defineIntegration((options: Options) => {
39+
return {
40+
name: 'ThirdPartyErrorsFilter',
41+
setup(client) {
42+
// We need to strip metadata from stack frames before sending them to Sentry since these are client side only.
43+
// TODO(lforst): Move this cleanup logic into a more central place in the SDK.
44+
client.on('beforeEnvelope', envelope => {
45+
forEachEnvelopeItem(envelope, (item, type) => {
46+
if (type === 'event') {
47+
const event = Array.isArray(item) ? (item as EventItem)[1] : undefined;
48+
49+
if (event) {
50+
stripMetadataFromStackFrames(event);
51+
item[1] = event;
52+
}
53+
}
54+
});
55+
});
56+
},
57+
processEvent(event, _hint, client) {
58+
const stackParser = client.getOptions().stackParser;
59+
addMetadataToStackFrames(stackParser, event);
60+
61+
const frameKeys = getBundleKeysForAllFramesWithFilenames(event);
62+
63+
if (frameKeys) {
64+
const arrayMethod =
65+
options.behaviour === 'drop-error-if-contains-third-party-frames' ||
66+
options.behaviour === 'apply-tag-if-contains-third-party-frames'
67+
? 'some'
68+
: 'every';
69+
70+
const behaviourApplies = frameKeys[arrayMethod](keys => !keys.some(key => options.filterKeys.includes(key)));
71+
72+
if (behaviourApplies) {
73+
const shouldDrop =
74+
options.behaviour === 'drop-error-if-contains-third-party-frames' ||
75+
options.behaviour === 'drop-error-if-exclusively-contains-third-party-frames';
76+
if (shouldDrop) {
77+
return null;
78+
} else {
79+
event.tags = {
80+
...event.tags,
81+
third_party_code: true,
82+
};
83+
}
84+
}
85+
}
86+
87+
return event;
88+
},
89+
};
90+
});
91+
92+
function getBundleKeysForAllFramesWithFilenames(event: Event): string[][] | undefined {
93+
const frames = getFramesFromEvent(event);
94+
95+
if (!frames) {
96+
return undefined;
97+
}
98+
99+
return (
100+
frames
101+
// Exclude frames without a filename since these are likely native code or built-ins
102+
.filter(frame => !!frame.filename)
103+
.map(frame => {
104+
if (frame.module_metadata) {
105+
return Object.keys(frame.module_metadata)
106+
.filter(key => key.startsWith(BUNDLER_PLUGIN_APP_KEY_PREFIX))
107+
.map(key => key.slice(BUNDLER_PLUGIN_APP_KEY_PREFIX.length));
108+
}
109+
return [];
110+
})
111+
);
112+
}
113+
114+
const BUNDLER_PLUGIN_APP_KEY_PREFIX = '_sentryBundlerPluginAppKey:';

packages/core/src/metadata.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -60,7 +60,7 @@ export function addMetadataToStackFrames(parser: StackParser, event: Event): voi
6060
}
6161

6262
for (const frame of exception.stacktrace.frames || []) {
63-
if (!frame.filename) {
63+
if (!frame.filename || frame.module_metadata) {
6464
continue;
6565
}
6666

0 commit comments

Comments
 (0)