diff --git a/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/resource-spans-ignored/assets/script.js b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/resource-spans-ignored/assets/script.js new file mode 100644 index 000000000000..eab583b75a75 --- /dev/null +++ b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/resource-spans-ignored/assets/script.js @@ -0,0 +1,3 @@ +(() => { + // I do nothing. +})(); diff --git a/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/resource-spans-ignored/init.js b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/resource-spans-ignored/init.js new file mode 100644 index 000000000000..70c0b30a03a5 --- /dev/null +++ b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/resource-spans-ignored/init.js @@ -0,0 +1,14 @@ +import * as Sentry from '@sentry/browser'; + +window.Sentry = Sentry; + +Sentry.init({ + dsn: 'https://public@dsn.ingest.sentry.io/1337', + integrations: [ + Sentry.browserTracingIntegration({ + ignoreResourceSpans: ['resource.script'], + idleTimeout: 9000, + }), + ], + tracesSampleRate: 1, +}); diff --git a/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/resource-spans-ignored/test.ts b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/resource-spans-ignored/test.ts new file mode 100644 index 000000000000..4bc9621f8395 --- /dev/null +++ b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/resource-spans-ignored/test.ts @@ -0,0 +1,20 @@ +import type { Route } from '@playwright/test'; +import { expect } from '@playwright/test'; +import type { Event } from '@sentry/core'; +import { sentryTest } from '../../../../utils/fixtures'; +import { getFirstSentryEnvelopeRequest, shouldSkipTracingTest } from '../../../../utils/helpers'; + +sentryTest('should allow specific types of resource spans to be ignored.', async ({ getLocalTestUrl, page }) => { + if (shouldSkipTracingTest()) { + sentryTest.skip(); + } + + await page.route('**/path/to/script.js', (route: Route) => route.fulfill({ path: `${__dirname}/assets/script.js` })); + + const url = await getLocalTestUrl({ testDir: __dirname }); + + const eventData = await getFirstSentryEnvelopeRequest(page, url); + const allSpans = eventData.spans?.filter(({ op }) => op?.startsWith('resource.script')); + + expect(allSpans?.length).toBe(0); +}); diff --git a/packages/browser-utils/src/metrics/browserMetrics.ts b/packages/browser-utils/src/metrics/browserMetrics.ts index 71470a0d8706..d5ca039c65f0 100644 --- a/packages/browser-utils/src/metrics/browserMetrics.ts +++ b/packages/browser-utils/src/metrics/browserMetrics.ts @@ -300,6 +300,13 @@ interface AddPerformanceEntriesOptions { * sent as a standalone span instead. */ recordClsOnPageloadSpan: boolean; + + /** + * Resource spans with `op`s matching strings in the array will not be emitted. + * + * Default: [] + */ + ignoreResourceSpans: Array<'resouce.script' | 'resource.css' | 'resource.img' | 'resource.other' | string>; } /** Add performance related spans to a transaction */ @@ -355,7 +362,15 @@ export function addPerformanceEntries(span: Span, options: AddPerformanceEntries break; } case 'resource': { - _addResourceSpans(span, entry as PerformanceResourceTiming, entry.name, startTime, duration, timeOrigin); + _addResourceSpans( + span, + entry as PerformanceResourceTiming, + entry.name, + startTime, + duration, + timeOrigin, + options.ignoreResourceSpans, + ); break; } // Ignore other entry types. @@ -568,6 +583,7 @@ export function _addResourceSpans( startTime: number, duration: number, timeOrigin: number, + ignoreResourceSpans?: Array, ): void { // we already instrument based on fetch and xhr, so we don't need to // duplicate spans here. @@ -575,6 +591,11 @@ export function _addResourceSpans( return; } + const op = entry.initiatorType ? `resource.${entry.initiatorType}` : 'resource.other'; + if (ignoreResourceSpans?.includes(op)) { + return; + } + const parsedUrl = parseUrl(resourceUrl); const attributes: SpanAttributes = { @@ -616,7 +637,7 @@ export function _addResourceSpans( startAndEndSpan(span, startTimestamp, endTimestamp, { name: resourceUrl.replace(WINDOW.location.origin, ''), - op: entry.initiatorType ? `resource.${entry.initiatorType}` : 'resource.other', + op, attributes, }); } diff --git a/packages/browser-utils/test/browser/browserMetrics.test.ts b/packages/browser-utils/test/browser/browserMetrics.test.ts index 99cf451f824e..87646a690f0e 100644 --- a/packages/browser-utils/test/browser/browserMetrics.test.ts +++ b/packages/browser-utils/test/browser/browserMetrics.test.ts @@ -271,6 +271,53 @@ describe('_addResourceSpans', () => { } }); + it('allows resource spans to be ignored via ignoreResourceSpans', () => { + const spans: Span[] = []; + const ignoredResourceSpans = ['resource.other', 'resource.script']; + + getClient()?.on('spanEnd', span => { + spans.push(span); + }); + + const table = [ + { + initiatorType: undefined, + op: 'resource.other', + }, + { + initiatorType: 'css', + op: 'resource.css', + }, + { + initiatorType: 'css', + op: 'resource.css', + }, + { + initiatorType: 'image', + op: 'resource.image', + }, + { + initiatorType: 'script', + op: 'resource.script', + }, + ]; + for (const row of table) { + const { initiatorType } = row; + const entry = mockPerformanceResourceTiming({ + initiatorType, + nextHopProtocol: 'http/1.1', + }); + _addResourceSpans(span, entry, 'https://example.com/assets/to/me', 123, 234, 465, ignoredResourceSpans); + } + expect(spans).toHaveLength(table.length - ignoredResourceSpans.length); + const spanOps = new Set( + spans.map(s => { + return spanToJSON(s).op; + }), + ); + expect(spanOps).toEqual(new Set(['resource.css', 'resource.image'])); + }); + it('allows for enter size of 0', () => { const spans: Span[] = []; diff --git a/packages/browser/src/tracing/browserTracingIntegration.ts b/packages/browser/src/tracing/browserTracingIntegration.ts index d31fe41742f8..3f38bdb6a8be 100644 --- a/packages/browser/src/tracing/browserTracingIntegration.ts +++ b/packages/browser/src/tracing/browserTracingIntegration.ts @@ -144,6 +144,13 @@ export interface BrowserTracingOptions { */ enableHTTPTimings: boolean; + /** + * Resource spans with `op`s matching strings in the array will not be emitted. + * + * Default: [] + */ + ignoreResourceSpans: Array; + /** * Link the currently started trace to a previous trace (e.g. a prior pageload, navigation or * manually started span). When enabled, this option will allow you to navigate between traces @@ -226,6 +233,7 @@ const DEFAULT_BROWSER_TRACING_OPTIONS: BrowserTracingOptions = { enableLongTask: true, enableLongAnimationFrame: true, enableInp: true, + ignoreResourceSpans: [], linkPreviousTrace: 'in-memory', consistentTraceSampling: false, _experiments: {}, @@ -268,6 +276,7 @@ export const browserTracingIntegration = ((_options: Partial