From 0efbab71b8080568f75388b660f9b2fd14fd0e55 Mon Sep 17 00:00:00 2001 From: Lukas Stracke Date: Mon, 2 Jun 2025 13:52:48 +0200 Subject: [PATCH 1/4] feat(browser): Add option to ignore `mark` and `measure` spans --- .../ignoreMeasureSpans/init.js | 19 +++++ .../ignoreMeasureSpans/test.ts | 47 +++++++++++ .../src/metrics/browserMetrics.ts | 16 +++- .../test/browser/browserMetrics.test.ts | 77 ++++++++++++++++++- .../src/tracing/browserTracingIntegration.ts | 25 +++++- 5 files changed, 178 insertions(+), 6 deletions(-) create mode 100644 dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/ignoreMeasureSpans/init.js create mode 100644 dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/ignoreMeasureSpans/test.ts 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..78bfb44e9475 --- /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({ + ignoreMeasureSpans: ['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..c93f82adeb4f --- /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 `ignoreMeasureSpans`', + 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..89bcba9264ca 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,14 @@ interface AddPerformanceEntriesOptions { * Default: [] */ ignoreResourceSpans: Array<'resouce.script' | 'resource.css' | 'resource.img' | 'resource.other' | string>; + + /** + * Performance spans created from `performance.mark(...)` or `performance.measure(...)` + * with `name`s matching strings in the array will not be emitted. + * + * Default: [] + */ + ignoreMeasureSpans: Array; } /** Add performance related spans to a transaction */ @@ -346,7 +355,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.ignoreMeasureSpans); // capture web vitals const firstHidden = getVisibilityWatcher(); @@ -440,7 +449,12 @@ export function _addMeasureSpans( startTime: number, duration: number, timeOrigin: number, + ignoreMeasureSpans: Array, ): void { + if (['mark', 'measure'].includes(entry.entryType) && stringMatchesSomePattern(entry.name, ignoreMeasureSpans)) { + 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..b94d20d9037a 100644 --- a/packages/browser-utils/test/browser/browserMetrics.test.ts +++ b/packages/browser-utils/test/browser/browserMetrics.test.ts @@ -10,7 +10,12 @@ import { spanToJSON, } from '@sentry/core'; import { afterAll, beforeAll, beforeEach, describe, expect, it } from 'vitest'; -import { _addMeasureSpans, _addNavigationSpans, _addResourceSpans } from '../../src/metrics/browserMetrics'; +import { + _addMeasureSpans, + _addNavigationSpans, + _addResourceSpans, + addPerformanceEntries, +} from '../../src/metrics/browserMetrics'; import { WINDOW } from '../../src/types'; import { getDefaultClientOptions, TestClient } from '../utils/TestClient'; @@ -76,7 +81,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 +117,76 @@ 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 ignoreMeasureSpans', () => { + 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 => { + // full match ('measure-ignore') and partial match ('mark-i') cause the span to be ignored + _addMeasureSpans(pageloadSpan, e, startTime, duration, timeOrigin, ['measure-ignore', 'mark-i']); + }); + + 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..20d781606a65 100644 --- a/packages/browser/src/tracing/browserTracingIntegration.ts +++ b/packages/browser/src/tracing/browserTracingIntegration.ts @@ -149,7 +149,22 @@ export interface BrowserTracingOptions { * * Default: [] */ - ignoreResourceSpans: Array; + ignoreResourceSpans: Array<'resouce.script' | 'resource.css' | 'resource.img' | 'resource.other' | string>; + + /** + * Spans created from + * [`performance.mark(...)`](https://developer.mozilla.org/en-US/docs/Web/API/Performance/mark) + * and + * [`performance.measure(...)`](https://developer.mozilla.org/en-US/docs/Web/API/Performance/measure) + * calls 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. + * + * Default: [] - By default, all `mark` and `measure` entries are sent as spans. + */ + ignoreMeasureSpans: Array; /** * Link the currently started trace to a previous trace (e.g. a prior pageload, navigation or @@ -234,6 +249,7 @@ const DEFAULT_BROWSER_TRACING_OPTIONS: BrowserTracingOptions = { enableLongAnimationFrame: true, enableInp: true, ignoreResourceSpans: [], + ignoreMeasureSpans: [], linkPreviousTrace: 'in-memory', consistentTraceSampling: false, _experiments: {}, @@ -277,6 +293,7 @@ export const browserTracingIntegration = ((_options: Partial Date: Mon, 2 Jun 2025 13:57:28 +0200 Subject: [PATCH 2/4] allow RegExp --- .../browserTracingIntegration/ignoreMeasureSpans/init.js | 2 +- packages/browser-utils/src/metrics/browserMetrics.ts | 4 ++-- packages/browser-utils/test/browser/browserMetrics.test.ts | 3 +-- packages/browser/src/tracing/browserTracingIntegration.ts | 2 +- 4 files changed, 5 insertions(+), 6 deletions(-) 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 index 78bfb44e9475..bee19ceb4c50 100644 --- a/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/ignoreMeasureSpans/init.js +++ b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/ignoreMeasureSpans/init.js @@ -6,7 +6,7 @@ Sentry.init({ dsn: 'https://public@dsn.ingest.sentry.io/1337', integrations: [ Sentry.browserTracingIntegration({ - ignoreMeasureSpans: ['measure-ignore', 'mark-i'], + ignoreMeasureSpans: ['measure-ignore', /mark-i/], idleTimeout: 9000, }), ], diff --git a/packages/browser-utils/src/metrics/browserMetrics.ts b/packages/browser-utils/src/metrics/browserMetrics.ts index 89bcba9264ca..cf4c02c69b9e 100644 --- a/packages/browser-utils/src/metrics/browserMetrics.ts +++ b/packages/browser-utils/src/metrics/browserMetrics.ts @@ -315,7 +315,7 @@ interface AddPerformanceEntriesOptions { * * Default: [] */ - ignoreMeasureSpans: Array; + ignoreMeasureSpans: Array; } /** Add performance related spans to a transaction */ @@ -449,7 +449,7 @@ export function _addMeasureSpans( startTime: number, duration: number, timeOrigin: number, - ignoreMeasureSpans: Array, + ignoreMeasureSpans: AddPerformanceEntriesOptions['ignoreMeasureSpans'], ): void { if (['mark', 'measure'].includes(entry.entryType) && stringMatchesSomePattern(entry.name, ignoreMeasureSpans)) { return; diff --git a/packages/browser-utils/test/browser/browserMetrics.test.ts b/packages/browser-utils/test/browser/browserMetrics.test.ts index b94d20d9037a..7f6224b338fa 100644 --- a/packages/browser-utils/test/browser/browserMetrics.test.ts +++ b/packages/browser-utils/test/browser/browserMetrics.test.ts @@ -173,8 +173,7 @@ describe('_addMeasureSpans', () => { const duration = 356; entries.forEach(e => { - // full match ('measure-ignore') and partial match ('mark-i') cause the span to be ignored - _addMeasureSpans(pageloadSpan, e, startTime, duration, timeOrigin, ['measure-ignore', 'mark-i']); + _addMeasureSpans(pageloadSpan, e, startTime, duration, timeOrigin, ['measure-i', /mark-ign/]); }); expect(spans).toHaveLength(3); diff --git a/packages/browser/src/tracing/browserTracingIntegration.ts b/packages/browser/src/tracing/browserTracingIntegration.ts index 20d781606a65..3874bf71e4b8 100644 --- a/packages/browser/src/tracing/browserTracingIntegration.ts +++ b/packages/browser/src/tracing/browserTracingIntegration.ts @@ -164,7 +164,7 @@ export interface BrowserTracingOptions { * * Default: [] - By default, all `mark` and `measure` entries are sent as spans. */ - ignoreMeasureSpans: Array; + ignoreMeasureSpans: Array; /** * Link the currently started trace to a previous trace (e.g. a prior pageload, navigation or From 6f9f30649b0538b3ac79e092198714e532d53db1 Mon Sep 17 00:00:00 2001 From: Lukas Stracke Date: Mon, 2 Jun 2025 14:48:18 +0200 Subject: [PATCH 3/4] lint --- packages/browser-utils/test/browser/browserMetrics.test.ts | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/packages/browser-utils/test/browser/browserMetrics.test.ts b/packages/browser-utils/test/browser/browserMetrics.test.ts index 7f6224b338fa..2bfbf26e2ee4 100644 --- a/packages/browser-utils/test/browser/browserMetrics.test.ts +++ b/packages/browser-utils/test/browser/browserMetrics.test.ts @@ -10,12 +10,7 @@ import { spanToJSON, } from '@sentry/core'; import { afterAll, beforeAll, beforeEach, describe, expect, it } from 'vitest'; -import { - _addMeasureSpans, - _addNavigationSpans, - _addResourceSpans, - addPerformanceEntries, -} from '../../src/metrics/browserMetrics'; +import { _addMeasureSpans, _addNavigationSpans, _addResourceSpans } from '../../src/metrics/browserMetrics'; import { WINDOW } from '../../src/types'; import { getDefaultClientOptions, TestClient } from '../utils/TestClient'; From ae2cfb0c049cefb2c83abf8b36b6dcccf42dbdff Mon Sep 17 00:00:00 2001 From: Lukas Stracke Date: Mon, 2 Jun 2025 17:11:53 +0200 Subject: [PATCH 4/4] rename to ignorePerformanceApiSpans --- .../ignoreMeasureSpans/init.js | 2 +- .../ignoreMeasureSpans/test.ts | 2 +- .../src/metrics/browserMetrics.ts | 14 ++++--- .../test/browser/browserMetrics.test.ts | 2 +- .../src/tracing/browserTracingIntegration.ts | 38 ++++++++++++++----- 5 files changed, 41 insertions(+), 17 deletions(-) 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 index bee19ceb4c50..409d1e4e7906 100644 --- a/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/ignoreMeasureSpans/init.js +++ b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/ignoreMeasureSpans/init.js @@ -6,7 +6,7 @@ Sentry.init({ dsn: 'https://public@dsn.ingest.sentry.io/1337', integrations: [ Sentry.browserTracingIntegration({ - ignoreMeasureSpans: ['measure-ignore', /mark-i/], + ignorePerformanceApiSpans: ['measure-ignore', /mark-i/], idleTimeout: 9000, }), ], 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 index c93f82adeb4f..6c1348b3185f 100644 --- a/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/ignoreMeasureSpans/test.ts +++ b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/ignoreMeasureSpans/test.ts @@ -4,7 +4,7 @@ import { sentryTest } from '../../../../utils/fixtures'; import { envelopeRequestParser, shouldSkipTracingTest, waitForTransactionRequest } from '../../../../utils/helpers'; sentryTest( - 'should ignore mark and measure spans that match `ignoreMeasureSpans`', + 'should ignore mark and measure spans that match `ignorePerformanceApiSpans`', async ({ getLocalTestUrl, page }) => { if (shouldSkipTracingTest()) { sentryTest.skip(); diff --git a/packages/browser-utils/src/metrics/browserMetrics.ts b/packages/browser-utils/src/metrics/browserMetrics.ts index cf4c02c69b9e..7695802941a6 100644 --- a/packages/browser-utils/src/metrics/browserMetrics.ts +++ b/packages/browser-utils/src/metrics/browserMetrics.ts @@ -310,12 +310,13 @@ interface AddPerformanceEntriesOptions { ignoreResourceSpans: Array<'resouce.script' | 'resource.css' | 'resource.img' | 'resource.other' | string>; /** - * Performance spans created from `performance.mark(...)` or `performance.measure(...)` + * 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: [] */ - ignoreMeasureSpans: Array; + ignorePerformanceApiSpans: Array; } /** Add performance related spans to a transaction */ @@ -355,7 +356,7 @@ export function addPerformanceEntries(span: Span, options: AddPerformanceEntries case 'mark': case 'paint': case 'measure': { - _addMeasureSpans(span, entry, startTime, duration, timeOrigin, options.ignoreMeasureSpans); + _addMeasureSpans(span, entry, startTime, duration, timeOrigin, options.ignorePerformanceApiSpans); // capture web vitals const firstHidden = getVisibilityWatcher(); @@ -449,9 +450,12 @@ export function _addMeasureSpans( startTime: number, duration: number, timeOrigin: number, - ignoreMeasureSpans: AddPerformanceEntriesOptions['ignoreMeasureSpans'], + ignorePerformanceApiSpans: AddPerformanceEntriesOptions['ignorePerformanceApiSpans'], ): void { - if (['mark', 'measure'].includes(entry.entryType) && stringMatchesSomePattern(entry.name, ignoreMeasureSpans)) { + if ( + ['mark', 'measure'].includes(entry.entryType) && + stringMatchesSomePattern(entry.name, ignorePerformanceApiSpans) + ) { return; } diff --git a/packages/browser-utils/test/browser/browserMetrics.test.ts b/packages/browser-utils/test/browser/browserMetrics.test.ts index 2bfbf26e2ee4..50dcfd65a528 100644 --- a/packages/browser-utils/test/browser/browserMetrics.test.ts +++ b/packages/browser-utils/test/browser/browserMetrics.test.ts @@ -117,7 +117,7 @@ describe('_addMeasureSpans', () => { expect(spans).toHaveLength(0); }); - it('ignores performance spans that match ignoreMeasureSpans', () => { + it('ignores performance spans that match ignorePerformanceApiSpans', () => { const pageloadSpan = new SentrySpan({ op: 'pageload', name: '/', sampled: true }); const spans: Span[] = []; diff --git a/packages/browser/src/tracing/browserTracingIntegration.ts b/packages/browser/src/tracing/browserTracingIntegration.ts index 3874bf71e4b8..1ba733ac4ca8 100644 --- a/packages/browser/src/tracing/browserTracingIntegration.ts +++ b/packages/browser/src/tracing/browserTracingIntegration.ts @@ -152,19 +152,39 @@ export interface BrowserTracingOptions { ignoreResourceSpans: Array<'resouce.script' | 'resource.css' | 'resource.img' | 'resource.other' | string>; /** - * Spans created from - * [`performance.mark(...)`](https://developer.mozilla.org/en-US/docs/Web/API/Performance/mark) - * and - * [`performance.measure(...)`](https://developer.mozilla.org/en-US/docs/Web/API/Performance/measure) - * calls will not be emitted if their names match strings in this array. + * 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. */ - ignoreMeasureSpans: Array; + ignorePerformanceApiSpans: Array; /** * Link the currently started trace to a previous trace (e.g. a prior pageload, navigation or @@ -249,7 +269,7 @@ const DEFAULT_BROWSER_TRACING_OPTIONS: BrowserTracingOptions = { enableLongAnimationFrame: true, enableInp: true, ignoreResourceSpans: [], - ignoreMeasureSpans: [], + ignorePerformanceApiSpans: [], linkPreviousTrace: 'in-memory', consistentTraceSampling: false, _experiments: {}, @@ -293,7 +313,7 @@ export const browserTracingIntegration = ((_options: Partial