diff --git a/.size-limit.js b/.size-limit.js index 59ad29c3ccf8..526f2a3a92c7 100644 --- a/.size-limit.js +++ b/.size-limit.js @@ -215,7 +215,7 @@ module.exports = [ import: createImport('init'), ignore: ['$app/stores'], gzip: true, - limit: '41 KB', + limit: '42 KB', }, // Node-Core SDK (ESM) { diff --git a/packages/browser-utils/src/metrics/browserMetrics.ts b/packages/browser-utils/src/metrics/browserMetrics.ts index 2c61408c1d76..ec7213ae4ff6 100644 --- a/packages/browser-utils/src/metrics/browserMetrics.ts +++ b/packages/browser-utils/src/metrics/browserMetrics.ts @@ -425,6 +425,24 @@ export function addPerformanceEntries(span: Span, options: AddPerformanceEntries _measurements = {}; } +/** + * React 19.2+ creates performance.measure entries for component renders. + * We can identify them by the `detail.devtools.track` property being set to 'Components ⚛'. + * see: https://react.dev/reference/dev-tools/react-performance-tracks + * see: https://github.com/facebook/react/blob/06fcc8f380c6a905c7bc18d94453f623cf8cbc81/packages/react-reconciler/src/ReactFiberPerformanceTrack.js#L454-L473 + */ +function isReact19MeasureEntry(entry: PerformanceEntry | null): boolean | void { + if (entry?.entryType !== 'measure') { + return; + } + try { + // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access + return (entry as PerformanceMeasure).detail.devtools.track === 'Components ⚛'; + } catch { + return; + } +} + /** * Create measure related spans. * Exported only for tests. @@ -437,6 +455,10 @@ export function _addMeasureSpans( timeOrigin: number, ignorePerformanceApiSpans: AddPerformanceEntriesOptions['ignorePerformanceApiSpans'], ): void { + if (isReact19MeasureEntry(entry)) { + return; + } + if ( ['mark', 'measure'].includes(entry.entryType) && stringMatchesSomePattern(entry.name, ignorePerformanceApiSpans) @@ -445,6 +467,7 @@ export function _addMeasureSpans( } const navEntry = getNavigationEntry(false); + const requestTime = msToSec(navEntry ? navEntry.requestStart : 0); // Because performance.measure accepts arbitrary timestamps it can produce // spans that happen before the browser even makes a request for the page. diff --git a/packages/browser-utils/test/browser/browserMetrics.test.ts b/packages/browser-utils/test/browser/browserMetrics.test.ts index c734ec326b47..6d6e03fa0643 100644 --- a/packages/browser-utils/test/browser/browserMetrics.test.ts +++ b/packages/browser-utils/test/browser/browserMetrics.test.ts @@ -186,6 +186,75 @@ describe('_addMeasureSpans', () => { ]), ); }); + + it('ignores React 19.2+ measure spans', () => { + const pageloadSpan = new SentrySpan({ op: 'pageload', name: '/', sampled: true }); + const spans: Span[] = []; + + getClient()?.on('spanEnd', span => { + spans.push(span); + }); + + const entries: PerformanceMeasure[] = [ + { + entryType: 'measure', + name: '\u200bLayout', + duration: 0.3, + startTime: 12, + detail: { + devtools: { + track: 'Components ⚛', + }, + }, + toJSON: () => ({ foo: 'bar' }), + }, + { + entryType: 'measure', + name: '\u200bButton', + duration: 0.1, + startTime: 13, + detail: { + devtools: { + track: 'Components ⚛', + }, + }, + toJSON: () => ({}), + }, + { + entryType: 'measure', + name: 'Unmount', + duration: 0.1, + startTime: 14, + detail: { + devtools: { + track: 'Components ⚛', + }, + }, + toJSON: () => ({}), + }, + { + entryType: 'measure', + name: 'my-measurement', + duration: 0, + startTime: 12, + detail: null, + toJSON: () => ({}), + }, + ]; + + const timeOrigin = 100; + const startTime = 23; + const duration = 356; + + entries.forEach(e => { + _addMeasureSpans(pageloadSpan, e, startTime, duration, timeOrigin, []); + }); + + expect(spans).toHaveLength(1); + expect(spans.map(spanToJSON)).toEqual( + expect.arrayContaining([expect.objectContaining({ description: 'my-measurement', op: 'measure' })]), + ); + }); }); describe('_addResourceSpans', () => {