diff --git a/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/ignoreMeasureSpans/init.js b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/ignoreMeasureSpans/init.js new file mode 100644 index 000000000000..409d1e4e7906 --- /dev/null +++ b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/ignoreMeasureSpans/init.js @@ -0,0 +1,19 @@ +import * as Sentry from '@sentry/browser'; + +window.Sentry = Sentry; + +Sentry.init({ + dsn: 'https://public@dsn.ingest.sentry.io/1337', + integrations: [ + Sentry.browserTracingIntegration({ + ignorePerformanceApiSpans: ['measure-ignore', /mark-i/], + idleTimeout: 9000, + }), + ], + tracesSampleRate: 1, +}); + +performance.mark('mark-pass'); +performance.mark('mark-ignore'); +performance.measure('measure-pass'); +performance.measure('measure-ignore'); diff --git a/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/ignoreMeasureSpans/test.ts b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/ignoreMeasureSpans/test.ts new file mode 100644 index 000000000000..6c1348b3185f --- /dev/null +++ b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/ignoreMeasureSpans/test.ts @@ -0,0 +1,47 @@ +import type { Route } from '@playwright/test'; +import { expect } from '@playwright/test'; +import { sentryTest } from '../../../../utils/fixtures'; +import { envelopeRequestParser, shouldSkipTracingTest, waitForTransactionRequest } from '../../../../utils/helpers'; + +sentryTest( + 'should ignore mark and measure spans that match `ignorePerformanceApiSpans`', + 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 transactionRequestPromise = waitForTransactionRequest( + page, + evt => evt.type === 'transaction' && evt.contexts?.trace?.op === 'pageload', + ); + + await page.goto(url); + + const transactionEvent = envelopeRequestParser(await transactionRequestPromise); + const markAndMeasureSpans = transactionEvent.spans?.filter(({ op }) => op && ['mark', 'measure'].includes(op)); + + expect(markAndMeasureSpans?.length).toBe(3); + expect(markAndMeasureSpans).toEqual( + expect.arrayContaining([ + expect.objectContaining({ + description: 'mark-pass', + op: 'mark', + }), + expect.objectContaining({ + description: 'measure-pass', + op: 'measure', + }), + expect.objectContaining({ + description: 'sentry-tracing-init', + op: 'mark', + }), + ]), + ); + }, +); diff --git a/packages/browser-utils/src/metrics/browserMetrics.ts b/packages/browser-utils/src/metrics/browserMetrics.ts index d5ca039c65f0..7695802941a6 100644 --- a/packages/browser-utils/src/metrics/browserMetrics.ts +++ b/packages/browser-utils/src/metrics/browserMetrics.ts @@ -9,6 +9,7 @@ import { SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN, setMeasurement, spanToJSON, + stringMatchesSomePattern, } from '@sentry/core'; import { WINDOW } from '../types'; import { trackClsAsStandaloneSpan } from './cls'; @@ -307,6 +308,15 @@ interface AddPerformanceEntriesOptions { * Default: [] */ ignoreResourceSpans: Array<'resouce.script' | 'resource.css' | 'resource.img' | 'resource.other' | string>; + + /** + * Performance spans created from browser Performance APIs, + * `performance.mark(...)` nand `performance.measure(...)` + * with `name`s matching strings in the array will not be emitted. + * + * Default: [] + */ + ignorePerformanceApiSpans: Array; } /** Add performance related spans to a transaction */ @@ -346,7 +356,7 @@ export function addPerformanceEntries(span: Span, options: AddPerformanceEntries case 'mark': case 'paint': case 'measure': { - _addMeasureSpans(span, entry, startTime, duration, timeOrigin); + _addMeasureSpans(span, entry, startTime, duration, timeOrigin, options.ignorePerformanceApiSpans); // capture web vitals const firstHidden = getVisibilityWatcher(); @@ -440,7 +450,15 @@ export function _addMeasureSpans( startTime: number, duration: number, timeOrigin: number, + ignorePerformanceApiSpans: AddPerformanceEntriesOptions['ignorePerformanceApiSpans'], ): void { + if ( + ['mark', 'measure'].includes(entry.entryType) && + stringMatchesSomePattern(entry.name, ignorePerformanceApiSpans) + ) { + return; + } + const navEntry = getNavigationEntry(false); const requestTime = msToSec(navEntry ? navEntry.requestStart : 0); // Because performance.measure accepts arbitrary timestamps it can produce diff --git a/packages/browser-utils/test/browser/browserMetrics.test.ts b/packages/browser-utils/test/browser/browserMetrics.test.ts index 87646a690f0e..50dcfd65a528 100644 --- a/packages/browser-utils/test/browser/browserMetrics.test.ts +++ b/packages/browser-utils/test/browser/browserMetrics.test.ts @@ -76,7 +76,7 @@ describe('_addMeasureSpans', () => { const startTime = 23; const duration = 356; - _addMeasureSpans(span, entry, startTime, duration, timeOrigin); + _addMeasureSpans(span, entry, startTime, duration, timeOrigin, []); expect(spans).toHaveLength(1); expect(spanToJSON(spans[0]!)).toEqual( @@ -112,10 +112,75 @@ describe('_addMeasureSpans', () => { const startTime = 23; const duration = -50; - _addMeasureSpans(span, entry, startTime, duration, timeOrigin); + _addMeasureSpans(span, entry, startTime, duration, timeOrigin, []); expect(spans).toHaveLength(0); }); + + it('ignores performance spans that match ignorePerformanceApiSpans', () => { + const pageloadSpan = new SentrySpan({ op: 'pageload', name: '/', sampled: true }); + const spans: Span[] = []; + + getClient()?.on('spanEnd', span => { + spans.push(span); + }); + + const entries: PerformanceEntry[] = [ + { + entryType: 'measure', + name: 'measure-pass', + duration: 10, + startTime: 12, + toJSON: () => ({}), + }, + { + entryType: 'measure', + name: 'measure-ignore', + duration: 10, + startTime: 12, + toJSON: () => ({}), + }, + { + entryType: 'mark', + name: 'mark-pass', + duration: 0, + startTime: 12, + toJSON: () => ({}), + }, + { + entryType: 'mark', + name: 'mark-ignore', + duration: 0, + startTime: 12, + toJSON: () => ({}), + }, + { + entryType: 'paint', + name: 'mark-ignore', + duration: 0, + startTime: 12, + toJSON: () => ({}), + }, + ]; + + const timeOrigin = 100; + const startTime = 23; + const duration = 356; + + entries.forEach(e => { + _addMeasureSpans(pageloadSpan, e, startTime, duration, timeOrigin, ['measure-i', /mark-ign/]); + }); + + expect(spans).toHaveLength(3); + expect(spans.map(spanToJSON)).toEqual( + expect.arrayContaining([ + expect.objectContaining({ description: 'measure-pass', op: 'measure' }), + expect.objectContaining({ description: 'mark-pass', op: 'mark' }), + // name matches but type is not (mark|measure) => should not be ignored + expect.objectContaining({ description: 'mark-ignore', op: 'paint' }), + ]), + ); + }); }); describe('_addResourceSpans', () => { diff --git a/packages/browser/src/tracing/browserTracingIntegration.ts b/packages/browser/src/tracing/browserTracingIntegration.ts index 3f38bdb6a8be..1ba733ac4ca8 100644 --- a/packages/browser/src/tracing/browserTracingIntegration.ts +++ b/packages/browser/src/tracing/browserTracingIntegration.ts @@ -149,7 +149,42 @@ export interface BrowserTracingOptions { * * Default: [] */ - ignoreResourceSpans: Array; + ignoreResourceSpans: Array<'resouce.script' | 'resource.css' | 'resource.img' | 'resource.other' | string>; + + /** + * Spans created from the following browser Performance APIs, + * + * - [`performance.mark(...)`](https://developer.mozilla.org/en-US/docs/Web/API/Performance/mark) + * - [`performance.measure(...)`](https://developer.mozilla.org/en-US/docs/Web/API/Performance/measure) + * + * will not be emitted if their names match strings in this array. + * + * This is useful, if you come across `mark` or `measure` spans in your Sentry traces + * that you want to ignore. For example, sometimes, browser extensions or libraries + * emit these entries on their own, which might not be relevant to your application. + * + * * @example + * ```ts + * Sentry.init({ + * integrations: [ + * Sentry.browserTracingIntegration({ + * ignorePerformanceApiSpans: ['myMeasurement', /myMark/], + * }), + * ], + * }); + * + * // no spans will be created for these: + * performance.mark('myMark'); + * performance.measure('myMeasurement'); + * + * // spans will be created for these: + * performance.mark('authenticated'); + * performance.measure('input-duration', ...); + * ``` + * + * Default: [] - By default, all `mark` and `measure` entries are sent as spans. + */ + ignorePerformanceApiSpans: Array; /** * Link the currently started trace to a previous trace (e.g. a prior pageload, navigation or @@ -234,6 +269,7 @@ const DEFAULT_BROWSER_TRACING_OPTIONS: BrowserTracingOptions = { enableLongAnimationFrame: true, enableInp: true, ignoreResourceSpans: [], + ignorePerformanceApiSpans: [], linkPreviousTrace: 'in-memory', consistentTraceSampling: false, _experiments: {}, @@ -277,6 +313,7 @@ export const browserTracingIntegration = ((_options: Partial