From dbd6b4ee9614e2c68024929cf7aadabc271b5f3b Mon Sep 17 00:00:00 2001 From: Tim Fish Date: Tue, 28 May 2024 19:52:25 +0200 Subject: [PATCH 1/5] feat: Add `thirdPartyErrorFilterIntegration` --- packages/core/src/index.ts | 1 + packages/core/src/integrations/dedupe.ts | 20 +- .../integrations/third-party-errors-filter.ts | 94 ++++++++ packages/core/src/metadata.ts | 2 +- .../third-party-errors-filter.test.ts | 204 ++++++++++++++++++ packages/utils/src/stacktrace.ts | 19 +- 6 files changed, 321 insertions(+), 19 deletions(-) create mode 100644 packages/core/src/integrations/third-party-errors-filter.ts create mode 100644 packages/core/test/lib/integrations/third-party-errors-filter.test.ts diff --git a/packages/core/src/index.ts b/packages/core/src/index.ts index 03dfa8e63aa3..c9aaef73da5b 100644 --- a/packages/core/src/index.ts +++ b/packages/core/src/index.ts @@ -96,6 +96,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'; diff --git a/packages/core/src/integrations/dedupe.ts b/packages/core/src/integrations/dedupe.ts index 13d92fe3d56b..e23085875f2b 100644 --- a/packages/core/src/integrations/dedupe.ts +++ b/packages/core/src/integrations/dedupe.ts @@ -1,5 +1,5 @@ import type { Event, Exception, IntegrationFn, StackFrame } from '@sentry/types'; -import { logger } from '@sentry/utils'; +import { logger, getFramesFromEvent } from '@sentry/utils'; import { defineIntegration } from '../integration'; import { DEBUG_BUILD } from '../debug-build'; @@ -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) { @@ -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; -} diff --git a/packages/core/src/integrations/third-party-errors-filter.ts b/packages/core/src/integrations/third-party-errors-filter.ts new file mode 100644 index 000000000000..c8c7efc05a84 --- /dev/null +++ b/packages/core/src/integrations/third-party-errors-filter.ts @@ -0,0 +1,94 @@ +import type { Event, EventItem } from '@sentry/types'; +import { forEachEnvelopeItem, getFramesFromEvent } from '@sentry/utils'; +import { defineIntegration } from '../integration'; +import { addMetadataToStackFrames, stripMetadataFromStackFrames } from '../metadata'; + +type Behaviour = + | 'drop-if-some-frames-not-matched' + | 'drop-if-every-frames-not-matched' + | 'apply-tag-if-some-frames-not-matched' + | 'apply-tag-if-every-frames-not-matched'; + +interface Options { + /** + * Keys that have been provided in the Sentry bundler plugin. + */ + filterKeys: string[]; + + /** + * Defines how the integration should behave: + * + * - `drop-if-some-frames-not-matched`: Drops error events that contain stack frames that did not come from files marked with a matching bundle key. + * - `drop-if-every-frames-not-matched`: Drops error events exclusively contain stack frames that did not come from files marked with a matching bundle key + * - `apply-tag-if-some-frames-not-matched`: Keep events, but apply a `not-application-code: True` tag in case some frames did not come from user code. + * - `apply-tag-if-every-frames-not-matched`: Keep events, but apply a `not-application-code: True` tag in case ale frames did not come from user code. + */ + behaviour: Behaviour; +} + +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) + // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access + .map(frame => (frame.module_metadata ? frame.module_metadata.bundle_key || '' : '')) + ); +} + +/** + * This integration filters out errors that do not come from user code marked with a bundle key via the Sentry bundler plugins. + */ +export const thirdPartyErrorFilterIntegration = defineIntegration((options: Options) => { + // Since the logic for out behaviours is inverted, we need to use the opposite array method. + const arrayMethod = options.behaviour.match(/some/) ? 'every' : 'some'; + const shouldDrop = !!options.behaviour.match(/drop/); + + return { + name: 'ThirdPartyErrorsFilter', + setup(client) { + // We need to strip metadata from stack frames before sending them to Sentry since these are client side only. + 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, _, client) { + const stackParser = client.getOptions().stackParser; + addMetadataToStackFrames(stackParser, event); + + const frameKeys = getBundleKeysForAllFramesWithFilenames(event); + + if (frameKeys) { + const match = frameKeys[arrayMethod](key => !options.filterKeys.includes(key)); + + if (match) { + if (shouldDrop) { + return null; + } else { + event.tags = { + ...event.tags, + 'not-application-code': true, + }; + } + } + } + + return event; + }, + }; +}); diff --git a/packages/core/src/metadata.ts b/packages/core/src/metadata.ts index d1ebac6e90e5..c13a5dabce34 100644 --- a/packages/core/src/metadata.ts +++ b/packages/core/src/metadata.ts @@ -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; } diff --git a/packages/core/test/lib/integrations/third-party-errors-filter.test.ts b/packages/core/test/lib/integrations/third-party-errors-filter.test.ts new file mode 100644 index 000000000000..46d4034ba964 --- /dev/null +++ b/packages/core/test/lib/integrations/third-party-errors-filter.test.ts @@ -0,0 +1,204 @@ +import type { Client, Event } from '@sentry/types'; +import { GLOBAL_OBJ, createStackParser, nodeStackLineParser } from '@sentry/utils'; +import { thirdPartyErrorFilterIntegration } from '../../../src/integrations/third-party-errors-filter'; + +function clone(data: T): T { + return JSON.parse(JSON.stringify(data)); +} + +const stack = new Error().stack || ''; + +const eventSomeFrames: Event = { + exception: { + values: [ + { + stacktrace: { + frames: [ + { + colno: 1, + filename: __filename, + function: 'function', + lineno: 1, + }, + { + colno: 2, + filename: 'other-file.js', + function: 'function', + lineno: 2, + }, + ], + }, + type: 'SyntaxError', + value: 'missing ( on line 10', + }, + ], + }, +}; + +const eventAllFrames: Event = { + exception: { + values: [ + { + stacktrace: { + frames: [ + { + colno: 1, + filename: __filename, + function: 'function', + lineno: 1, + }, + { + colno: 2, + filename: __filename, + function: 'function', + lineno: 2, + }, + ], + }, + type: 'SyntaxError', + value: 'missing ( on line 10', + }, + ], + }, +}; + +const eventNoFrames: Event = { + exception: { + values: [ + { + stacktrace: { + frames: [ + { + colno: 1, + filename: 'other-file.js', + function: 'function', + lineno: 1, + }, + { + colno: 2, + filename: 'other-file.js', + function: 'function', + lineno: 2, + }, + ], + }, + type: 'SyntaxError', + value: 'missing ( on line 10', + }, + ], + }, +}; + +// This only needs the stackParser +const MOCK_CLIENT = { + getOptions: () => ({ + stackParser: createStackParser(nodeStackLineParser()), + }), +} as unknown as Client; + +describe('ThirdPartyErrorFilter', () => { + beforeEach(() => { + GLOBAL_OBJ._sentryModuleMetadata = GLOBAL_OBJ._sentryModuleMetadata || {}; + GLOBAL_OBJ._sentryModuleMetadata[stack] = { bundle_key: 'some-key' }; + }); + + describe('drop-if-any-frames-not-matched', () => { + it('should drop event if not all frames matched', async () => { + const integration = thirdPartyErrorFilterIntegration({ + behaviour: 'drop-if-every-frames-not-matched', + filterKeys: ['some-key'], + }); + + const event = clone(eventSomeFrames); + const result = await integration.processEvent?.(event, {}, MOCK_CLIENT); + expect(result).toBe(null); + }); + + it('should keep event if all frames matched', async () => { + const integration = thirdPartyErrorFilterIntegration({ + behaviour: 'drop-if-every-frames-not-matched', + filterKeys: ['some-key'], + }); + + const event = clone(eventAllFrames); + const result = await integration.processEvent?.(event, {}, MOCK_CLIENT); + expect(result).toBeDefined(); + }); + }); + + describe('drop-if-some-frames-not-matched', () => { + it('should drop event if not all frames matched', async () => { + const integration = thirdPartyErrorFilterIntegration({ + behaviour: 'drop-if-some-frames-not-matched', + filterKeys: ['some-key'], + }); + + const event = clone(eventNoFrames); + const result = await integration.processEvent?.(event, {}, MOCK_CLIENT); + expect(result).toBe(null); + }); + + it('should keep event if all frames matched', async () => { + const integration = thirdPartyErrorFilterIntegration({ + behaviour: 'drop-if-some-frames-not-matched', + filterKeys: ['some-key'], + }); + + const event = clone(eventSomeFrames); + const result = await integration.processEvent?.(event, {}, MOCK_CLIENT); + expect(result).toBeDefined(); + }); + }); + + describe('apply-tag-if-any-frames-not-matched', () => { + it('should tag event if not all frames matched', async () => { + const integration = thirdPartyErrorFilterIntegration({ + behaviour: 'apply-tag-if-every-frames-not-matched', + filterKeys: ['some-key'], + }); + + const event = clone(eventSomeFrames); + const result = await integration.processEvent?.(event, {}, MOCK_CLIENT); + expect(result).toBeDefined(); + expect(result?.tags).toEqual({ 'not-application-code': true }); + }); + + it('should not tag event if all frames matched', async () => { + const integration = thirdPartyErrorFilterIntegration({ + behaviour: 'apply-tag-if-every-frames-not-matched', + filterKeys: ['some-key'], + }); + + const event = clone(eventAllFrames); + const result = await integration.processEvent?.(event, {}, MOCK_CLIENT); + expect(result).toBeDefined(); + expect(result?.tags).toBeUndefined(); + }); + }); + + describe('apply-tag-if-some-frames-not-matched', () => { + it('should tag event if not all frames matched', async () => { + const integration = thirdPartyErrorFilterIntegration({ + behaviour: 'apply-tag-if-some-frames-not-matched', + filterKeys: ['some-key'], + }); + + const event = clone(eventNoFrames); + const result = await integration.processEvent?.(event, {}, MOCK_CLIENT); + expect(result).toBeDefined(); + expect(result?.tags).toEqual({ 'not-application-code': true }); + }); + + it('should not tag event if all frames matched', async () => { + const integration = thirdPartyErrorFilterIntegration({ + behaviour: 'apply-tag-if-some-frames-not-matched', + filterKeys: ['some-key'], + }); + + const event = clone(eventSomeFrames); + const result = await integration.processEvent?.(event, {}, MOCK_CLIENT); + expect(result).toBeDefined(); + expect(result?.tags).toBeUndefined(); + }); + }); +}); diff --git a/packages/utils/src/stacktrace.ts b/packages/utils/src/stacktrace.ts index bc2274ef522f..7fbddc760d44 100644 --- a/packages/utils/src/stacktrace.ts +++ b/packages/utils/src/stacktrace.ts @@ -1,4 +1,4 @@ -import type { StackFrame, StackLineParser, StackParser } from '@sentry/types'; +import type { Event, StackFrame, StackLineParser, StackParser } from '@sentry/types'; const STACKTRACE_FRAME_LIMIT = 50; export const UNKNOWN_FUNCTION = '?'; @@ -133,3 +133,20 @@ export function getFunctionName(fn: unknown): string { return defaultFunctionName; } } + +/** + * Get's stack frames from an event without needing to check for undefined properties. + */ +export 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; +} From 0632b0c23bbb7b04ea9ec5b675f235c9893811ae Mon Sep 17 00:00:00 2001 From: Tim Fish Date: Tue, 28 May 2024 20:52:23 +0200 Subject: [PATCH 2/5] lint --- packages/core/src/integrations/dedupe.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/core/src/integrations/dedupe.ts b/packages/core/src/integrations/dedupe.ts index e23085875f2b..94eea57e68cb 100644 --- a/packages/core/src/integrations/dedupe.ts +++ b/packages/core/src/integrations/dedupe.ts @@ -1,5 +1,5 @@ import type { Event, Exception, IntegrationFn, StackFrame } from '@sentry/types'; -import { logger, getFramesFromEvent } from '@sentry/utils'; +import { getFramesFromEvent, logger } from '@sentry/utils'; import { defineIntegration } from '../integration'; import { DEBUG_BUILD } from '../debug-build'; From 6632edc74e9b3fe27638ed4ad139f9c785c20078 Mon Sep 17 00:00:00 2001 From: Luca Forstner Date: Fri, 31 May 2024 11:37:24 +0000 Subject: [PATCH 3/5] Refactor - Get rid of negation in behaviour modes - Co-locate some variables with their checcks - Extend getFramesFromEvent to get frames from all event-values - Change tag to `third_party_code` to get rid of negation --- .../integrations/third-party-errors-filter.ts | 85 ++++++------ .../third-party-errors-filter.test.ts | 122 ++++++++++++------ packages/utils/src/stacktrace.ts | 10 +- 3 files changed, 136 insertions(+), 81 deletions(-) diff --git a/packages/core/src/integrations/third-party-errors-filter.ts b/packages/core/src/integrations/third-party-errors-filter.ts index c8c7efc05a84..88d0323c6f95 100644 --- a/packages/core/src/integrations/third-party-errors-filter.ts +++ b/packages/core/src/integrations/third-party-errors-filter.ts @@ -3,57 +3,39 @@ import { forEachEnvelopeItem, getFramesFromEvent } from '@sentry/utils'; import { defineIntegration } from '../integration'; import { addMetadataToStackFrames, stripMetadataFromStackFrames } from '../metadata'; -type Behaviour = - | 'drop-if-some-frames-not-matched' - | 'drop-if-every-frames-not-matched' - | 'apply-tag-if-some-frames-not-matched' - | 'apply-tag-if-every-frames-not-matched'; - interface Options { /** - * Keys that have been provided in the Sentry bundler plugin. + * Keys that have been provided in the Sentry bundler plugin, identifying your bundles. */ + // TODO(lforst): Explain in JSDoc which option exactly needs to be set when we have figured out the API and deep link to the option in npm filterKeys: string[]; /** - * Defines how the integration should behave: + * 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. * - * - `drop-if-some-frames-not-matched`: Drops error events that contain stack frames that did not come from files marked with a matching bundle key. - * - `drop-if-every-frames-not-matched`: Drops error events exclusively contain stack frames that did not come from files marked with a matching bundle key - * - `apply-tag-if-some-frames-not-matched`: Keep events, but apply a `not-application-code: True` tag in case some frames did not come from user code. - * - `apply-tag-if-every-frames-not-matched`: Keep events, but apply a `not-application-code: True` tag in case ale frames did not come from user code. + * 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: Behaviour; + 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'; } - -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) - // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access - .map(frame => (frame.module_metadata ? frame.module_metadata.bundle_key || '' : '')) - ); -} - /** - * This integration filters out errors that do not come from user code marked with a bundle key via the Sentry bundler plugins. + * 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) => { - // Since the logic for out behaviours is inverted, we need to use the opposite array method. - const arrayMethod = options.behaviour.match(/some/) ? 'every' : 'some'; - const shouldDrop = !!options.behaviour.match(/drop/); - 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') { @@ -67,22 +49,31 @@ export const thirdPartyErrorFilterIntegration = defineIntegration((options: Opti }); }); }, - processEvent(event, _, client) { + processEvent(event, _hint, client) { const stackParser = client.getOptions().stackParser; addMetadataToStackFrames(stackParser, event); const frameKeys = getBundleKeysForAllFramesWithFilenames(event); if (frameKeys) { - const match = frameKeys[arrayMethod](key => !options.filterKeys.includes(key)); + 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](key => !options.filterKeys.includes(key)); - if (match) { + 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, - 'not-application-code': true, + third_party_code: true, }; } } @@ -92,3 +83,19 @@ export const thirdPartyErrorFilterIntegration = defineIntegration((options: Opti }, }; }); + +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) + // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access + .map(frame => (frame.module_metadata ? frame.module_metadata.bundle_key || '' : '')) + ); +} diff --git a/packages/core/test/lib/integrations/third-party-errors-filter.test.ts b/packages/core/test/lib/integrations/third-party-errors-filter.test.ts index 46d4034ba964..e99965475da2 100644 --- a/packages/core/test/lib/integrations/third-party-errors-filter.test.ts +++ b/packages/core/test/lib/integrations/third-party-errors-filter.test.ts @@ -8,7 +8,7 @@ function clone(data: T): T { const stack = new Error().stack || ''; -const eventSomeFrames: Event = { +const eventWithThirdAndFirstPartyFrames: Event = { exception: { values: [ { @@ -35,7 +35,7 @@ const eventSomeFrames: Event = { }, }; -const eventAllFrames: Event = { +const eventWithOnlyFirstPartyFrames: Event = { exception: { values: [ { @@ -62,7 +62,7 @@ const eventAllFrames: Event = { }, }; -const eventNoFrames: Event = { +const eventWithOnlyThirdPartyFrames: Event = { exception: { values: [ { @@ -102,103 +102,143 @@ describe('ThirdPartyErrorFilter', () => { GLOBAL_OBJ._sentryModuleMetadata[stack] = { bundle_key: 'some-key' }; }); - describe('drop-if-any-frames-not-matched', () => { - it('should drop event if not all frames matched', async () => { + describe('drop-error-if-contains-third-party-frames', () => { + it('should keep event if there are exclusively first-party frames', async () => { const integration = thirdPartyErrorFilterIntegration({ - behaviour: 'drop-if-every-frames-not-matched', + behaviour: 'drop-error-if-contains-third-party-frames', filterKeys: ['some-key'], }); - const event = clone(eventSomeFrames); + const event = clone(eventWithOnlyFirstPartyFrames); + const result = await integration.processEvent?.(event, {}, MOCK_CLIENT); + expect(result).toBeDefined(); + }); + + it('should drop event if there is at least one third-party frame', async () => { + const integration = thirdPartyErrorFilterIntegration({ + behaviour: 'drop-error-if-contains-third-party-frames', + filterKeys: ['some-key'], + }); + + const event = clone(eventWithThirdAndFirstPartyFrames); const result = await integration.processEvent?.(event, {}, MOCK_CLIENT); expect(result).toBe(null); }); - it('should keep event if all frames matched', async () => { + it('should drop event if all frames are third-party frames', async () => { const integration = thirdPartyErrorFilterIntegration({ - behaviour: 'drop-if-every-frames-not-matched', + behaviour: 'drop-error-if-contains-third-party-frames', filterKeys: ['some-key'], }); - const event = clone(eventAllFrames); + const event = clone(eventWithOnlyThirdPartyFrames); const result = await integration.processEvent?.(event, {}, MOCK_CLIENT); - expect(result).toBeDefined(); + expect(result).toBe(null); }); }); - describe('drop-if-some-frames-not-matched', () => { - it('should drop event if not all frames matched', async () => { + describe('drop-error-if-exclusively-contains-third-party-frames', () => { + it('should keep event if there are exclusively first-party frames', async () => { const integration = thirdPartyErrorFilterIntegration({ - behaviour: 'drop-if-some-frames-not-matched', + behaviour: 'drop-error-if-exclusively-contains-third-party-frames', filterKeys: ['some-key'], }); - const event = clone(eventNoFrames); + const event = clone(eventWithOnlyFirstPartyFrames); const result = await integration.processEvent?.(event, {}, MOCK_CLIENT); - expect(result).toBe(null); + expect(result).toBeDefined(); }); - it('should keep event if all frames matched', async () => { + it('should keep event if there is at least one first-party frame', async () => { const integration = thirdPartyErrorFilterIntegration({ - behaviour: 'drop-if-some-frames-not-matched', + behaviour: 'drop-error-if-exclusively-contains-third-party-frames', filterKeys: ['some-key'], }); - const event = clone(eventSomeFrames); + const event = clone(eventWithThirdAndFirstPartyFrames); const result = await integration.processEvent?.(event, {}, MOCK_CLIENT); expect(result).toBeDefined(); }); + + it('should drop event if all frames are third-party frames', async () => { + const integration = thirdPartyErrorFilterIntegration({ + behaviour: 'drop-error-if-exclusively-contains-third-party-frames', + filterKeys: ['some-key'], + }); + + const event = clone(eventWithOnlyThirdPartyFrames); + const result = await integration.processEvent?.(event, {}, MOCK_CLIENT); + expect(result).toBe(null); + }); }); - describe('apply-tag-if-any-frames-not-matched', () => { - it('should tag event if not all frames matched', async () => { + describe('apply-tag-if-contains-third-party-frames', () => { + it('should not tag event if exclusively contains first-party frames', async () => { const integration = thirdPartyErrorFilterIntegration({ - behaviour: 'apply-tag-if-every-frames-not-matched', + behaviour: 'apply-tag-if-contains-third-party-frames', filterKeys: ['some-key'], }); - const event = clone(eventSomeFrames); + const event = clone(eventWithOnlyFirstPartyFrames); const result = await integration.processEvent?.(event, {}, MOCK_CLIENT); - expect(result).toBeDefined(); - expect(result?.tags).toEqual({ 'not-application-code': true }); + expect(result?.tags?.third_party_code).toBeUndefined(); }); - it('should not tag event if all frames matched', async () => { + it('should tag event if contains at least one third-party frame', async () => { const integration = thirdPartyErrorFilterIntegration({ - behaviour: 'apply-tag-if-every-frames-not-matched', + behaviour: 'apply-tag-if-contains-third-party-frames', filterKeys: ['some-key'], }); - const event = clone(eventAllFrames); + const event = clone(eventWithThirdAndFirstPartyFrames); const result = await integration.processEvent?.(event, {}, MOCK_CLIENT); - expect(result).toBeDefined(); - expect(result?.tags).toBeUndefined(); + expect(result?.tags).toMatchObject({ third_party_code: true }); + }); + + it('should tag event if contains exclusively third-party frames', async () => { + const integration = thirdPartyErrorFilterIntegration({ + behaviour: 'apply-tag-if-contains-third-party-frames', + filterKeys: ['some-key'], + }); + + const event = clone(eventWithOnlyThirdPartyFrames); + const result = await integration.processEvent?.(event, {}, MOCK_CLIENT); + expect(result?.tags).toMatchObject({ third_party_code: true }); }); }); - describe('apply-tag-if-some-frames-not-matched', () => { - it('should tag event if not all frames matched', async () => { + describe('apply-tag-if-exclusively-contains-third-party-frames', () => { + it('should not tag event if exclusively contains first-party frames', async () => { const integration = thirdPartyErrorFilterIntegration({ - behaviour: 'apply-tag-if-some-frames-not-matched', + behaviour: 'apply-tag-if-exclusively-contains-third-party-frames', filterKeys: ['some-key'], }); - const event = clone(eventNoFrames); + const event = clone(eventWithOnlyFirstPartyFrames); const result = await integration.processEvent?.(event, {}, MOCK_CLIENT); - expect(result).toBeDefined(); - expect(result?.tags).toEqual({ 'not-application-code': true }); + expect(result?.tags?.third_party_code).toBeUndefined(); }); - it('should not tag event if all frames matched', async () => { + it('should not tag event if contains at least one first-party frame', async () => { const integration = thirdPartyErrorFilterIntegration({ - behaviour: 'apply-tag-if-some-frames-not-matched', + behaviour: 'apply-tag-if-exclusively-contains-third-party-frames', filterKeys: ['some-key'], }); - const event = clone(eventSomeFrames); + const event = clone(eventWithThirdAndFirstPartyFrames); const result = await integration.processEvent?.(event, {}, MOCK_CLIENT); - expect(result).toBeDefined(); - expect(result?.tags).toBeUndefined(); + expect(result?.tags?.third_party_code).toBeUndefined(); + }); + + it('should tag event if contains exclusively third-party frames', async () => { + const integration = thirdPartyErrorFilterIntegration({ + behaviour: 'apply-tag-if-exclusively-contains-third-party-frames', + filterKeys: ['some-key'], + }); + + const event = clone(eventWithOnlyThirdPartyFrames); + const result = await integration.processEvent?.(event, {}, MOCK_CLIENT); + expect(result?.tags).toMatchObject({ third_party_code: true }); }); }); }); diff --git a/packages/utils/src/stacktrace.ts b/packages/utils/src/stacktrace.ts index 7fbddc760d44..dfb2a6e6269f 100644 --- a/packages/utils/src/stacktrace.ts +++ b/packages/utils/src/stacktrace.ts @@ -141,9 +141,17 @@ export function getFramesFromEvent(event: Event): StackFrame[] | undefined { const exception = event.exception; if (exception) { + const frames: StackFrame[] = []; try { // @ts-expect-error Object could be undefined - return exception.values[0].stacktrace.frames; + exception.values.forEach(value => { + // @ts-expect-error Value could be undefined + if (value.stacktrace.frames) { + // @ts-expect-error Value could be undefined + frames.push(...value.stacktrace.frames); + } + }); + return frames; } catch (_oO) { return undefined; } From c83905e1839dbff1113e2a7f9cee0a126fcbe5fc Mon Sep 17 00:00:00 2001 From: Luca Forstner Date: Fri, 31 May 2024 14:19:47 +0000 Subject: [PATCH 4/5] Sync with https://github.com/getsentry/sentry-javascript-bundler-plugins/pull/540 --- .../integrations/third-party-errors-filter.ts | 16 ++++++++++++---- .../third-party-errors-filter.test.ts | 5 ++++- 2 files changed, 16 insertions(+), 5 deletions(-) diff --git a/packages/core/src/integrations/third-party-errors-filter.ts b/packages/core/src/integrations/third-party-errors-filter.ts index 88d0323c6f95..d4294ea28e7c 100644 --- a/packages/core/src/integrations/third-party-errors-filter.ts +++ b/packages/core/src/integrations/third-party-errors-filter.ts @@ -62,7 +62,7 @@ export const thirdPartyErrorFilterIntegration = defineIntegration((options: Opti ? 'some' : 'every'; - const behaviourApplies = frameKeys[arrayMethod](key => !options.filterKeys.includes(key)); + const behaviourApplies = frameKeys[arrayMethod](keys => !keys.some(key => options.filterKeys.includes(key))); if (behaviourApplies) { const shouldDrop = @@ -84,7 +84,7 @@ export const thirdPartyErrorFilterIntegration = defineIntegration((options: Opti }; }); -function getBundleKeysForAllFramesWithFilenames(event: Event): string[] | undefined { +function getBundleKeysForAllFramesWithFilenames(event: Event): string[][] | undefined { const frames = getFramesFromEvent(event); if (!frames) { @@ -95,7 +95,15 @@ function getBundleKeysForAllFramesWithFilenames(event: Event): string[] | undefi frames // Exclude frames without a filename since these are likely native code or built-ins .filter(frame => !!frame.filename) - // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access - .map(frame => (frame.module_metadata ? frame.module_metadata.bundle_key || '' : '')) + .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:'; diff --git a/packages/core/test/lib/integrations/third-party-errors-filter.test.ts b/packages/core/test/lib/integrations/third-party-errors-filter.test.ts index e99965475da2..d0fd02045080 100644 --- a/packages/core/test/lib/integrations/third-party-errors-filter.test.ts +++ b/packages/core/test/lib/integrations/third-party-errors-filter.test.ts @@ -99,7 +99,10 @@ const MOCK_CLIENT = { describe('ThirdPartyErrorFilter', () => { beforeEach(() => { GLOBAL_OBJ._sentryModuleMetadata = GLOBAL_OBJ._sentryModuleMetadata || {}; - GLOBAL_OBJ._sentryModuleMetadata[stack] = { bundle_key: 'some-key' }; + GLOBAL_OBJ._sentryModuleMetadata[stack] = { + '_sentryBundlerPluginAppKey:some-key': true, + '_sentryBundlerPluginAppKey:some-other-key': true, + }; }); describe('drop-error-if-contains-third-party-frames', () => { From dcad85bea9ef4030297b86c65eb16b178f95f395 Mon Sep 17 00:00:00 2001 From: Luca Forstner Date: Mon, 3 Jun 2024 09:46:35 +0000 Subject: [PATCH 5/5] Update comment with bundler plugin option --- .../core/src/integrations/third-party-errors-filter.ts | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/packages/core/src/integrations/third-party-errors-filter.ts b/packages/core/src/integrations/third-party-errors-filter.ts index d4294ea28e7c..70e7317f58c3 100644 --- a/packages/core/src/integrations/third-party-errors-filter.ts +++ b/packages/core/src/integrations/third-party-errors-filter.ts @@ -5,9 +5,13 @@ import { addMetadataToStackFrames, stripMetadataFromStackFrames } from '../metad interface Options { /** - * Keys that have been provided in the Sentry bundler plugin, identifying your bundles. + * 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 */ - // TODO(lforst): Explain in JSDoc which option exactly needs to be set when we have figured out the API and deep link to the option in npm filterKeys: string[]; /** @@ -27,6 +31,7 @@ interface Options { | 'apply-tag-if-contains-third-party-frames' | 'apply-tag-if-exclusively-contains-third-party-frames'; } + /** * 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. */