From 3ca7008c317d9f467f88aad10f4e3659dd491650 Mon Sep 17 00:00:00 2001 From: Charly Gomez Date: Fri, 7 Mar 2025 13:35:35 +0100 Subject: [PATCH] add experimental integration --- packages/browser/src/index.ts | 1 + packages/core/src/index.ts | 1 + .../domain-based-errors-filter.ts | 160 ++++++++++++++++++ .../core/src/integrations/eventFilters.ts | 1 + 4 files changed, 163 insertions(+) create mode 100644 packages/core/src/integrations/domain-based-errors-filter.ts diff --git a/packages/browser/src/index.ts b/packages/browser/src/index.ts index d034330b6283..804db7608dee 100644 --- a/packages/browser/src/index.ts +++ b/packages/browser/src/index.ts @@ -56,6 +56,7 @@ export { moduleMetadataIntegration, zodErrorsIntegration, thirdPartyErrorFilterIntegration, + _experimentalDomainBasedErrorsFilterIntegration, } from '@sentry/core'; export type { Span } from '@sentry/core'; export { makeBrowserOfflineTransport } from './transports/offline'; diff --git a/packages/core/src/index.ts b/packages/core/src/index.ts index 35bfc35bc603..9aeca897ea19 100644 --- a/packages/core/src/index.ts +++ b/packages/core/src/index.ts @@ -108,6 +108,7 @@ export { extraErrorDataIntegration } from './integrations/extraerrordata'; export { rewriteFramesIntegration } from './integrations/rewriteframes'; export { zodErrorsIntegration } from './integrations/zoderrors'; export { thirdPartyErrorFilterIntegration } from './integrations/third-party-errors-filter'; +export { _experimentalDomainBasedErrorsFilterIntegration } from './integrations/domain-based-errors-filter'; export { profiler } from './profiling'; export { instrumentFetchRequest } from './fetch'; export { trpcMiddleware } from './trpc'; diff --git a/packages/core/src/integrations/domain-based-errors-filter.ts b/packages/core/src/integrations/domain-based-errors-filter.ts new file mode 100644 index 000000000000..c0290ab489bb --- /dev/null +++ b/packages/core/src/integrations/domain-based-errors-filter.ts @@ -0,0 +1,160 @@ +import { defineIntegration } from '../integration'; +import type { StackFrame } from '../types-hoist'; +import { GLOBAL_OBJ, isErrorEvent } from '../utils-hoist'; +import { getFramesFromEvent } from '../utils-hoist/stacktrace'; + +type DomainBasedErrorsFilterOptions = { + /** + * List of domains that are considered "first-party" (your application domains). + * Errors from these domains will not be filtered. + * Example: ['myapp.com', 'cdn.myapp.com'] + */ + appDomains: string[]; + + /** + * List of third-party domains that should be allowed despite not being in appDomains. + * Errors from these domains will not be filtered. + * + */ + allowlistedDomains?: string[]; + + /** + * Defines how the integration should behave with third-party errors. + * + * - `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_domain: 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_domain: true` tag in case the error exclusively contains third-party stack frames. + */ + 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'; + + /** + * Whether to apply the `is_external` flag to stack frames from third-party domains. + * + * Default: `false` + */ + applyIsExternalFrameFlag?: boolean; +}; + +export const _experimentalDomainBasedErrorsFilterIntegration = defineIntegration( + (options: DomainBasedErrorsFilterOptions) => { + const isRunningOnLocalhost = (): boolean => { + // Check if we're in a browser environment + const WINDOW = GLOBAL_OBJ as typeof GLOBAL_OBJ & Window; + if (WINDOW?.location?.href) { + const href = WINDOW.location.href; + + // todo: add a more advanced check + if (href.includes('://localhost:') || href.includes('://127.0.0.1')) { + return true; + } + } + + return false; + }; + + const isLocalhost = isRunningOnLocalhost(); + + return { + name: '_experimentalDomainBasedErrorsFilter', + processEvent(event) { + // skip for non error events and locally running apps + if (isLocalhost || !isErrorEvent(event)) { + return event; + } + + const frames = getFramesFromEvent(event); + if (!frames || frames.length === 0) { + return event; + } + + // collect firstParty domains + // todo: get a sensible default, maybe href + subdomains + const appDomains = options.appDomains || []; + + // todo: merge this list with clientOptions.allowUrls + const allowlistedDomains = options.allowlistedDomains || []; + + let hasThirdPartyFrames = false; + let allFramesAreThirdParty = true; + + frames.forEach(frame => { + // todo: check abs_path or filename here? + if (frame.abs_path) { + try { + const url = new URL(frame.abs_path); + const domain = url.hostname; + + const isExternal = isThirdPartyDomain(domain, appDomains, allowlistedDomains); + + // Add is_external flag to the frame + if (options.applyIsExternalFrameFlag) { + (frame as StackFrame & { is_external?: boolean }).is_external = isExternal; + } + + if (isExternal) { + hasThirdPartyFrames = true; + } else { + allFramesAreThirdParty = false; + } + } catch (e) { + // can't get URL + allFramesAreThirdParty = false; + } + } else { + // No abs path + allFramesAreThirdParty = false; + } + }); + + let applyTag = false; + + if (hasThirdPartyFrames) { + if (options.behaviour === 'drop-error-if-contains-third-party-frames') { + return null; + } + if (options.behaviour === 'apply-tag-if-contains-third-party-frames') { + applyTag = true; + } + } + + if (allFramesAreThirdParty) { + if (options.behaviour === 'drop-error-if-exclusively-contains-third-party-frames') { + return null; + } + if (options.behaviour === 'apply-tag-if-exclusively-contains-third-party-frames') { + applyTag = true; + } + } + + if (applyTag) { + event.tags = { + ...event.tags, + third_party_code: true, + }; + } + + return event; + }, + }; + }, +); + +const isThirdPartyDomain = (domain: string, appDomains: string[], allowlistedDomains: string[]): boolean => { + const isAppDomain = appDomains.some(appDomain => domain === appDomain || domain.endsWith(`.${appDomain}`)); + + if (isAppDomain) { + return false; + } + + // todo: extend this check also check for regexes + const isAllowlisted = allowlistedDomains?.some( + allowedDomain => domain === allowedDomain || domain.endsWith(`.${allowedDomain}`), + ); + + return !isAllowlisted; +}; diff --git a/packages/core/src/integrations/eventFilters.ts b/packages/core/src/integrations/eventFilters.ts index fbf9475feca2..43462fd5c30c 100644 --- a/packages/core/src/integrations/eventFilters.ts +++ b/packages/core/src/integrations/eventFilters.ts @@ -148,6 +148,7 @@ function _shouldDropEvent(event: Event, options: Partial): ); return true; } + return false; }