Skip to content
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

feat: Add thirdPartyErrorFilterIntegration #12267

Merged
merged 9 commits into from
Jun 6, 2024
1 change: 1 addition & 0 deletions packages/core/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,7 @@ export { extraErrorDataIntegration } from './integrations/extraerrordata';
export { rewriteFramesIntegration } from './integrations/rewriteframes';
export { sessionTimingIntegration } from './integrations/sessiontiming';
export { zodErrorsIntegration } from './integrations/zoderrors';
export { thirdPartyErrorFilterIntegration } from './integrations/third-party-errors-filter';
export { metrics } from './metrics/exports';
export type { MetricData } from '@sentry/types';
export { metricsDefault } from './metrics/exports-default';
Expand Down
20 changes: 3 additions & 17 deletions packages/core/src/integrations/dedupe.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import type { Event, Exception, IntegrationFn, StackFrame } from '@sentry/types';
import { logger } from '@sentry/utils';
import { getFramesFromEvent, logger } from '@sentry/utils';
import { defineIntegration } from '../integration';

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

function _isSameStacktrace(currentEvent: Event, previousEvent: Event): boolean {
let currentFrames = _getFramesFromEvent(currentEvent);
let previousFrames = _getFramesFromEvent(previousEvent);
let currentFrames = getFramesFromEvent(currentEvent);
let previousFrames = getFramesFromEvent(previousEvent);

// If neither event has a stacktrace, they are assumed to be the same
if (!currentFrames && !previousFrames) {
Expand Down Expand Up @@ -173,17 +173,3 @@ function _isSameFingerprint(currentEvent: Event, previousEvent: Event): boolean
function _getExceptionFromEvent(event: Event): Exception | undefined {
return event.exception && event.exception.values && event.exception.values[0];
}

function _getFramesFromEvent(event: Event): StackFrame[] | undefined {
const exception = event.exception;

if (exception) {
try {
// @ts-expect-error Object could be undefined
return exception.values[0].stacktrace.frames;
} catch (_oO) {
return undefined;
}
}
return undefined;
}
114 changes: 114 additions & 0 deletions packages/core/src/integrations/third-party-errors-filter.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,114 @@
import type { Event, EventItem } from '@sentry/types';
import { forEachEnvelopeItem, getFramesFromEvent } from '@sentry/utils';
import { defineIntegration } from '../integration';
import { addMetadataToStackFrames, stripMetadataFromStackFrames } from '../metadata';

interface Options {
/**
* Keys that have been provided in the Sentry bundler plugin via the the `applicationKey` option, identifying your bundles.
*
* - Webpack plugin: https://www.npmjs.com/package/@sentry/webpack-plugin#applicationkey
* - Vite plugin: https://www.npmjs.com/package/@sentry/vite-plugin#applicationkey
* - Esbuild plugin: https://www.npmjs.com/package/@sentry/esbuild-plugin#applicationkey
* - Rollup plugin: https://www.npmjs.com/package/@sentry/rollup-plugin#applicationkey
*/
filterKeys: string[];

/**
* 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.
*
* You can define the behaviour with one of 4 modes:
* - `drop-error-if-contains-third-party-frames`: Drop error events that contain at least one third-party stack frame.
* - `drop-error-if-exclusively-contains-third-party-frames`: Drop error events that exclusively contain third-party stack frames.
* - `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.
* - `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.
*
* 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.
*/
behaviour:
| 'drop-error-if-contains-third-party-frames'
| 'drop-error-if-exclusively-contains-third-party-frames'
| 'apply-tag-if-contains-third-party-frames'
| 'apply-tag-if-exclusively-contains-third-party-frames';
}
Copy link
Collaborator Author

Choose a reason for hiding this comment

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

These are much better names!

Copy link
Member

Choose a reason for hiding this comment

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

Yup, I agree. Your comment was a wake-up call 😂


/**
* 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.
*/
export const thirdPartyErrorFilterIntegration = defineIntegration((options: Options) => {
return {
name: 'ThirdPartyErrorsFilter',
setup(client) {
// We need to strip metadata from stack frames before sending them to Sentry since these are client side only.
// TODO(lforst): Move this cleanup logic into a more central place in the SDK.
client.on('beforeEnvelope', envelope => {
forEachEnvelopeItem(envelope, (item, type) => {
if (type === 'event') {
const event = Array.isArray(item) ? (item as EventItem)[1] : undefined;

if (event) {
stripMetadataFromStackFrames(event);
item[1] = event;
}
}
});
});
},
processEvent(event, _hint, client) {
const stackParser = client.getOptions().stackParser;
addMetadataToStackFrames(stackParser, event);

const frameKeys = getBundleKeysForAllFramesWithFilenames(event);

if (frameKeys) {
const arrayMethod =
options.behaviour === 'drop-error-if-contains-third-party-frames' ||
options.behaviour === 'apply-tag-if-contains-third-party-frames'
? 'some'
: 'every';

const behaviourApplies = frameKeys[arrayMethod](keys => !keys.some(key => options.filterKeys.includes(key)));

if (behaviourApplies) {
const shouldDrop =
options.behaviour === 'drop-error-if-contains-third-party-frames' ||
options.behaviour === 'drop-error-if-exclusively-contains-third-party-frames';
if (shouldDrop) {
return null;
} else {
event.tags = {
...event.tags,
third_party_code: true,
};
}
}
}

return event;
},
};
});

function getBundleKeysForAllFramesWithFilenames(event: Event): string[][] | undefined {
const frames = getFramesFromEvent(event);

if (!frames) {
return undefined;
}

return (
frames
// Exclude frames without a filename since these are likely native code or built-ins
.filter(frame => !!frame.filename)
.map(frame => {
if (frame.module_metadata) {
return Object.keys(frame.module_metadata)
.filter(key => key.startsWith(BUNDLER_PLUGIN_APP_KEY_PREFIX))
.map(key => key.slice(BUNDLER_PLUGIN_APP_KEY_PREFIX.length));
}
return [];
})
);
}

const BUNDLER_PLUGIN_APP_KEY_PREFIX = '_sentryBundlerPluginAppKey:';
2 changes: 1 addition & 1 deletion packages/core/src/metadata.ts
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,7 @@ export function addMetadataToStackFrames(parser: StackParser, event: Event): voi
}

for (const frame of exception.stacktrace.frames || []) {
if (!frame.filename) {
if (!frame.filename || frame.module_metadata) {
continue;
}

Expand Down
Loading
Loading