From c376629b336bcb5acf0d53a4157944b146f1fc70 Mon Sep 17 00:00:00 2001 From: Martin Sonnberger Date: Thu, 23 Jul 2026 13:33:34 +0200 Subject: [PATCH 01/12] feat(browser)!: Extract `performance.{mark,measure}` spans into new `userTimingSpansIntegration` --- MIGRATION.md | 23 ++ .../ignoreMeasureSpans/test.ts | 47 ---- .../user-timing-spans-disabled/init.js | 12 + .../user-timing-spans-disabled/test.ts | 21 ++ .../user-timing-spans-streamed/init.js | 17 ++ .../user-timing-spans-streamed/test.ts | 22 ++ .../user-timing-spans}/init.js | 5 +- .../tracing/metrics/user-timing-spans/test.ts | 39 +++ packages/browser-utils/src/index.ts | 2 + .../src/metrics/browserMetrics.ts | 144 ++--------- .../browser-utils/src/metrics/instrument.ts | 2 + .../src/metrics/userTimingSpans.ts | 198 +++++++++++++++ .../src/metrics/web-vitals/lib/observe.ts | 2 + .../test/browser/browserMetrics.test.ts | 211 ---------------- .../test/metrics/userTimingSpans.test.ts | 234 ++++++++++++++++++ packages/browser/src/index.ts | 1 + .../src/tracing/browserTracingIntegration.ts | 38 --- .../tracing/browserTracingIntegration.test.ts | 10 +- 18 files changed, 593 insertions(+), 435 deletions(-) delete mode 100644 dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/ignoreMeasureSpans/test.ts create mode 100644 dev-packages/browser-integration-tests/suites/tracing/metrics/user-timing-spans-disabled/init.js create mode 100644 dev-packages/browser-integration-tests/suites/tracing/metrics/user-timing-spans-disabled/test.ts create mode 100644 dev-packages/browser-integration-tests/suites/tracing/metrics/user-timing-spans-streamed/init.js create mode 100644 dev-packages/browser-integration-tests/suites/tracing/metrics/user-timing-spans-streamed/test.ts rename dev-packages/browser-integration-tests/suites/tracing/{browserTracingIntegration/ignoreMeasureSpans => metrics/user-timing-spans}/init.js (74%) create mode 100644 dev-packages/browser-integration-tests/suites/tracing/metrics/user-timing-spans/test.ts create mode 100644 packages/browser-utils/src/metrics/userTimingSpans.ts create mode 100644 packages/browser-utils/test/metrics/userTimingSpans.test.ts diff --git a/MIGRATION.md b/MIGRATION.md index 006380d9f4e0..808c62d50cfe 100644 --- a/MIGRATION.md +++ b/MIGRATION.md @@ -345,6 +345,29 @@ Sentry.init({ - The experimental `_experiments.enableStandaloneClsSpans` and `_experiments.enableStandaloneLcpSpans` options were removed from both `browserTracingIntegration` and `webVitalsIntegration`. CLS and LCP are no longer configurable: they are recorded as measurements on the pageload span, unless span streaming is enabled (`traceLifecycle: 'stream'`), in which case they are sent as dedicated spans. - INP is now always sent as a web vital span (streamed when span streaming is enabled, standalone otherwise) that carries its value as a `browser.web_vital.inp.value` attribute. Previously, with span streaming disabled, INP was sent as a standalone span that carried its value as a span measurement. +- `browserTracingIntegration` no longer captures spans created by `performance.mark()` and `performance.measure()` by default. Add `userTimingSpansIntegration()` to continue capturing them. The `ignorePerformanceApiSpans` option moved to the new integration as `ignore`. + +```js +// before +Sentry.init({ + integrations: [ + Sentry.browserTracingIntegration({ + ignorePerformanceApiSpans: ['third-party-mark'], + }), + ], +}); + +// after +Sentry.init({ + integrations: [ + Sentry.browserTracingIntegration(), + Sentry.userTimingSpansIntegration({ + ignore: ['third-party-mark'], + }), + ], +}); +``` + ### `@sentry/node` / Server-side SDKs - `SentryContextManager` is no longer exported. It is no longer needed now that Sentry does not set up OpenTelemetry by default. 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 deleted file mode 100644 index 6c1348b3185f..000000000000 --- a/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/ignoreMeasureSpans/test.ts +++ /dev/null @@ -1,47 +0,0 @@ -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/dev-packages/browser-integration-tests/suites/tracing/metrics/user-timing-spans-disabled/init.js b/dev-packages/browser-integration-tests/suites/tracing/metrics/user-timing-spans-disabled/init.js new file mode 100644 index 000000000000..d9b15c028d89 --- /dev/null +++ b/dev-packages/browser-integration-tests/suites/tracing/metrics/user-timing-spans-disabled/init.js @@ -0,0 +1,12 @@ +import * as Sentry from '@sentry/browser'; + +window.Sentry = Sentry; + +Sentry.init({ + dsn: 'https://public@dsn.ingest.sentry.io/1337', + integrations: [Sentry.browserTracingIntegration()], + tracesSampleRate: 1, +}); + +performance.mark('app-ready'); +performance.measure('app-initialization'); diff --git a/dev-packages/browser-integration-tests/suites/tracing/metrics/user-timing-spans-disabled/test.ts b/dev-packages/browser-integration-tests/suites/tracing/metrics/user-timing-spans-disabled/test.ts new file mode 100644 index 000000000000..3021d063c2d5 --- /dev/null +++ b/dev-packages/browser-integration-tests/suites/tracing/metrics/user-timing-spans-disabled/test.ts @@ -0,0 +1,21 @@ +import { expect } from '@playwright/test'; +import { sentryTest } from '../../../../utils/fixtures'; +import { envelopeRequestParser, shouldSkipTracingTest, waitForTransactionRequest } from '../../../../utils/helpers'; + +sentryTest('does not capture mark and measure spans by default', async ({ getLocalTestUrl, page }) => { + if (shouldSkipTracingTest()) { + sentryTest.skip(); + } + + const url = await getLocalTestUrl({ testDir: __dirname }); + const transactionRequestPromise = waitForTransactionRequest( + page, + event => event.type === 'transaction' && event.contexts?.trace?.op === 'pageload', + ); + + await page.goto(url); + + const transactionEvent = envelopeRequestParser(await transactionRequestPromise); + const userTimingSpans = transactionEvent.spans?.filter(({ op }) => op === 'mark' || op === 'measure'); + expect(userTimingSpans).toHaveLength(0); +}); diff --git a/dev-packages/browser-integration-tests/suites/tracing/metrics/user-timing-spans-streamed/init.js b/dev-packages/browser-integration-tests/suites/tracing/metrics/user-timing-spans-streamed/init.js new file mode 100644 index 000000000000..6d79eef2be33 --- /dev/null +++ b/dev-packages/browser-integration-tests/suites/tracing/metrics/user-timing-spans-streamed/init.js @@ -0,0 +1,17 @@ +import * as Sentry from '@sentry/browser'; + +window.Sentry = Sentry; + +Sentry.init({ + dsn: 'https://public@dsn.ingest.sentry.io/1337', + integrations: [ + Sentry.browserTracingIntegration(), + Sentry.userTimingSpansIntegration(), + Sentry.spanStreamingIntegration(), + ], + traceLifecycle: 'stream', + tracesSampleRate: 1, +}); + +performance.mark('app-ready'); +performance.measure('app-initialization'); diff --git a/dev-packages/browser-integration-tests/suites/tracing/metrics/user-timing-spans-streamed/test.ts b/dev-packages/browser-integration-tests/suites/tracing/metrics/user-timing-spans-streamed/test.ts new file mode 100644 index 000000000000..aa39b8203ba8 --- /dev/null +++ b/dev-packages/browser-integration-tests/suites/tracing/metrics/user-timing-spans-streamed/test.ts @@ -0,0 +1,22 @@ +import { expect } from '@playwright/test'; +import { sentryTest } from '../../../../utils/fixtures'; +import { shouldSkipTracingTest } from '../../../../utils/helpers'; +import { getSpanOp, waitForStreamedSpans } from '../../../../utils/spanUtils'; + +sentryTest('captures each mark and measure once with span streaming', async ({ getLocalTestUrl, page }) => { + sentryTest.skip(shouldSkipTracingTest()); + + const url = await getLocalTestUrl({ testDir: __dirname }); + const spansPromise = waitForStreamedSpans(page, spans => spans.some(span => getSpanOp(span) === 'pageload')); + + await page.goto(url); + + const spans = await spansPromise; + const userTimingSpans = spans.filter(span => ['mark', 'measure'].includes(getSpanOp(span) ?? '')); + expect(userTimingSpans).toHaveLength(3); + expect(userTimingSpans.map(span => span.name).sort()).toEqual([ + 'app-initialization', + 'app-ready', + 'sentry-tracing-init', + ]); +}); diff --git a/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/ignoreMeasureSpans/init.js b/dev-packages/browser-integration-tests/suites/tracing/metrics/user-timing-spans/init.js similarity index 74% rename from dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/ignoreMeasureSpans/init.js rename to dev-packages/browser-integration-tests/suites/tracing/metrics/user-timing-spans/init.js index 71956ea21570..4299b62e1cca 100644 --- a/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/ignoreMeasureSpans/init.js +++ b/dev-packages/browser-integration-tests/suites/tracing/metrics/user-timing-spans/init.js @@ -6,9 +6,8 @@ Sentry.init({ traceLifecycle: 'static', dsn: 'https://public@dsn.ingest.sentry.io/1337', integrations: [ - Sentry.browserTracingIntegration({ - ignorePerformanceApiSpans: ['measure-ignore', /mark-i/], - }), + Sentry.browserTracingIntegration(), + Sentry.userTimingSpansIntegration({ ignore: ['measure-ignore', /mark-i/] }), ], tracesSampleRate: 1, }); diff --git a/dev-packages/browser-integration-tests/suites/tracing/metrics/user-timing-spans/test.ts b/dev-packages/browser-integration-tests/suites/tracing/metrics/user-timing-spans/test.ts new file mode 100644 index 000000000000..67527d31b57e --- /dev/null +++ b/dev-packages/browser-integration-tests/suites/tracing/metrics/user-timing-spans/test.ts @@ -0,0 +1,39 @@ +import { expect } from '@playwright/test'; +import { sentryTest } from '../../../../utils/fixtures'; +import { envelopeRequestParser, shouldSkipTracingTest, waitForTransactionRequest } from '../../../../utils/helpers'; + +sentryTest('captures non-ignored mark and measure spans', async ({ getLocalTestUrl, page }) => { + if (shouldSkipTracingTest()) { + sentryTest.skip(); + } + + 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/index.ts b/packages/browser-utils/src/index.ts index 8e839a99251f..c3008d7524c5 100644 --- a/packages/browser-utils/src/index.ts +++ b/packages/browser-utils/src/index.ts @@ -20,6 +20,8 @@ export { // eslint-disable-next-line typescript/no-deprecated export { elementTimingIntegration, startTrackingElementTiming } from './metrics/elementTiming'; +export { userTimingSpansIntegration } from './metrics/userTimingSpans'; + export { extractNetworkProtocol } from './metrics/utils'; export { trackClsAsSpan, trackInpAsSpan, trackLcpAsSpan } from './metrics/webVitalSpans'; diff --git a/packages/browser-utils/src/metrics/browserMetrics.ts b/packages/browser-utils/src/metrics/browserMetrics.ts index 902e1abd135b..dcac15fe9e0b 100644 --- a/packages/browser-utils/src/metrics/browserMetrics.ts +++ b/packages/browser-utils/src/metrics/browserMetrics.ts @@ -1,16 +1,14 @@ /* eslint-disable max-lines */ -import type { Client, Measurements, Span, SpanAttributes, SpanAttributeValue, StartSpanOptions } from '@sentry/core'; +import type { Client, Measurements, Span, SpanAttributes, StartSpanOptions } from '@sentry/core'; import { browserPerformanceTimeOrigin, debug, getActiveSpan, getComponentName, - isPrimitive, parseUrl, SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN, setMeasurement, spanToJSON, - stringMatchesSomePattern, } from '@sentry/core'; import { htmlTreeAsString } from '../htmlTreeAsString'; import { WINDOW } from '../types'; @@ -305,15 +303,6 @@ interface AddPerformanceEntriesOptions { */ 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; - /** * Whether span streaming is enabled. */ @@ -354,7 +343,7 @@ export function addPerformanceEntries(span: Span, options: AddPerformanceEntries return; } - const { spanStreamingEnabled, ignorePerformanceApiSpans, ignoreResourceSpans } = options; + const { spanStreamingEnabled, ignoreResourceSpans } = options; const timeOrigin = msToSec(origin); @@ -381,10 +370,8 @@ export function addPerformanceEntries(span: Span, options: AddPerformanceEntries _addNavigationSpans(span, entry as PerformanceNavigationTiming, timeOrigin); break; } - case 'mark': - case 'paint': - case 'measure': { - _addMeasureSpans(span, entry, startTime, duration, timeOrigin, ignorePerformanceApiSpans); + case 'paint': { + _addPaintSpan(span, entry, startTime, duration, timeOrigin); break; } case 'resource': { @@ -489,126 +476,23 @@ function resetWebVitalState(): void { _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. - */ -export function _addMeasureSpans( +/** Create a span for a browser paint performance entry. */ +function _addPaintSpan( span: Span, entry: PerformanceEntry, startTime: number, duration: number, timeOrigin: number, - ignorePerformanceApiSpans: AddPerformanceEntriesOptions['ignorePerformanceApiSpans'], ): void { - if (isReact19MeasureEntry(entry)) { - return; - } - - 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 - // spans that happen before the browser even makes a request for the page. - // - // An example of this is the automatically generated Next.js-before-hydration - // spans created by the Next.js framework. - // - // To prevent this we will pin the start timestamp to the request start time - // This does make duration inaccurate, so if this does happen, we will add - // an attribute to the span - const measureStartTimestamp = timeOrigin + Math.max(startTime, requestTime); - const startTimeStamp = timeOrigin + startTime; - const measureEndTimestamp = startTimeStamp + duration; - - const attributes: SpanAttributes = { - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.resource.browser.metrics', - }; - - if (measureStartTimestamp !== startTimeStamp) { - attributes['sentry.browser.measure_happened_before_request'] = true; - attributes['sentry.browser.measure_start_time'] = measureStartTimestamp; - } - - _addDetailToSpanAttributes(attributes, entry as PerformanceMeasure); - - // Measurements from third parties can be off, which would create invalid spans, dropping transactions in the process. - if (measureStartTimestamp <= measureEndTimestamp) { - startAndEndSpan(span, measureStartTimestamp, measureEndTimestamp, { - name: entry.name, - op: entry.entryType, - attributes, - }); - } -} - -function _addDetailToSpanAttributes(attributes: SpanAttributes, performanceMeasure: PerformanceMeasure): void { - try { - // Accessing detail might throw in some browsers (e.g., Firefox) due to security restrictions - const detail = performanceMeasure.detail; - - if (!detail) { - return; - } - - // Process detail based on its type - if (typeof detail === 'object') { - // Handle object details - for (const [key, value] of Object.entries(detail)) { - if (value && isPrimitive(value)) { - attributes[`sentry.browser.measure.detail.${key}`] = value as SpanAttributeValue; - } else if (value !== undefined) { - try { - // This is user defined so we can't guarantee it's serializable - attributes[`sentry.browser.measure.detail.${key}`] = JSON.stringify(value); - } catch { - // Skip values that can't be stringified - } - } - } - return; - } - - if (isPrimitive(detail)) { - // Handle primitive details - attributes['sentry.browser.measure.detail'] = detail as SpanAttributeValue; - return; - } + const startTimestamp = timeOrigin + startTime; - try { - attributes['sentry.browser.measure.detail'] = JSON.stringify(detail); - } catch { - // Skip if stringification fails - } - } catch { - // Silently ignore any errors when accessing detail - // This handles the Firefox "Permission denied to access object" error - } + startAndEndSpan(span, startTimestamp, startTimestamp + duration, { + name: entry.name, + op: entry.entryType, + attributes: { + [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.resource.browser.metrics', + }, + }); } /** diff --git a/packages/browser-utils/src/metrics/instrument.ts b/packages/browser-utils/src/metrics/instrument.ts index 608a5fd11511..0590b421ce39 100644 --- a/packages/browser-utils/src/metrics/instrument.ts +++ b/packages/browser-utils/src/metrics/instrument.ts @@ -10,6 +10,8 @@ type InstrumentHandlerTypePerformanceObserver = | 'longtask' | 'event' | 'navigation' + | 'mark' + | 'measure' | 'paint' | 'resource' | 'element' diff --git a/packages/browser-utils/src/metrics/userTimingSpans.ts b/packages/browser-utils/src/metrics/userTimingSpans.ts new file mode 100644 index 000000000000..84d4f5142cb4 --- /dev/null +++ b/packages/browser-utils/src/metrics/userTimingSpans.ts @@ -0,0 +1,198 @@ +import { SENTRY_ORIGIN } from '@sentry/conventions/attributes'; +import type { IntegrationFn, Span, SpanAttributes, SpanAttributeValue } from '@sentry/core'; +import { + browserPerformanceTimeOrigin, + defineIntegration, + getActiveSpan, + getRootSpan, + isPrimitive, + spanToJSON, + stringMatchesSomePattern, +} from '@sentry/core'; +import { addPerformanceInstrumentationHandler } from './instrument'; +import { msToSec, startAndEndSpan } from './utils'; +import { getNavigationEntry } from './web-vitals/lib/getNavigationEntry'; + +interface UserTimingSpansOptions { + /** + * User Timing entries with names matching any of these strings or regular expressions will not be emitted. + * + * Default: [] + */ + ignore?: Array; +} + +const INTEGRATION_NAME = 'UserTimingSpans'; + +const _userTimingSpansIntegration = ((options: UserTimingSpansOptions = {}) => { + return { + name: INTEGRATION_NAME, + setup() { + const timeOrigin = browserPerformanceTimeOrigin(); + if (!timeOrigin) { + return; + } + const timeOriginInSeconds = msToSec(timeOrigin); + + const handleEntries = ({ entries }: { entries: PerformanceEntry[] }): void => { + const activeSpan = getActiveSpan(); + if (!activeSpan) { + return; + } + + // Attach entries to the pageload/navigation root span rather than whatever child span happens to + // be active when the browser reports them, so they consistently nest under the transaction. + const rootSpan = getRootSpan(activeSpan); + const { op: parentOp, start_timestamp: parentStartTimestamp } = spanToJSON(rootSpan); + if (parentOp !== 'pageload' && parentOp !== 'navigation') { + return; + } + + // The navigation entry is stable for the lifetime of the document, so resolve it once per batch + // rather than for every entry. + const requestTime = msToSec(getNavigationEntry(false)?.requestStart ?? 0); + + for (const entry of entries) { + const startTime = msToSec(entry.startTime); + const absoluteStartTime = timeOriginInSeconds + startTime; + + if (parentOp === 'navigation' && parentStartTimestamp && absoluteStartTime < parentStartTimestamp) { + continue; + } + + _addUserTimingSpan( + rootSpan, + entry, + startTime, + msToSec(Math.max(0, entry.duration)), + timeOriginInSeconds, + requestTime, + options.ignore ?? [], + ); + } + }; + + addPerformanceInstrumentationHandler('mark', handleEntries); + addPerformanceInstrumentationHandler('measure', handleEntries); + }, + }; +}) satisfies IntegrationFn; + +/** + * Captures spans created with the browser's User Timing APIs, `performance.mark` and `performance.measure`. + * + * The integration must be explicitly added to `Sentry.init`. Entries are attached to the active pageload or + * navigation span when the browser reports them through the PerformanceObserver API. + * + * @example + * ```ts + * Sentry.init({ + * integrations: [ + * Sentry.browserTracingIntegration(), + * Sentry.userTimingSpansIntegration({ + * ignore: ['third-party-mark', /framework-measure/], + * }), + * ], + * }); + * ``` + */ +export const userTimingSpansIntegration = defineIntegration(_userTimingSpansIntegration); + +/** + * Creates a span for a browser User Timing entry. + * Exported only for tests. + */ +export function _addUserTimingSpan( + parentSpan: Span, + entry: PerformanceEntry, + startTime: number, + duration: number, + timeOrigin: number, + requestTime: number, + ignore: Array, +): void { + if (isReact19MeasureEntry(entry) || stringMatchesSomePattern(entry.name, ignore)) { + return; + } + + // Measures can reference arbitrary timestamps, including timestamps before the page request started. + const spanStartTimestamp = timeOrigin + Math.max(startTime, requestTime); + const originalStartTimestamp = timeOrigin + startTime; + const spanEndTimestamp = originalStartTimestamp + duration; + + const attributes: SpanAttributes = { + [SENTRY_ORIGIN]: 'auto.resource.browser.metrics', + }; + + if (spanStartTimestamp !== originalStartTimestamp) { + attributes['sentry.browser.measure_happened_before_request'] = true; + attributes['sentry.browser.measure_start_time'] = spanStartTimestamp; + } + + addDetailToSpanAttributes(attributes, entry as PerformanceMeasure); + + // Third-party measurements can contain timestamps which would produce invalid spans. + if (spanStartTimestamp <= spanEndTimestamp) { + startAndEndSpan(parentSpan, spanStartTimestamp, spanEndTimestamp, { + name: entry.name, + op: entry.entryType, + attributes, + }); + } +} + +/** + * 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. + */ +function isReact19MeasureEntry(entry: PerformanceEntry): 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; + } +} + +function addDetailToSpanAttributes(attributes: SpanAttributes, entry: PerformanceMeasure): void { + try { + // Accessing detail can throw in some browsers due to security restrictions. + const detail = entry.detail; + if (!detail) { + return; + } + + if (typeof detail === 'object') { + for (const [key, value] of Object.entries(detail)) { + if (value && isPrimitive(value)) { + attributes[`sentry.browser.measure.detail.${key}`] = value as SpanAttributeValue; + } else if (value !== undefined) { + try { + attributes[`sentry.browser.measure.detail.${key}`] = JSON.stringify(value); + } catch { + // User-provided detail values are not guaranteed to be serializable. + } + } + } + return; + } + + if (isPrimitive(detail)) { + attributes['sentry.browser.measure.detail'] = detail as SpanAttributeValue; + return; + } + + try { + attributes['sentry.browser.measure.detail'] = JSON.stringify(detail); + } catch { + // User-provided detail values are not guaranteed to be serializable. + } + } catch { + // Accessing detail can throw in some browsers due to security restrictions. + } +} diff --git a/packages/browser-utils/src/metrics/web-vitals/lib/observe.ts b/packages/browser-utils/src/metrics/web-vitals/lib/observe.ts index 6071893dfa8e..8c52446cfacd 100644 --- a/packages/browser-utils/src/metrics/web-vitals/lib/observe.ts +++ b/packages/browser-utils/src/metrics/web-vitals/lib/observe.ts @@ -21,6 +21,8 @@ interface PerformanceEntryMap { 'largest-contentful-paint': LargestContentfulPaint[]; 'long-animation-frame': PerformanceLongAnimationFrameTiming[]; paint: PerformancePaintTiming[]; + mark: PerformanceMark[]; + measure: PerformanceMeasure[]; navigation: PerformanceNavigationTiming[]; resource: PerformanceResourceTiming[]; // Sentry-specific change: diff --git a/packages/browser-utils/test/browser/browserMetrics.test.ts b/packages/browser-utils/test/browser/browserMetrics.test.ts index 8acb93833c2a..fd1ae31d178a 100644 --- a/packages/browser-utils/test/browser/browserMetrics.test.ts +++ b/packages/browser-utils/test/browser/browserMetrics.test.ts @@ -11,7 +11,6 @@ import { } from '@sentry/core'; import { afterAll, afterEach, beforeAll, beforeEach, describe, expect, it, vi } from 'vitest'; import { - _addMeasureSpans, _addNavigationSpans, _addResourceSpans, _setResourceRequestAttributes, @@ -145,216 +144,6 @@ describe('addWebVitalsToSpan', () => { }); }); -describe('_addMeasureSpans', () => { - const span = new SentrySpan({ op: 'pageload', name: '/', sampled: true }); - - beforeEach(() => { - getCurrentScope().clear(); - getIsolationScope().clear(); - - const client = new TestClient( - getDefaultClientOptions({ - tracesSampleRate: 1, - }), - ); - setCurrentClient(client); - client.init(); - }); - - it('adds measure spans to a span', () => { - const spans: Span[] = []; - - getClient()?.on('spanEnd', span => { - spans.push(span); - }); - - const entry = { - entryType: 'measure', - name: 'measure-1', - duration: 10, - startTime: 12, - } as PerformanceEntry; - - const timeOrigin = 100; - const startTime = 23; - const duration = 356; - - _addMeasureSpans(span, entry, startTime, duration, timeOrigin, []); - - expect(spans).toHaveLength(1); - expect(spanToJSON(spans[0]!)).toEqual( - expect.objectContaining({ - description: 'measure-1', - start_timestamp: timeOrigin + startTime, - timestamp: timeOrigin + startTime + duration, - op: 'measure', - origin: 'auto.resource.browser.metrics', - data: { - [SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'measure', - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.resource.browser.metrics', - }, - }), - ); - }); - - it('drops measurement spans with negative duration', () => { - const spans: Span[] = []; - - getClient()?.on('spanEnd', span => { - spans.push(span); - }); - - const entry = { - entryType: 'measure', - name: 'measure-1', - duration: 10, - startTime: 12, - } as PerformanceEntry; - - const timeOrigin = 100; - const startTime = 23; - const duration = -50; - - _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' }), - ]), - ); - }); - - 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', () => { const span = new SentrySpan({ op: 'pageload', name: '/', sampled: true }); diff --git a/packages/browser-utils/test/metrics/userTimingSpans.test.ts b/packages/browser-utils/test/metrics/userTimingSpans.test.ts new file mode 100644 index 000000000000..654c0ca2a30c --- /dev/null +++ b/packages/browser-utils/test/metrics/userTimingSpans.test.ts @@ -0,0 +1,234 @@ +import type { Span } from '@sentry/core'; +import { + getCurrentScope, + getIsolationScope, + SentrySpan, + setCurrentClient, + spanToJSON, + startInactiveSpan, + withActiveSpan, +} from '@sentry/core'; +import { beforeEach, describe, expect, it, vi } from 'vitest'; +import * as instrument from '../../src/metrics/instrument'; +import { _addUserTimingSpan, userTimingSpansIntegration } from '../../src/metrics/userTimingSpans'; +import { getDefaultClientOptions, TestClient } from '../utils/TestClient'; + +type PerformanceEntryHandler = (data: { entries: PerformanceEntry[] }) => void; + +describe('userTimingSpansIntegration', () => { + let handlers: Map; + let spans: Span[]; + + beforeEach(() => { + vi.restoreAllMocks(); + getCurrentScope().clear(); + getIsolationScope().clear(); + + const client = new TestClient(getDefaultClientOptions({ tracesSampleRate: 1 })); + setCurrentClient(client); + client.init(); + + spans = []; + client.on('spanEnd', span => { + spans.push(span); + }); + + handlers = new Map(); + vi.spyOn(instrument, 'addPerformanceInstrumentationHandler').mockImplementation((type, handler) => { + handlers.set(type, handler); + return () => undefined; + }); + }); + + it('captures mark and measure entries as child spans', () => { + userTimingSpansIntegration().setup?.({} as never); + const parentSpan = new SentrySpan({ op: 'pageload', name: '/', sampled: true }); + + withActiveSpan(parentSpan, () => { + handlers.get('mark')!({ + entries: [createPerformanceEntry('mark', 'app-ready', 12, 0)], + }); + handlers.get('measure')!({ + entries: [createPerformanceEntry('measure', 'hydrate', 14, 25)], + }); + }); + + expect(spans).toHaveLength(2); + expect(spans.map(span => spanToJSON(span).description)).toEqual(['app-ready', 'hydrate']); + expect(spans.map(span => spanToJSON(span).op)).toEqual(['mark', 'measure']); + expect(spanToJSON(spans[0]!).timestamp).toBe(spanToJSON(spans[0]!).start_timestamp); + expect(spanToJSON(spans[1]!).timestamp! - spanToJSON(spans[1]!).start_timestamp).toBeCloseTo(0.025); + }); + + it('does not capture entries without an active span', () => { + userTimingSpansIntegration().setup?.({} as never); + + handlers.get('measure')!({ + entries: [createPerformanceEntry('measure', 'background-work', 14, 25)], + }); + + expect(spans).toHaveLength(0); + }); + + it('attaches entries to the root pageload span even when a child span is active', () => { + userTimingSpansIntegration().setup?.({} as never); + const rootSpan = new SentrySpan({ op: 'pageload', name: '/', sampled: true }); + + withActiveSpan(rootSpan, () => { + const childSpan = startInactiveSpan({ name: 'child' }); + withActiveSpan(childSpan, () => { + handlers.get('measure')!({ + entries: [createPerformanceEntry('measure', 'hydrate', 14, 25)], + }); + }); + }); + + const measureSpan = spans.find(span => spanToJSON(span).description === 'hydrate'); + expect(measureSpan).toBeDefined(); + expect(spanToJSON(measureSpan!).parent_span_id).toBe(rootSpan.spanContext().spanId); + }); + + it('does not capture entries when the active span is not a pageload or navigation', () => { + userTimingSpansIntegration().setup?.({} as never); + const rootSpan = new SentrySpan({ op: 'ui.action', name: 'click', sampled: true }); + + withActiveSpan(rootSpan, () => { + handlers.get('measure')!({ + entries: [createPerformanceEntry('measure', 'work', 14, 25)], + }); + }); + + expect(spans).toHaveLength(0); + }); + + it('ignores entries matching strings and regular expressions', () => { + userTimingSpansIntegration({ ignore: ['extension-mark', /^framework-/] }).setup?.({} as never); + const parentSpan = new SentrySpan({ op: 'pageload', name: '/', sampled: true }); + + withActiveSpan(parentSpan, () => { + handlers.get('mark')!({ + entries: [ + createPerformanceEntry('mark', 'extension-mark', 10, 0), + createPerformanceEntry('mark', 'application-mark', 11, 0), + ], + }); + handlers.get('measure')!({ + entries: [ + createPerformanceEntry('measure', 'framework-render', 12, 10), + createPerformanceEntry('measure', 'application-render', 13, 10), + ], + }); + }); + + expect(spans).toHaveLength(2); + expect(spans.map(span => spanToJSON(span).description)).toEqual(['application-mark', 'application-render']); + }); + + it('does not attach entries preceding a navigation span', () => { + userTimingSpansIntegration().setup?.({} as never); + const timeOrigin = performance.timeOrigin / 1000; + const parentSpan = new SentrySpan({ + op: 'navigation', + name: '/settings', + sampled: true, + startTimestamp: timeOrigin + 0.02, + }); + + withActiveSpan(parentSpan, () => { + handlers.get('measure')!({ + entries: [ + createPerformanceEntry('measure', 'previous-route', 10, 5), + createPerformanceEntry('measure', 'current-route', 30, 5), + ], + }); + }); + + expect(spans).toHaveLength(1); + expect(spanToJSON(spans[0]!).description).toBe('current-route'); + }); +}); + +describe('_addUserTimingSpan', () => { + let parentSpan: Span; + let spans: Span[]; + + beforeEach(() => { + vi.restoreAllMocks(); + getCurrentScope().clear(); + getIsolationScope().clear(); + + const client = new TestClient(getDefaultClientOptions({ tracesSampleRate: 1 })); + setCurrentClient(client); + client.init(); + + parentSpan = new SentrySpan({ op: 'pageload', name: '/', sampled: true }); + spans = []; + client.on('spanEnd', span => { + spans.push(span); + }); + }); + + it('adds measure detail as span attributes', () => { + const entry = { + ...createPerformanceEntry('measure', 'hydrate', 12, 10), + detail: { + phase: 'client', + counts: { components: 4 }, + }, + } as PerformanceMeasure; + + _addUserTimingSpan(parentSpan, entry, 0.012, 0.01, 100, 0, []); + + expect(spans).toHaveLength(1); + expect(spanToJSON(spans[0]!).data).toEqual({ + 'sentry.browser.measure.detail.phase': 'client', + 'sentry.browser.measure.detail.counts': '{"components":4}', + 'sentry.op': 'measure', + 'sentry.origin': 'auto.resource.browser.metrics', + }); + }); + + it('ignores React component performance measures', () => { + const entry = { + ...createPerformanceEntry('measure', '​SettingsPanel', 12, 10), + detail: { + devtools: { + track: 'Components ⚛', + }, + }, + } as PerformanceMeasure; + + _addUserTimingSpan(parentSpan, entry, 0.012, 0.01, 100, 0, []); + + expect(spans).toHaveLength(0); + }); + + it('drops entries whose adjusted start is after their end', () => { + _addUserTimingSpan( + parentSpan, + createPerformanceEntry('measure', 'before-request', 10, 10), + 0.01, + 0.01, + 100, + 0.05, + [], + ); + + expect(spans).toHaveLength(0); + }); +}); + +function createPerformanceEntry( + entryType: 'mark' | 'measure', + name: string, + startTime: number, + duration: number, +): PerformanceEntry { + return { + entryType, + name, + startTime, + duration, + toJSON: () => ({}), + }; +} diff --git a/packages/browser/src/index.ts b/packages/browser/src/index.ts index 46fec05bbb0a..c242e43f2f84 100644 --- a/packages/browser/src/index.ts +++ b/packages/browser/src/index.ts @@ -48,6 +48,7 @@ export { setActiveSpanInBrowser } from './tracing/setActiveSpan'; export { spanStreamingIntegration } from './integrations/spanstreaming'; export { fetchStreamPerformanceIntegration } from './integrations/fetchStreamPerformance'; export { webVitalsIntegration } from './integrations/webVitals'; +export { userTimingSpansIntegration } from '@sentry/browser-utils'; export type { RequestInstrumentationOptions } from './tracing/request'; export { diff --git a/packages/browser/src/tracing/browserTracingIntegration.ts b/packages/browser/src/tracing/browserTracingIntegration.ts index 60f8f7707ad8..97f639cad216 100644 --- a/packages/browser/src/tracing/browserTracingIntegration.ts +++ b/packages/browser/src/tracing/browserTracingIntegration.ts @@ -192,41 +192,6 @@ export interface BrowserTracingOptions { */ 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; - /** * By default, the SDK will try to detect redirects and avoid creating separate spans for them. * If you want to opt-out of this behavior, you can set this option to `false`. @@ -339,7 +304,6 @@ const DEFAULT_BROWSER_TRACING_OPTIONS: BrowserTracingOptions = { enableLongAnimationFrame: true, enableInp: true, ignoreResourceSpans: [], - ignorePerformanceApiSpans: [], detectRedirects: true, linkPreviousTrace: 'in-memory', consistentTraceSampling: false, @@ -395,7 +359,6 @@ export const browserTracingIntegration = ((options: Partial { addPerformanceEntries(span, { ignoreResourceSpans, - ignorePerformanceApiSpans, spanStreamingEnabled: hasSpanStreamingEnabled(client), }); setActiveIdleSpan(client, undefined); diff --git a/packages/browser/test/tracing/browserTracingIntegration.test.ts b/packages/browser/test/tracing/browserTracingIntegration.test.ts index 87aa268c2248..9ba95626cca2 100644 --- a/packages/browser/test/tracing/browserTracingIntegration.test.ts +++ b/packages/browser/test/tracing/browserTracingIntegration.test.ts @@ -1489,9 +1489,8 @@ describe('browserTracingIntegration', () => { vi.advanceTimersByTime(TRACING_DEFAULTS.idleTimeout); // idle span itself is now ended - // there is also the `sentry-tracing-init` span included - expect(spans).toHaveLength(3); - expect(spans[2]).toBe(idleSpan); + expect(spans).toHaveLength(2); + expect(spans[1]).toBe(idleSpan); }); it('can be a custom value', () => { @@ -1523,9 +1522,8 @@ describe('browserTracingIntegration', () => { vi.advanceTimersByTime(2000); // idle span itself is now ended - // there is also the `sentry-tracing-init` span included - expect(spans).toHaveLength(3); - expect(spans[2]).toBe(idleSpan); + expect(spans).toHaveLength(2); + expect(spans[1]).toBe(idleSpan); }); }); From b8e4a21ddce22b20daa74741af67301502bc1751 Mon Sep 17 00:00:00 2001 From: Martin Sonnberger Date: Thu, 23 Jul 2026 15:51:05 +0200 Subject: [PATCH 02/12] fix tests --- .../pageload-measure-spans-domexception-details/init.js | 2 +- .../tracing/metrics/pageload-measure-spans/init.js | 2 +- .../browser/src/index.bundle.tracing.logs.metrics.ts | 1 + .../index.bundle.tracing.replay.feedback.logs.metrics.ts | 1 + .../browser/src/index.bundle.tracing.replay.feedback.ts | 1 + .../src/index.bundle.tracing.replay.logs.metrics.ts | 1 + packages/browser/src/index.bundle.tracing.replay.ts | 1 + packages/browser/src/index.bundle.tracing.ts | 1 + .../test/index.bundle.tracing.logs.metrics.test.ts | 8 +++++++- ...x.bundle.tracing.replay.feedback.logs.metrics.test.ts | 2 ++ .../test/index.bundle.tracing.replay.feedback.test.ts | 2 ++ .../index.bundle.tracing.replay.logs.metrics.test.ts | 9 ++++++++- .../browser/test/index.bundle.tracing.replay.test.ts | 9 ++++++++- packages/browser/test/index.bundle.tracing.test.ts | 8 +++++++- 14 files changed, 42 insertions(+), 6 deletions(-) diff --git a/dev-packages/browser-integration-tests/suites/tracing/metrics/pageload-measure-spans-domexception-details/init.js b/dev-packages/browser-integration-tests/suites/tracing/metrics/pageload-measure-spans-domexception-details/init.js index dcbf047b0b04..e78fc188cb90 100644 --- a/dev-packages/browser-integration-tests/suites/tracing/metrics/pageload-measure-spans-domexception-details/init.js +++ b/dev-packages/browser-integration-tests/suites/tracing/metrics/pageload-measure-spans-domexception-details/init.js @@ -24,7 +24,7 @@ window.Sentry = Sentry; Sentry.init({ traceLifecycle: 'static', dsn: 'https://public@dsn.ingest.sentry.io/1337', - integrations: [Sentry.browserTracingIntegration({})], + integrations: [Sentry.browserTracingIntegration(), Sentry.userTimingSpansIntegration()], tracesSampleRate: 1, }); diff --git a/dev-packages/browser-integration-tests/suites/tracing/metrics/pageload-measure-spans/init.js b/dev-packages/browser-integration-tests/suites/tracing/metrics/pageload-measure-spans/init.js index c80e4049e83d..e675ac92e7cc 100644 --- a/dev-packages/browser-integration-tests/suites/tracing/metrics/pageload-measure-spans/init.js +++ b/dev-packages/browser-integration-tests/suites/tracing/metrics/pageload-measure-spans/init.js @@ -12,6 +12,6 @@ window.Sentry = Sentry; Sentry.init({ traceLifecycle: 'static', dsn: 'https://public@dsn.ingest.sentry.io/1337', - integrations: [Sentry.browserTracingIntegration()], + integrations: [Sentry.browserTracingIntegration(), Sentry.userTimingSpansIntegration()], tracesSampleRate: 1, }); diff --git a/packages/browser/src/index.bundle.tracing.logs.metrics.ts b/packages/browser/src/index.bundle.tracing.logs.metrics.ts index 1dd34bb2ff42..a3c5e88d3d06 100644 --- a/packages/browser/src/index.bundle.tracing.logs.metrics.ts +++ b/packages/browser/src/index.bundle.tracing.logs.metrics.ts @@ -33,6 +33,7 @@ export { setActiveSpanInBrowser } from './tracing/setActiveSpan'; export { spanStreamingIntegration } from './integrations/spanstreaming'; export { fetchStreamPerformanceIntegration } from './integrations/fetchStreamPerformance'; export { webVitalsIntegration } from './integrations/webVitals'; +export { userTimingSpansIntegration } from '@sentry/browser-utils'; export { feedbackIntegrationShim as feedbackAsyncIntegration, diff --git a/packages/browser/src/index.bundle.tracing.replay.feedback.logs.metrics.ts b/packages/browser/src/index.bundle.tracing.replay.feedback.logs.metrics.ts index cc7262dc9dbe..5b7492a590df 100644 --- a/packages/browser/src/index.bundle.tracing.replay.feedback.logs.metrics.ts +++ b/packages/browser/src/index.bundle.tracing.replay.feedback.logs.metrics.ts @@ -33,6 +33,7 @@ export { setActiveSpanInBrowser } from './tracing/setActiveSpan'; export { spanStreamingIntegration } from './integrations/spanstreaming'; export { fetchStreamPerformanceIntegration } from './integrations/fetchStreamPerformance'; export { webVitalsIntegration } from './integrations/webVitals'; +export { userTimingSpansIntegration } from '@sentry/browser-utils'; export { getFeedback, sendFeedback } from '@sentry/feedback'; diff --git a/packages/browser/src/index.bundle.tracing.replay.feedback.ts b/packages/browser/src/index.bundle.tracing.replay.feedback.ts index 1e821b38f824..8a3fad48fc00 100644 --- a/packages/browser/src/index.bundle.tracing.replay.feedback.ts +++ b/packages/browser/src/index.bundle.tracing.replay.feedback.ts @@ -39,6 +39,7 @@ export { reportPageLoaded } from './tracing/reportPageLoaded'; export { spanStreamingIntegration } from './integrations/spanstreaming'; export { fetchStreamPerformanceIntegration } from './integrations/fetchStreamPerformance'; export { webVitalsIntegration } from './integrations/webVitals'; +export { userTimingSpansIntegration } from '@sentry/browser-utils'; export { getFeedback, sendFeedback } from '@sentry/feedback'; diff --git a/packages/browser/src/index.bundle.tracing.replay.logs.metrics.ts b/packages/browser/src/index.bundle.tracing.replay.logs.metrics.ts index 9abdd4675d5e..6a9f5d7b7092 100644 --- a/packages/browser/src/index.bundle.tracing.replay.logs.metrics.ts +++ b/packages/browser/src/index.bundle.tracing.replay.logs.metrics.ts @@ -33,6 +33,7 @@ export { setActiveSpanInBrowser } from './tracing/setActiveSpan'; export { spanStreamingIntegration } from './integrations/spanstreaming'; export { fetchStreamPerformanceIntegration } from './integrations/fetchStreamPerformance'; export { webVitalsIntegration } from './integrations/webVitals'; +export { userTimingSpansIntegration } from '@sentry/browser-utils'; export { feedbackIntegrationShim as feedbackAsyncIntegration, feedbackIntegrationShim as feedbackIntegration }; diff --git a/packages/browser/src/index.bundle.tracing.replay.ts b/packages/browser/src/index.bundle.tracing.replay.ts index 50da2dac87c4..46e2cbcf27cc 100644 --- a/packages/browser/src/index.bundle.tracing.replay.ts +++ b/packages/browser/src/index.bundle.tracing.replay.ts @@ -38,6 +38,7 @@ export { setActiveSpanInBrowser } from './tracing/setActiveSpan'; export { spanStreamingIntegration } from './integrations/spanstreaming'; export { fetchStreamPerformanceIntegration } from './integrations/fetchStreamPerformance'; export { webVitalsIntegration } from './integrations/webVitals'; +export { userTimingSpansIntegration } from '@sentry/browser-utils'; export { feedbackIntegrationShim as feedbackAsyncIntegration, feedbackIntegrationShim as feedbackIntegration }; diff --git a/packages/browser/src/index.bundle.tracing.ts b/packages/browser/src/index.bundle.tracing.ts index aac6825ecc65..70b9b629d759 100644 --- a/packages/browser/src/index.bundle.tracing.ts +++ b/packages/browser/src/index.bundle.tracing.ts @@ -40,6 +40,7 @@ export { reportPageLoaded } from './tracing/reportPageLoaded'; export { spanStreamingIntegration } from './integrations/spanstreaming'; export { fetchStreamPerformanceIntegration } from './integrations/fetchStreamPerformance'; export { webVitalsIntegration } from './integrations/webVitals'; +export { userTimingSpansIntegration } from '@sentry/browser-utils'; export { feedbackIntegrationShim as feedbackAsyncIntegration, diff --git a/packages/browser/test/index.bundle.tracing.logs.metrics.test.ts b/packages/browser/test/index.bundle.tracing.logs.metrics.test.ts index 81d239c8ff2d..1ee2fb99e315 100644 --- a/packages/browser/test/index.bundle.tracing.logs.metrics.test.ts +++ b/packages/browser/test/index.bundle.tracing.logs.metrics.test.ts @@ -1,7 +1,12 @@ import { logger as coreLogger, metrics as coreMetrics } from '@sentry/core/browser'; import { feedbackIntegrationShim, replayIntegrationShim } from '@sentry-internal/integration-shims'; import { describe, expect, it } from 'vitest'; -import { browserTracingIntegration, spanStreamingIntegration, webVitalsIntegration } from '../src'; +import { + browserTracingIntegration, + spanStreamingIntegration, + userTimingSpansIntegration, + webVitalsIntegration, +} from '../src'; import * as TracingLogsMetricsBundle from '../src/index.bundle.tracing.logs.metrics'; describe('index.bundle.tracing.logs.metrics', () => { @@ -11,6 +16,7 @@ describe('index.bundle.tracing.logs.metrics', () => { expect(TracingLogsMetricsBundle.feedbackIntegration).toBe(feedbackIntegrationShim); expect(TracingLogsMetricsBundle.replayIntegration).toBe(replayIntegrationShim); expect(TracingLogsMetricsBundle.spanStreamingIntegration).toBe(spanStreamingIntegration); + expect(TracingLogsMetricsBundle.userTimingSpansIntegration).toBe(userTimingSpansIntegration); expect(TracingLogsMetricsBundle.webVitalsIntegration).toBe(webVitalsIntegration); expect(TracingLogsMetricsBundle.logger).toBe(coreLogger); diff --git a/packages/browser/test/index.bundle.tracing.replay.feedback.logs.metrics.test.ts b/packages/browser/test/index.bundle.tracing.replay.feedback.logs.metrics.test.ts index dcb7b869aa4d..77b47757bcf9 100644 --- a/packages/browser/test/index.bundle.tracing.replay.feedback.logs.metrics.test.ts +++ b/packages/browser/test/index.bundle.tracing.replay.feedback.logs.metrics.test.ts @@ -5,6 +5,7 @@ import { feedbackAsyncIntegration, replayIntegration, spanStreamingIntegration, + userTimingSpansIntegration, webVitalsIntegration, } from '../src'; import * as TracingReplayFeedbackLogsMetricsBundle from '../src/index.bundle.tracing.replay.feedback.logs.metrics'; @@ -16,6 +17,7 @@ describe('index.bundle.tracing.replay.feedback.logs.metrics', () => { expect(TracingReplayFeedbackLogsMetricsBundle.feedbackIntegration).toBe(feedbackAsyncIntegration); expect(TracingReplayFeedbackLogsMetricsBundle.replayIntegration).toBe(replayIntegration); expect(TracingReplayFeedbackLogsMetricsBundle.spanStreamingIntegration).toBe(spanStreamingIntegration); + expect(TracingReplayFeedbackLogsMetricsBundle.userTimingSpansIntegration).toBe(userTimingSpansIntegration); expect(TracingReplayFeedbackLogsMetricsBundle.webVitalsIntegration).toBe(webVitalsIntegration); expect(TracingReplayFeedbackLogsMetricsBundle.logger).toBe(coreLogger); diff --git a/packages/browser/test/index.bundle.tracing.replay.feedback.test.ts b/packages/browser/test/index.bundle.tracing.replay.feedback.test.ts index 1bb8fffbeef9..ccd348f67416 100644 --- a/packages/browser/test/index.bundle.tracing.replay.feedback.test.ts +++ b/packages/browser/test/index.bundle.tracing.replay.feedback.test.ts @@ -5,6 +5,7 @@ import { feedbackAsyncIntegration, replayIntegration, spanStreamingIntegration, + userTimingSpansIntegration, webVitalsIntegration, } from '../src'; import * as TracingReplayFeedbackBundle from '../src/index.bundle.tracing.replay.feedback'; @@ -16,6 +17,7 @@ describe('index.bundle.tracing.replay.feedback', () => { expect(TracingReplayFeedbackBundle.feedbackIntegration).toBe(feedbackAsyncIntegration); expect(TracingReplayFeedbackBundle.replayIntegration).toBe(replayIntegration); expect(TracingReplayFeedbackBundle.spanStreamingIntegration).toBe(spanStreamingIntegration); + expect(TracingReplayFeedbackBundle.userTimingSpansIntegration).toBe(userTimingSpansIntegration); expect(TracingReplayFeedbackBundle.webVitalsIntegration).toBe(webVitalsIntegration); expect(TracingReplayFeedbackBundle.logger).toBe(loggerShim); diff --git a/packages/browser/test/index.bundle.tracing.replay.logs.metrics.test.ts b/packages/browser/test/index.bundle.tracing.replay.logs.metrics.test.ts index aecd1c995dda..27923e73cd16 100644 --- a/packages/browser/test/index.bundle.tracing.replay.logs.metrics.test.ts +++ b/packages/browser/test/index.bundle.tracing.replay.logs.metrics.test.ts @@ -1,7 +1,13 @@ import { logger as coreLogger, metrics as coreMetrics } from '@sentry/core/browser'; import { feedbackIntegrationShim } from '@sentry-internal/integration-shims'; import { describe, expect, it } from 'vitest'; -import { browserTracingIntegration, replayIntegration, spanStreamingIntegration, webVitalsIntegration } from '../src'; +import { + browserTracingIntegration, + replayIntegration, + spanStreamingIntegration, + userTimingSpansIntegration, + webVitalsIntegration, +} from '../src'; import * as TracingReplayLogsMetricsBundle from '../src/index.bundle.tracing.replay.logs.metrics'; describe('index.bundle.tracing.replay.logs.metrics', () => { @@ -11,6 +17,7 @@ describe('index.bundle.tracing.replay.logs.metrics', () => { expect(TracingReplayLogsMetricsBundle.feedbackIntegration).toBe(feedbackIntegrationShim); expect(TracingReplayLogsMetricsBundle.replayIntegration).toBe(replayIntegration); expect(TracingReplayLogsMetricsBundle.spanStreamingIntegration).toBe(spanStreamingIntegration); + expect(TracingReplayLogsMetricsBundle.userTimingSpansIntegration).toBe(userTimingSpansIntegration); expect(TracingReplayLogsMetricsBundle.webVitalsIntegration).toBe(webVitalsIntegration); expect(TracingReplayLogsMetricsBundle.logger).toBe(coreLogger); diff --git a/packages/browser/test/index.bundle.tracing.replay.test.ts b/packages/browser/test/index.bundle.tracing.replay.test.ts index 847e572be009..75fe2970acd8 100644 --- a/packages/browser/test/index.bundle.tracing.replay.test.ts +++ b/packages/browser/test/index.bundle.tracing.replay.test.ts @@ -1,6 +1,12 @@ import { consoleLoggingIntegrationShim, feedbackIntegrationShim, loggerShim } from '@sentry-internal/integration-shims'; import { describe, expect, it } from 'vitest'; -import { browserTracingIntegration, replayIntegration, spanStreamingIntegration, webVitalsIntegration } from '../src'; +import { + browserTracingIntegration, + replayIntegration, + spanStreamingIntegration, + userTimingSpansIntegration, + webVitalsIntegration, +} from '../src'; import * as TracingReplayBundle from '../src/index.bundle.tracing.replay'; describe('index.bundle.tracing.replay', () => { @@ -10,6 +16,7 @@ describe('index.bundle.tracing.replay', () => { expect(TracingReplayBundle.feedbackIntegration).toBe(feedbackIntegrationShim); expect(TracingReplayBundle.replayIntegration).toBe(replayIntegration); expect(TracingReplayBundle.spanStreamingIntegration).toBe(spanStreamingIntegration); + expect(TracingReplayBundle.userTimingSpansIntegration).toBe(userTimingSpansIntegration); expect(TracingReplayBundle.webVitalsIntegration).toBe(webVitalsIntegration); expect(TracingReplayBundle.logger).toBe(loggerShim); diff --git a/packages/browser/test/index.bundle.tracing.test.ts b/packages/browser/test/index.bundle.tracing.test.ts index 9d41fcd10e42..80aed0b06bc0 100644 --- a/packages/browser/test/index.bundle.tracing.test.ts +++ b/packages/browser/test/index.bundle.tracing.test.ts @@ -5,7 +5,12 @@ import { replayIntegrationShim, } from '@sentry-internal/integration-shims'; import { describe, expect, it } from 'vitest'; -import { browserTracingIntegration, spanStreamingIntegration, webVitalsIntegration } from '../src'; +import { + browserTracingIntegration, + spanStreamingIntegration, + userTimingSpansIntegration, + webVitalsIntegration, +} from '../src'; import * as TracingBundle from '../src/index.bundle.tracing'; describe('index.bundle.tracing', () => { @@ -15,6 +20,7 @@ describe('index.bundle.tracing', () => { expect(TracingBundle.feedbackIntegration).toBe(feedbackIntegrationShim); expect(TracingBundle.replayIntegration).toBe(replayIntegrationShim); expect(TracingBundle.spanStreamingIntegration).toBe(spanStreamingIntegration); + expect(TracingBundle.userTimingSpansIntegration).toBe(userTimingSpansIntegration); expect(TracingBundle.webVitalsIntegration).toBe(webVitalsIntegration); expect(TracingBundle.logger).toBe(loggerShim); From ea4df09385c477c9429155fcd46053f497b5592b Mon Sep 17 00:00:00 2001 From: Martin Sonnberger Date: Mon, 27 Jul 2026 18:35:03 +0200 Subject: [PATCH 03/12] rename integration and sentry.origin, move tests --- MIGRATION.md | 4 ++-- .../init.js | 2 +- .../test.ts | 6 +++--- .../metrics/pageload-measure-spans/init.js | 2 +- .../metrics/pageload-measure-spans/test.ts | 2 +- .../user-timing-spans-disabled/init.js | 0 .../user-timing-spans-disabled/test.ts | 4 ++-- .../user-timing-spans-streamed/init.js | 6 +----- .../user-timing-spans-streamed/test.ts | 6 +++--- .../{metrics => }/user-timing-spans/init.js | 2 +- .../{metrics => }/user-timing-spans/test.ts | 4 ++-- packages/browser-utils/src/index.ts | 2 +- .../{userTimingSpans.ts => userTiming.ts} | 12 ++++++------ ...rTimingSpans.test.ts => userTiming.test.ts} | 18 +++++++++--------- .../src/index.bundle.tracing.logs.metrics.ts | 2 +- ...dle.tracing.replay.feedback.logs.metrics.ts | 2 +- .../index.bundle.tracing.replay.feedback.ts | 2 +- ...index.bundle.tracing.replay.logs.metrics.ts | 2 +- .../browser/src/index.bundle.tracing.replay.ts | 2 +- packages/browser/src/index.bundle.tracing.ts | 2 +- packages/browser/src/index.ts | 2 +- .../index.bundle.tracing.logs.metrics.test.ts | 4 ++-- ...racing.replay.feedback.logs.metrics.test.ts | 4 ++-- ...ndex.bundle.tracing.replay.feedback.test.ts | 4 ++-- ....bundle.tracing.replay.logs.metrics.test.ts | 4 ++-- .../test/index.bundle.tracing.replay.test.ts | 4 ++-- .../browser/test/index.bundle.tracing.test.ts | 4 ++-- 27 files changed, 52 insertions(+), 56 deletions(-) rename dev-packages/browser-integration-tests/suites/tracing/{metrics => }/user-timing-spans-disabled/init.js (100%) rename dev-packages/browser-integration-tests/suites/tracing/{metrics => }/user-timing-spans-disabled/test.ts (87%) rename dev-packages/browser-integration-tests/suites/tracing/{metrics => }/user-timing-spans-streamed/init.js (64%) rename dev-packages/browser-integration-tests/suites/tracing/{metrics => }/user-timing-spans-streamed/test.ts (78%) rename dev-packages/browser-integration-tests/suites/tracing/{metrics => }/user-timing-spans/init.js (82%) rename dev-packages/browser-integration-tests/suites/tracing/{metrics => }/user-timing-spans/test.ts (91%) rename packages/browser-utils/src/metrics/{userTimingSpans.ts => userTiming.ts} (94%) rename packages/browser-utils/test/metrics/{userTimingSpans.test.ts => userTiming.test.ts} (92%) diff --git a/MIGRATION.md b/MIGRATION.md index 808c62d50cfe..e12208fcba74 100644 --- a/MIGRATION.md +++ b/MIGRATION.md @@ -345,7 +345,7 @@ Sentry.init({ - The experimental `_experiments.enableStandaloneClsSpans` and `_experiments.enableStandaloneLcpSpans` options were removed from both `browserTracingIntegration` and `webVitalsIntegration`. CLS and LCP are no longer configurable: they are recorded as measurements on the pageload span, unless span streaming is enabled (`traceLifecycle: 'stream'`), in which case they are sent as dedicated spans. - INP is now always sent as a web vital span (streamed when span streaming is enabled, standalone otherwise) that carries its value as a `browser.web_vital.inp.value` attribute. Previously, with span streaming disabled, INP was sent as a standalone span that carried its value as a span measurement. -- `browserTracingIntegration` no longer captures spans created by `performance.mark()` and `performance.measure()` by default. Add `userTimingSpansIntegration()` to continue capturing them. The `ignorePerformanceApiSpans` option moved to the new integration as `ignore`. +- `browserTracingIntegration` no longer captures spans created by `performance.mark()` and `performance.measure()` by default. Add `userTimingIntegration()` to continue capturing them. The `ignorePerformanceApiSpans` option moved to the new integration as `ignore`. ```js // before @@ -361,7 +361,7 @@ Sentry.init({ Sentry.init({ integrations: [ Sentry.browserTracingIntegration(), - Sentry.userTimingSpansIntegration({ + Sentry.userTimingIntegration({ ignore: ['third-party-mark'], }), ], diff --git a/dev-packages/browser-integration-tests/suites/tracing/metrics/pageload-measure-spans-domexception-details/init.js b/dev-packages/browser-integration-tests/suites/tracing/metrics/pageload-measure-spans-domexception-details/init.js index e78fc188cb90..6d8ddc4c1830 100644 --- a/dev-packages/browser-integration-tests/suites/tracing/metrics/pageload-measure-spans-domexception-details/init.js +++ b/dev-packages/browser-integration-tests/suites/tracing/metrics/pageload-measure-spans-domexception-details/init.js @@ -24,7 +24,7 @@ window.Sentry = Sentry; Sentry.init({ traceLifecycle: 'static', dsn: 'https://public@dsn.ingest.sentry.io/1337', - integrations: [Sentry.browserTracingIntegration(), Sentry.userTimingSpansIntegration()], + integrations: [Sentry.browserTracingIntegration(), Sentry.userTimingIntegration()], tracesSampleRate: 1, }); diff --git a/dev-packages/browser-integration-tests/suites/tracing/metrics/pageload-measure-spans-domexception-details/test.ts b/dev-packages/browser-integration-tests/suites/tracing/metrics/pageload-measure-spans-domexception-details/test.ts index a990694b46bf..036c8fb8e0a9 100644 --- a/dev-packages/browser-integration-tests/suites/tracing/metrics/pageload-measure-spans-domexception-details/test.ts +++ b/dev-packages/browser-integration-tests/suites/tracing/metrics/pageload-measure-spans-domexception-details/test.ts @@ -25,7 +25,7 @@ sentryTest( expect(restrictedMeasure).toBeDefined(); expect(restrictedMeasure?.data).toMatchObject({ 'sentry.op': 'measure', - 'sentry.origin': 'auto.resource.browser.metrics', + 'sentry.origin': 'auto.resource.browser.user_timing', }); // Verify no detail attributes were added due to the permission error @@ -39,7 +39,7 @@ sentryTest( expect(normalMeasure?.data).toMatchObject({ 'sentry.browser.measure.detail': 'this-should-work', 'sentry.op': 'measure', - 'sentry.origin': 'auto.resource.browser.metrics', + 'sentry.origin': 'auto.resource.browser.user_timing', }); // Test 3: Verify the complex detail object is captured correctly @@ -47,7 +47,7 @@ sentryTest( expect(complexMeasure).toBeDefined(); expect(complexMeasure?.data).toMatchObject({ 'sentry.op': 'measure', - 'sentry.origin': 'auto.resource.browser.metrics', + 'sentry.origin': 'auto.resource.browser.user_timing', // The entire nested object is stringified as a single value 'sentry.browser.measure.detail.nested': JSON.stringify({ array: [1, 2, 3], diff --git a/dev-packages/browser-integration-tests/suites/tracing/metrics/pageload-measure-spans/init.js b/dev-packages/browser-integration-tests/suites/tracing/metrics/pageload-measure-spans/init.js index e675ac92e7cc..1b14ef4fc428 100644 --- a/dev-packages/browser-integration-tests/suites/tracing/metrics/pageload-measure-spans/init.js +++ b/dev-packages/browser-integration-tests/suites/tracing/metrics/pageload-measure-spans/init.js @@ -12,6 +12,6 @@ window.Sentry = Sentry; Sentry.init({ traceLifecycle: 'static', dsn: 'https://public@dsn.ingest.sentry.io/1337', - integrations: [Sentry.browserTracingIntegration(), Sentry.userTimingSpansIntegration()], + integrations: [Sentry.browserTracingIntegration(), Sentry.userTimingIntegration()], tracesSampleRate: 1, }); diff --git a/dev-packages/browser-integration-tests/suites/tracing/metrics/pageload-measure-spans/test.ts b/dev-packages/browser-integration-tests/suites/tracing/metrics/pageload-measure-spans/test.ts index 8d331118028e..67a3dab51535 100644 --- a/dev-packages/browser-integration-tests/suites/tracing/metrics/pageload-measure-spans/test.ts +++ b/dev-packages/browser-integration-tests/suites/tracing/metrics/pageload-measure-spans/test.ts @@ -30,6 +30,6 @@ sentryTest('should add browser-related spans to pageload transaction', async ({ 'sentry.browser.measure_happened_before_request': true, 'sentry.browser.measure_start_time': expect.any(Number), 'sentry.op': 'measure', - 'sentry.origin': 'auto.resource.browser.metrics', + 'sentry.origin': 'auto.resource.browser.user_timing', }); }); diff --git a/dev-packages/browser-integration-tests/suites/tracing/metrics/user-timing-spans-disabled/init.js b/dev-packages/browser-integration-tests/suites/tracing/user-timing-spans-disabled/init.js similarity index 100% rename from dev-packages/browser-integration-tests/suites/tracing/metrics/user-timing-spans-disabled/init.js rename to dev-packages/browser-integration-tests/suites/tracing/user-timing-spans-disabled/init.js diff --git a/dev-packages/browser-integration-tests/suites/tracing/metrics/user-timing-spans-disabled/test.ts b/dev-packages/browser-integration-tests/suites/tracing/user-timing-spans-disabled/test.ts similarity index 87% rename from dev-packages/browser-integration-tests/suites/tracing/metrics/user-timing-spans-disabled/test.ts rename to dev-packages/browser-integration-tests/suites/tracing/user-timing-spans-disabled/test.ts index 3021d063c2d5..ee71b159d2e6 100644 --- a/dev-packages/browser-integration-tests/suites/tracing/metrics/user-timing-spans-disabled/test.ts +++ b/dev-packages/browser-integration-tests/suites/tracing/user-timing-spans-disabled/test.ts @@ -1,6 +1,6 @@ import { expect } from '@playwright/test'; -import { sentryTest } from '../../../../utils/fixtures'; -import { envelopeRequestParser, shouldSkipTracingTest, waitForTransactionRequest } from '../../../../utils/helpers'; +import { sentryTest } from '../../../utils/fixtures'; +import { envelopeRequestParser, shouldSkipTracingTest, waitForTransactionRequest } from '../../../utils/helpers'; sentryTest('does not capture mark and measure spans by default', async ({ getLocalTestUrl, page }) => { if (shouldSkipTracingTest()) { diff --git a/dev-packages/browser-integration-tests/suites/tracing/metrics/user-timing-spans-streamed/init.js b/dev-packages/browser-integration-tests/suites/tracing/user-timing-spans-streamed/init.js similarity index 64% rename from dev-packages/browser-integration-tests/suites/tracing/metrics/user-timing-spans-streamed/init.js rename to dev-packages/browser-integration-tests/suites/tracing/user-timing-spans-streamed/init.js index 6d79eef2be33..862c83a1a783 100644 --- a/dev-packages/browser-integration-tests/suites/tracing/metrics/user-timing-spans-streamed/init.js +++ b/dev-packages/browser-integration-tests/suites/tracing/user-timing-spans-streamed/init.js @@ -4,11 +4,7 @@ window.Sentry = Sentry; Sentry.init({ dsn: 'https://public@dsn.ingest.sentry.io/1337', - integrations: [ - Sentry.browserTracingIntegration(), - Sentry.userTimingSpansIntegration(), - Sentry.spanStreamingIntegration(), - ], + integrations: [Sentry.browserTracingIntegration(), Sentry.userTimingIntegration(), Sentry.spanStreamingIntegration()], traceLifecycle: 'stream', tracesSampleRate: 1, }); diff --git a/dev-packages/browser-integration-tests/suites/tracing/metrics/user-timing-spans-streamed/test.ts b/dev-packages/browser-integration-tests/suites/tracing/user-timing-spans-streamed/test.ts similarity index 78% rename from dev-packages/browser-integration-tests/suites/tracing/metrics/user-timing-spans-streamed/test.ts rename to dev-packages/browser-integration-tests/suites/tracing/user-timing-spans-streamed/test.ts index aa39b8203ba8..5049b86e6d0f 100644 --- a/dev-packages/browser-integration-tests/suites/tracing/metrics/user-timing-spans-streamed/test.ts +++ b/dev-packages/browser-integration-tests/suites/tracing/user-timing-spans-streamed/test.ts @@ -1,7 +1,7 @@ import { expect } from '@playwright/test'; -import { sentryTest } from '../../../../utils/fixtures'; -import { shouldSkipTracingTest } from '../../../../utils/helpers'; -import { getSpanOp, waitForStreamedSpans } from '../../../../utils/spanUtils'; +import { sentryTest } from '../../../utils/fixtures'; +import { shouldSkipTracingTest } from '../../../utils/helpers'; +import { getSpanOp, waitForStreamedSpans } from '../../../utils/spanUtils'; sentryTest('captures each mark and measure once with span streaming', async ({ getLocalTestUrl, page }) => { sentryTest.skip(shouldSkipTracingTest()); diff --git a/dev-packages/browser-integration-tests/suites/tracing/metrics/user-timing-spans/init.js b/dev-packages/browser-integration-tests/suites/tracing/user-timing-spans/init.js similarity index 82% rename from dev-packages/browser-integration-tests/suites/tracing/metrics/user-timing-spans/init.js rename to dev-packages/browser-integration-tests/suites/tracing/user-timing-spans/init.js index 4299b62e1cca..3bf7dcc8495e 100644 --- a/dev-packages/browser-integration-tests/suites/tracing/metrics/user-timing-spans/init.js +++ b/dev-packages/browser-integration-tests/suites/tracing/user-timing-spans/init.js @@ -7,7 +7,7 @@ Sentry.init({ dsn: 'https://public@dsn.ingest.sentry.io/1337', integrations: [ Sentry.browserTracingIntegration(), - Sentry.userTimingSpansIntegration({ ignore: ['measure-ignore', /mark-i/] }), + Sentry.userTimingIntegration({ ignore: ['measure-ignore', /mark-i/] }), ], tracesSampleRate: 1, }); diff --git a/dev-packages/browser-integration-tests/suites/tracing/metrics/user-timing-spans/test.ts b/dev-packages/browser-integration-tests/suites/tracing/user-timing-spans/test.ts similarity index 91% rename from dev-packages/browser-integration-tests/suites/tracing/metrics/user-timing-spans/test.ts rename to dev-packages/browser-integration-tests/suites/tracing/user-timing-spans/test.ts index 67527d31b57e..86c79a9bf8d7 100644 --- a/dev-packages/browser-integration-tests/suites/tracing/metrics/user-timing-spans/test.ts +++ b/dev-packages/browser-integration-tests/suites/tracing/user-timing-spans/test.ts @@ -1,6 +1,6 @@ import { expect } from '@playwright/test'; -import { sentryTest } from '../../../../utils/fixtures'; -import { envelopeRequestParser, shouldSkipTracingTest, waitForTransactionRequest } from '../../../../utils/helpers'; +import { sentryTest } from '../../../utils/fixtures'; +import { envelopeRequestParser, shouldSkipTracingTest, waitForTransactionRequest } from '../../../utils/helpers'; sentryTest('captures non-ignored mark and measure spans', async ({ getLocalTestUrl, page }) => { if (shouldSkipTracingTest()) { diff --git a/packages/browser-utils/src/index.ts b/packages/browser-utils/src/index.ts index c3008d7524c5..7722fa9962cf 100644 --- a/packages/browser-utils/src/index.ts +++ b/packages/browser-utils/src/index.ts @@ -20,7 +20,7 @@ export { // eslint-disable-next-line typescript/no-deprecated export { elementTimingIntegration, startTrackingElementTiming } from './metrics/elementTiming'; -export { userTimingSpansIntegration } from './metrics/userTimingSpans'; +export { userTimingIntegration } from './metrics/userTiming'; export { extractNetworkProtocol } from './metrics/utils'; diff --git a/packages/browser-utils/src/metrics/userTimingSpans.ts b/packages/browser-utils/src/metrics/userTiming.ts similarity index 94% rename from packages/browser-utils/src/metrics/userTimingSpans.ts rename to packages/browser-utils/src/metrics/userTiming.ts index 84d4f5142cb4..5cd889f7ab17 100644 --- a/packages/browser-utils/src/metrics/userTimingSpans.ts +++ b/packages/browser-utils/src/metrics/userTiming.ts @@ -13,7 +13,7 @@ import { addPerformanceInstrumentationHandler } from './instrument'; import { msToSec, startAndEndSpan } from './utils'; import { getNavigationEntry } from './web-vitals/lib/getNavigationEntry'; -interface UserTimingSpansOptions { +interface UserTimingOptions { /** * User Timing entries with names matching any of these strings or regular expressions will not be emitted. * @@ -22,9 +22,9 @@ interface UserTimingSpansOptions { ignore?: Array; } -const INTEGRATION_NAME = 'UserTimingSpans'; +const INTEGRATION_NAME = 'UserTiming'; -const _userTimingSpansIntegration = ((options: UserTimingSpansOptions = {}) => { +const _userTimingIntegration = ((options: UserTimingOptions = {}) => { return { name: INTEGRATION_NAME, setup() { @@ -89,14 +89,14 @@ const _userTimingSpansIntegration = ((options: UserTimingSpansOptions = {}) => { * Sentry.init({ * integrations: [ * Sentry.browserTracingIntegration(), - * Sentry.userTimingSpansIntegration({ + * Sentry.userTimingIntegration({ * ignore: ['third-party-mark', /framework-measure/], * }), * ], * }); * ``` */ -export const userTimingSpansIntegration = defineIntegration(_userTimingSpansIntegration); +export const userTimingIntegration = defineIntegration(_userTimingIntegration); /** * Creates a span for a browser User Timing entry. @@ -121,7 +121,7 @@ export function _addUserTimingSpan( const spanEndTimestamp = originalStartTimestamp + duration; const attributes: SpanAttributes = { - [SENTRY_ORIGIN]: 'auto.resource.browser.metrics', + [SENTRY_ORIGIN]: 'auto.resource.browser.user_timing', }; if (spanStartTimestamp !== originalStartTimestamp) { diff --git a/packages/browser-utils/test/metrics/userTimingSpans.test.ts b/packages/browser-utils/test/metrics/userTiming.test.ts similarity index 92% rename from packages/browser-utils/test/metrics/userTimingSpans.test.ts rename to packages/browser-utils/test/metrics/userTiming.test.ts index 654c0ca2a30c..2c0932a3c158 100644 --- a/packages/browser-utils/test/metrics/userTimingSpans.test.ts +++ b/packages/browser-utils/test/metrics/userTiming.test.ts @@ -10,12 +10,12 @@ import { } from '@sentry/core'; import { beforeEach, describe, expect, it, vi } from 'vitest'; import * as instrument from '../../src/metrics/instrument'; -import { _addUserTimingSpan, userTimingSpansIntegration } from '../../src/metrics/userTimingSpans'; +import { _addUserTimingSpan, userTimingIntegration } from '../../src/metrics/userTiming'; import { getDefaultClientOptions, TestClient } from '../utils/TestClient'; type PerformanceEntryHandler = (data: { entries: PerformanceEntry[] }) => void; -describe('userTimingSpansIntegration', () => { +describe('userTimingIntegration', () => { let handlers: Map; let spans: Span[]; @@ -41,7 +41,7 @@ describe('userTimingSpansIntegration', () => { }); it('captures mark and measure entries as child spans', () => { - userTimingSpansIntegration().setup?.({} as never); + userTimingIntegration().setup?.({} as never); const parentSpan = new SentrySpan({ op: 'pageload', name: '/', sampled: true }); withActiveSpan(parentSpan, () => { @@ -61,7 +61,7 @@ describe('userTimingSpansIntegration', () => { }); it('does not capture entries without an active span', () => { - userTimingSpansIntegration().setup?.({} as never); + userTimingIntegration().setup?.({} as never); handlers.get('measure')!({ entries: [createPerformanceEntry('measure', 'background-work', 14, 25)], @@ -71,7 +71,7 @@ describe('userTimingSpansIntegration', () => { }); it('attaches entries to the root pageload span even when a child span is active', () => { - userTimingSpansIntegration().setup?.({} as never); + userTimingIntegration().setup?.({} as never); const rootSpan = new SentrySpan({ op: 'pageload', name: '/', sampled: true }); withActiveSpan(rootSpan, () => { @@ -89,7 +89,7 @@ describe('userTimingSpansIntegration', () => { }); it('does not capture entries when the active span is not a pageload or navigation', () => { - userTimingSpansIntegration().setup?.({} as never); + userTimingIntegration().setup?.({} as never); const rootSpan = new SentrySpan({ op: 'ui.action', name: 'click', sampled: true }); withActiveSpan(rootSpan, () => { @@ -102,7 +102,7 @@ describe('userTimingSpansIntegration', () => { }); it('ignores entries matching strings and regular expressions', () => { - userTimingSpansIntegration({ ignore: ['extension-mark', /^framework-/] }).setup?.({} as never); + userTimingIntegration({ ignore: ['extension-mark', /^framework-/] }).setup?.({} as never); const parentSpan = new SentrySpan({ op: 'pageload', name: '/', sampled: true }); withActiveSpan(parentSpan, () => { @@ -125,7 +125,7 @@ describe('userTimingSpansIntegration', () => { }); it('does not attach entries preceding a navigation span', () => { - userTimingSpansIntegration().setup?.({} as never); + userTimingIntegration().setup?.({} as never); const timeOrigin = performance.timeOrigin / 1000; const parentSpan = new SentrySpan({ op: 'navigation', @@ -184,7 +184,7 @@ describe('_addUserTimingSpan', () => { 'sentry.browser.measure.detail.phase': 'client', 'sentry.browser.measure.detail.counts': '{"components":4}', 'sentry.op': 'measure', - 'sentry.origin': 'auto.resource.browser.metrics', + 'sentry.origin': 'auto.resource.browser.user_timing', }); }); diff --git a/packages/browser/src/index.bundle.tracing.logs.metrics.ts b/packages/browser/src/index.bundle.tracing.logs.metrics.ts index a3c5e88d3d06..ffe111648c40 100644 --- a/packages/browser/src/index.bundle.tracing.logs.metrics.ts +++ b/packages/browser/src/index.bundle.tracing.logs.metrics.ts @@ -33,7 +33,7 @@ export { setActiveSpanInBrowser } from './tracing/setActiveSpan'; export { spanStreamingIntegration } from './integrations/spanstreaming'; export { fetchStreamPerformanceIntegration } from './integrations/fetchStreamPerformance'; export { webVitalsIntegration } from './integrations/webVitals'; -export { userTimingSpansIntegration } from '@sentry/browser-utils'; +export { userTimingIntegration } from '@sentry/browser-utils'; export { feedbackIntegrationShim as feedbackAsyncIntegration, diff --git a/packages/browser/src/index.bundle.tracing.replay.feedback.logs.metrics.ts b/packages/browser/src/index.bundle.tracing.replay.feedback.logs.metrics.ts index 5b7492a590df..4c8365c28b95 100644 --- a/packages/browser/src/index.bundle.tracing.replay.feedback.logs.metrics.ts +++ b/packages/browser/src/index.bundle.tracing.replay.feedback.logs.metrics.ts @@ -33,7 +33,7 @@ export { setActiveSpanInBrowser } from './tracing/setActiveSpan'; export { spanStreamingIntegration } from './integrations/spanstreaming'; export { fetchStreamPerformanceIntegration } from './integrations/fetchStreamPerformance'; export { webVitalsIntegration } from './integrations/webVitals'; -export { userTimingSpansIntegration } from '@sentry/browser-utils'; +export { userTimingIntegration } from '@sentry/browser-utils'; export { getFeedback, sendFeedback } from '@sentry/feedback'; diff --git a/packages/browser/src/index.bundle.tracing.replay.feedback.ts b/packages/browser/src/index.bundle.tracing.replay.feedback.ts index 8a3fad48fc00..04c892f802df 100644 --- a/packages/browser/src/index.bundle.tracing.replay.feedback.ts +++ b/packages/browser/src/index.bundle.tracing.replay.feedback.ts @@ -39,7 +39,7 @@ export { reportPageLoaded } from './tracing/reportPageLoaded'; export { spanStreamingIntegration } from './integrations/spanstreaming'; export { fetchStreamPerformanceIntegration } from './integrations/fetchStreamPerformance'; export { webVitalsIntegration } from './integrations/webVitals'; -export { userTimingSpansIntegration } from '@sentry/browser-utils'; +export { userTimingIntegration } from '@sentry/browser-utils'; export { getFeedback, sendFeedback } from '@sentry/feedback'; diff --git a/packages/browser/src/index.bundle.tracing.replay.logs.metrics.ts b/packages/browser/src/index.bundle.tracing.replay.logs.metrics.ts index 6a9f5d7b7092..52ce1a4dc258 100644 --- a/packages/browser/src/index.bundle.tracing.replay.logs.metrics.ts +++ b/packages/browser/src/index.bundle.tracing.replay.logs.metrics.ts @@ -33,7 +33,7 @@ export { setActiveSpanInBrowser } from './tracing/setActiveSpan'; export { spanStreamingIntegration } from './integrations/spanstreaming'; export { fetchStreamPerformanceIntegration } from './integrations/fetchStreamPerformance'; export { webVitalsIntegration } from './integrations/webVitals'; -export { userTimingSpansIntegration } from '@sentry/browser-utils'; +export { userTimingIntegration } from '@sentry/browser-utils'; export { feedbackIntegrationShim as feedbackAsyncIntegration, feedbackIntegrationShim as feedbackIntegration }; diff --git a/packages/browser/src/index.bundle.tracing.replay.ts b/packages/browser/src/index.bundle.tracing.replay.ts index 46e2cbcf27cc..b8dfe11158b2 100644 --- a/packages/browser/src/index.bundle.tracing.replay.ts +++ b/packages/browser/src/index.bundle.tracing.replay.ts @@ -38,7 +38,7 @@ export { setActiveSpanInBrowser } from './tracing/setActiveSpan'; export { spanStreamingIntegration } from './integrations/spanstreaming'; export { fetchStreamPerformanceIntegration } from './integrations/fetchStreamPerformance'; export { webVitalsIntegration } from './integrations/webVitals'; -export { userTimingSpansIntegration } from '@sentry/browser-utils'; +export { userTimingIntegration } from '@sentry/browser-utils'; export { feedbackIntegrationShim as feedbackAsyncIntegration, feedbackIntegrationShim as feedbackIntegration }; diff --git a/packages/browser/src/index.bundle.tracing.ts b/packages/browser/src/index.bundle.tracing.ts index 70b9b629d759..b60f215d9be9 100644 --- a/packages/browser/src/index.bundle.tracing.ts +++ b/packages/browser/src/index.bundle.tracing.ts @@ -40,7 +40,7 @@ export { reportPageLoaded } from './tracing/reportPageLoaded'; export { spanStreamingIntegration } from './integrations/spanstreaming'; export { fetchStreamPerformanceIntegration } from './integrations/fetchStreamPerformance'; export { webVitalsIntegration } from './integrations/webVitals'; -export { userTimingSpansIntegration } from '@sentry/browser-utils'; +export { userTimingIntegration } from '@sentry/browser-utils'; export { feedbackIntegrationShim as feedbackAsyncIntegration, diff --git a/packages/browser/src/index.ts b/packages/browser/src/index.ts index c242e43f2f84..e93c74997a0b 100644 --- a/packages/browser/src/index.ts +++ b/packages/browser/src/index.ts @@ -48,7 +48,7 @@ export { setActiveSpanInBrowser } from './tracing/setActiveSpan'; export { spanStreamingIntegration } from './integrations/spanstreaming'; export { fetchStreamPerformanceIntegration } from './integrations/fetchStreamPerformance'; export { webVitalsIntegration } from './integrations/webVitals'; -export { userTimingSpansIntegration } from '@sentry/browser-utils'; +export { userTimingIntegration } from '@sentry/browser-utils'; export type { RequestInstrumentationOptions } from './tracing/request'; export { diff --git a/packages/browser/test/index.bundle.tracing.logs.metrics.test.ts b/packages/browser/test/index.bundle.tracing.logs.metrics.test.ts index 1ee2fb99e315..1a81580bc3fb 100644 --- a/packages/browser/test/index.bundle.tracing.logs.metrics.test.ts +++ b/packages/browser/test/index.bundle.tracing.logs.metrics.test.ts @@ -4,7 +4,7 @@ import { describe, expect, it } from 'vitest'; import { browserTracingIntegration, spanStreamingIntegration, - userTimingSpansIntegration, + userTimingIntegration, webVitalsIntegration, } from '../src'; import * as TracingLogsMetricsBundle from '../src/index.bundle.tracing.logs.metrics'; @@ -16,7 +16,7 @@ describe('index.bundle.tracing.logs.metrics', () => { expect(TracingLogsMetricsBundle.feedbackIntegration).toBe(feedbackIntegrationShim); expect(TracingLogsMetricsBundle.replayIntegration).toBe(replayIntegrationShim); expect(TracingLogsMetricsBundle.spanStreamingIntegration).toBe(spanStreamingIntegration); - expect(TracingLogsMetricsBundle.userTimingSpansIntegration).toBe(userTimingSpansIntegration); + expect(TracingLogsMetricsBundle.userTimingIntegration).toBe(userTimingIntegration); expect(TracingLogsMetricsBundle.webVitalsIntegration).toBe(webVitalsIntegration); expect(TracingLogsMetricsBundle.logger).toBe(coreLogger); diff --git a/packages/browser/test/index.bundle.tracing.replay.feedback.logs.metrics.test.ts b/packages/browser/test/index.bundle.tracing.replay.feedback.logs.metrics.test.ts index 77b47757bcf9..c13709e995f3 100644 --- a/packages/browser/test/index.bundle.tracing.replay.feedback.logs.metrics.test.ts +++ b/packages/browser/test/index.bundle.tracing.replay.feedback.logs.metrics.test.ts @@ -5,7 +5,7 @@ import { feedbackAsyncIntegration, replayIntegration, spanStreamingIntegration, - userTimingSpansIntegration, + userTimingIntegration, webVitalsIntegration, } from '../src'; import * as TracingReplayFeedbackLogsMetricsBundle from '../src/index.bundle.tracing.replay.feedback.logs.metrics'; @@ -17,7 +17,7 @@ describe('index.bundle.tracing.replay.feedback.logs.metrics', () => { expect(TracingReplayFeedbackLogsMetricsBundle.feedbackIntegration).toBe(feedbackAsyncIntegration); expect(TracingReplayFeedbackLogsMetricsBundle.replayIntegration).toBe(replayIntegration); expect(TracingReplayFeedbackLogsMetricsBundle.spanStreamingIntegration).toBe(spanStreamingIntegration); - expect(TracingReplayFeedbackLogsMetricsBundle.userTimingSpansIntegration).toBe(userTimingSpansIntegration); + expect(TracingReplayFeedbackLogsMetricsBundle.userTimingIntegration).toBe(userTimingIntegration); expect(TracingReplayFeedbackLogsMetricsBundle.webVitalsIntegration).toBe(webVitalsIntegration); expect(TracingReplayFeedbackLogsMetricsBundle.logger).toBe(coreLogger); diff --git a/packages/browser/test/index.bundle.tracing.replay.feedback.test.ts b/packages/browser/test/index.bundle.tracing.replay.feedback.test.ts index ccd348f67416..bdc494e8fd86 100644 --- a/packages/browser/test/index.bundle.tracing.replay.feedback.test.ts +++ b/packages/browser/test/index.bundle.tracing.replay.feedback.test.ts @@ -5,7 +5,7 @@ import { feedbackAsyncIntegration, replayIntegration, spanStreamingIntegration, - userTimingSpansIntegration, + userTimingIntegration, webVitalsIntegration, } from '../src'; import * as TracingReplayFeedbackBundle from '../src/index.bundle.tracing.replay.feedback'; @@ -17,7 +17,7 @@ describe('index.bundle.tracing.replay.feedback', () => { expect(TracingReplayFeedbackBundle.feedbackIntegration).toBe(feedbackAsyncIntegration); expect(TracingReplayFeedbackBundle.replayIntegration).toBe(replayIntegration); expect(TracingReplayFeedbackBundle.spanStreamingIntegration).toBe(spanStreamingIntegration); - expect(TracingReplayFeedbackBundle.userTimingSpansIntegration).toBe(userTimingSpansIntegration); + expect(TracingReplayFeedbackBundle.userTimingIntegration).toBe(userTimingIntegration); expect(TracingReplayFeedbackBundle.webVitalsIntegration).toBe(webVitalsIntegration); expect(TracingReplayFeedbackBundle.logger).toBe(loggerShim); diff --git a/packages/browser/test/index.bundle.tracing.replay.logs.metrics.test.ts b/packages/browser/test/index.bundle.tracing.replay.logs.metrics.test.ts index 27923e73cd16..6caf07d28ab6 100644 --- a/packages/browser/test/index.bundle.tracing.replay.logs.metrics.test.ts +++ b/packages/browser/test/index.bundle.tracing.replay.logs.metrics.test.ts @@ -5,7 +5,7 @@ import { browserTracingIntegration, replayIntegration, spanStreamingIntegration, - userTimingSpansIntegration, + userTimingIntegration, webVitalsIntegration, } from '../src'; import * as TracingReplayLogsMetricsBundle from '../src/index.bundle.tracing.replay.logs.metrics'; @@ -17,7 +17,7 @@ describe('index.bundle.tracing.replay.logs.metrics', () => { expect(TracingReplayLogsMetricsBundle.feedbackIntegration).toBe(feedbackIntegrationShim); expect(TracingReplayLogsMetricsBundle.replayIntegration).toBe(replayIntegration); expect(TracingReplayLogsMetricsBundle.spanStreamingIntegration).toBe(spanStreamingIntegration); - expect(TracingReplayLogsMetricsBundle.userTimingSpansIntegration).toBe(userTimingSpansIntegration); + expect(TracingReplayLogsMetricsBundle.userTimingIntegration).toBe(userTimingIntegration); expect(TracingReplayLogsMetricsBundle.webVitalsIntegration).toBe(webVitalsIntegration); expect(TracingReplayLogsMetricsBundle.logger).toBe(coreLogger); diff --git a/packages/browser/test/index.bundle.tracing.replay.test.ts b/packages/browser/test/index.bundle.tracing.replay.test.ts index 75fe2970acd8..780b56b9552d 100644 --- a/packages/browser/test/index.bundle.tracing.replay.test.ts +++ b/packages/browser/test/index.bundle.tracing.replay.test.ts @@ -4,7 +4,7 @@ import { browserTracingIntegration, replayIntegration, spanStreamingIntegration, - userTimingSpansIntegration, + userTimingIntegration, webVitalsIntegration, } from '../src'; import * as TracingReplayBundle from '../src/index.bundle.tracing.replay'; @@ -16,7 +16,7 @@ describe('index.bundle.tracing.replay', () => { expect(TracingReplayBundle.feedbackIntegration).toBe(feedbackIntegrationShim); expect(TracingReplayBundle.replayIntegration).toBe(replayIntegration); expect(TracingReplayBundle.spanStreamingIntegration).toBe(spanStreamingIntegration); - expect(TracingReplayBundle.userTimingSpansIntegration).toBe(userTimingSpansIntegration); + expect(TracingReplayBundle.userTimingIntegration).toBe(userTimingIntegration); expect(TracingReplayBundle.webVitalsIntegration).toBe(webVitalsIntegration); expect(TracingReplayBundle.logger).toBe(loggerShim); diff --git a/packages/browser/test/index.bundle.tracing.test.ts b/packages/browser/test/index.bundle.tracing.test.ts index 80aed0b06bc0..71d4387d3b3b 100644 --- a/packages/browser/test/index.bundle.tracing.test.ts +++ b/packages/browser/test/index.bundle.tracing.test.ts @@ -8,7 +8,7 @@ import { describe, expect, it } from 'vitest'; import { browserTracingIntegration, spanStreamingIntegration, - userTimingSpansIntegration, + userTimingIntegration, webVitalsIntegration, } from '../src'; import * as TracingBundle from '../src/index.bundle.tracing'; @@ -20,7 +20,7 @@ describe('index.bundle.tracing', () => { expect(TracingBundle.feedbackIntegration).toBe(feedbackIntegrationShim); expect(TracingBundle.replayIntegration).toBe(replayIntegrationShim); expect(TracingBundle.spanStreamingIntegration).toBe(spanStreamingIntegration); - expect(TracingBundle.userTimingSpansIntegration).toBe(userTimingSpansIntegration); + expect(TracingBundle.userTimingIntegration).toBe(userTimingIntegration); expect(TracingBundle.webVitalsIntegration).toBe(webVitalsIntegration); expect(TracingBundle.logger).toBe(loggerShim); From 88d45ad3652b5e49a3cb36dd1eadb6b51bc35f01 Mon Sep 17 00:00:00 2001 From: Martin Sonnberger Date: Tue, 28 Jul 2026 10:47:16 +0200 Subject: [PATCH 04/12] use new `beforeIdleSpanEnd` hook instead of performance observer --- .../browser-utils/src/metrics/instrument.ts | 2 - .../browser-utils/src/metrics/userTiming.ts | 39 +++-- .../src/metrics/web-vitals/lib/observe.ts | 2 - .../test/metrics/userTiming.test.ts | 147 ++++++++---------- packages/core/src/client.ts | 6 + packages/core/src/tracing/idleSpan.ts | 2 + .../core/test/lib/tracing/idleSpan.test.ts | 18 +++ 7 files changed, 107 insertions(+), 109 deletions(-) diff --git a/packages/browser-utils/src/metrics/instrument.ts b/packages/browser-utils/src/metrics/instrument.ts index 0590b421ce39..608a5fd11511 100644 --- a/packages/browser-utils/src/metrics/instrument.ts +++ b/packages/browser-utils/src/metrics/instrument.ts @@ -10,8 +10,6 @@ type InstrumentHandlerTypePerformanceObserver = | 'longtask' | 'event' | 'navigation' - | 'mark' - | 'measure' | 'paint' | 'resource' | 'element' diff --git a/packages/browser-utils/src/metrics/userTiming.ts b/packages/browser-utils/src/metrics/userTiming.ts index 5cd889f7ab17..961534707340 100644 --- a/packages/browser-utils/src/metrics/userTiming.ts +++ b/packages/browser-utils/src/metrics/userTiming.ts @@ -3,14 +3,11 @@ import type { IntegrationFn, Span, SpanAttributes, SpanAttributeValue } from '@s import { browserPerformanceTimeOrigin, defineIntegration, - getActiveSpan, - getRootSpan, isPrimitive, spanToJSON, stringMatchesSomePattern, } from '@sentry/core'; -import { addPerformanceInstrumentationHandler } from './instrument'; -import { msToSec, startAndEndSpan } from './utils'; +import { getBrowserPerformanceAPI, msToSec, startAndEndSpan } from './utils'; import { getNavigationEntry } from './web-vitals/lib/getNavigationEntry'; interface UserTimingOptions { @@ -27,32 +24,33 @@ const INTEGRATION_NAME = 'UserTiming'; const _userTimingIntegration = ((options: UserTimingOptions = {}) => { return { name: INTEGRATION_NAME, - setup() { + setup(client) { + const performance = getBrowserPerformanceAPI(); const timeOrigin = browserPerformanceTimeOrigin(); - if (!timeOrigin) { + if (!performance?.getEntries || !timeOrigin) { return; } const timeOriginInSeconds = msToSec(timeOrigin); + let performanceCursor = 0; - const handleEntries = ({ entries }: { entries: PerformanceEntry[] }): void => { - const activeSpan = getActiveSpan(); - if (!activeSpan) { + client.on('beforeIdleSpanEnd', idleSpan => { + if (!idleSpan.isRecording()) { return; } - // Attach entries to the pageload/navigation root span rather than whatever child span happens to - // be active when the browser reports them, so they consistently nest under the transaction. - const rootSpan = getRootSpan(activeSpan); - const { op: parentOp, start_timestamp: parentStartTimestamp } = spanToJSON(rootSpan); + const { op: parentOp, start_timestamp: parentStartTimestamp } = spanToJSON(idleSpan); if (parentOp !== 'pageload' && parentOp !== 'navigation') { return; } - // The navigation entry is stable for the lifetime of the document, so resolve it once per batch - // rather than for every entry. const requestTime = msToSec(getNavigationEntry(false)?.requestStart ?? 0); + const performanceEntries = performance.getEntries(); + + for (const entry of performanceEntries.slice(performanceCursor)) { + if (entry.entryType !== 'mark' && entry.entryType !== 'measure') { + continue; + } - for (const entry of entries) { const startTime = msToSec(entry.startTime); const absoluteStartTime = timeOriginInSeconds + startTime; @@ -61,7 +59,7 @@ const _userTimingIntegration = ((options: UserTimingOptions = {}) => { } _addUserTimingSpan( - rootSpan, + idleSpan, entry, startTime, msToSec(Math.max(0, entry.duration)), @@ -70,10 +68,9 @@ const _userTimingIntegration = ((options: UserTimingOptions = {}) => { options.ignore ?? [], ); } - }; - addPerformanceInstrumentationHandler('mark', handleEntries); - addPerformanceInstrumentationHandler('measure', handleEntries); + performanceCursor = performanceEntries.length; + }); }, }; }) satisfies IntegrationFn; @@ -82,7 +79,7 @@ const _userTimingIntegration = ((options: UserTimingOptions = {}) => { * Captures spans created with the browser's User Timing APIs, `performance.mark` and `performance.measure`. * * The integration must be explicitly added to `Sentry.init`. Entries are attached to the active pageload or - * navigation span when the browser reports them through the PerformanceObserver API. + * navigation span when it ends. * * @example * ```ts diff --git a/packages/browser-utils/src/metrics/web-vitals/lib/observe.ts b/packages/browser-utils/src/metrics/web-vitals/lib/observe.ts index 8c52446cfacd..6071893dfa8e 100644 --- a/packages/browser-utils/src/metrics/web-vitals/lib/observe.ts +++ b/packages/browser-utils/src/metrics/web-vitals/lib/observe.ts @@ -21,8 +21,6 @@ interface PerformanceEntryMap { 'largest-contentful-paint': LargestContentfulPaint[]; 'long-animation-frame': PerformanceLongAnimationFrameTiming[]; paint: PerformancePaintTiming[]; - mark: PerformanceMark[]; - measure: PerformanceMeasure[]; navigation: PerformanceNavigationTiming[]; resource: PerformanceResourceTiming[]; // Sentry-specific change: diff --git a/packages/browser-utils/test/metrics/userTiming.test.ts b/packages/browser-utils/test/metrics/userTiming.test.ts index 2c0932a3c158..e79c3f31df9c 100644 --- a/packages/browser-utils/test/metrics/userTiming.test.ts +++ b/packages/browser-utils/test/metrics/userTiming.test.ts @@ -1,22 +1,13 @@ import type { Span } from '@sentry/core'; -import { - getCurrentScope, - getIsolationScope, - SentrySpan, - setCurrentClient, - spanToJSON, - startInactiveSpan, - withActiveSpan, -} from '@sentry/core'; +import { getCurrentScope, getIsolationScope, SentrySpan, setCurrentClient, spanToJSON } from '@sentry/core'; import { beforeEach, describe, expect, it, vi } from 'vitest'; -import * as instrument from '../../src/metrics/instrument'; import { _addUserTimingSpan, userTimingIntegration } from '../../src/metrics/userTiming'; +import * as utils from '../../src/metrics/utils'; import { getDefaultClientOptions, TestClient } from '../utils/TestClient'; -type PerformanceEntryHandler = (data: { entries: PerformanceEntry[] }) => void; - describe('userTimingIntegration', () => { - let handlers: Map; + let client: TestClient; + let performanceEntries: PerformanceEntry[]; let spans: Span[]; beforeEach(() => { @@ -24,108 +15,99 @@ describe('userTimingIntegration', () => { getCurrentScope().clear(); getIsolationScope().clear(); - const client = new TestClient(getDefaultClientOptions({ tracesSampleRate: 1 })); + client = new TestClient(getDefaultClientOptions({ tracesSampleRate: 1 })); setCurrentClient(client); client.init(); + performanceEntries = []; + vi.spyOn(utils, 'getBrowserPerformanceAPI').mockReturnValue({ + getEntries: () => performanceEntries, + } as Performance); + spans = []; client.on('spanEnd', span => { spans.push(span); }); - - handlers = new Map(); - vi.spyOn(instrument, 'addPerformanceInstrumentationHandler').mockImplementation((type, handler) => { - handlers.set(type, handler); - return () => undefined; - }); }); - it('captures mark and measure entries as child spans', () => { - userTimingIntegration().setup?.({} as never); + it('captures mark and measure entries created before setup', () => { + performanceEntries.push( + createPerformanceEntry('mark', 'app-ready', 12, 0), + createPerformanceEntry('measure', 'hydrate', 14, 25), + ); + + userTimingIntegration().setup?.(client); const parentSpan = new SentrySpan({ op: 'pageload', name: '/', sampled: true }); - withActiveSpan(parentSpan, () => { - handlers.get('mark')!({ - entries: [createPerformanceEntry('mark', 'app-ready', 12, 0)], - }); - handlers.get('measure')!({ - entries: [createPerformanceEntry('measure', 'hydrate', 14, 25)], - }); - }); + client.emit('beforeIdleSpanEnd', parentSpan); expect(spans).toHaveLength(2); expect(spans.map(span => spanToJSON(span).description)).toEqual(['app-ready', 'hydrate']); expect(spans.map(span => spanToJSON(span).op)).toEqual(['mark', 'measure']); expect(spanToJSON(spans[0]!).timestamp).toBe(spanToJSON(spans[0]!).start_timestamp); expect(spanToJSON(spans[1]!).timestamp! - spanToJSON(spans[1]!).start_timestamp).toBeCloseTo(0.025); + expect(spanToJSON(spans[1]!).parent_span_id).toBe(parentSpan.spanContext().spanId); }); - it('does not capture entries without an active span', () => { - userTimingIntegration().setup?.({} as never); - - handlers.get('measure')!({ - entries: [createPerformanceEntry('measure', 'background-work', 14, 25)], - }); + it('captures only entries added since the previous idle span ended', () => { + userTimingIntegration().setup?.(client); + performanceEntries.push(createPerformanceEntry('mark', 'initial-render', 12, 0)); + + client.emit('beforeIdleSpanEnd', new SentrySpan({ op: 'pageload', name: '/', sampled: true })); + performanceEntries.push(createPerformanceEntry('measure', 'route-render', 30, 10)); + client.emit( + 'beforeIdleSpanEnd', + new SentrySpan({ + op: 'navigation', + name: '/settings', + sampled: true, + startTimestamp: performance.timeOrigin / 1000 + 0.02, + }), + ); - expect(spans).toHaveLength(0); + expect(spans).toHaveLength(2); + expect(spans.map(span => spanToJSON(span).description)).toEqual(['initial-render', 'route-render']); }); - it('attaches entries to the root pageload span even when a child span is active', () => { - userTimingIntegration().setup?.({} as never); - const rootSpan = new SentrySpan({ op: 'pageload', name: '/', sampled: true }); - - withActiveSpan(rootSpan, () => { - const childSpan = startInactiveSpan({ name: 'child' }); - withActiveSpan(childSpan, () => { - handlers.get('measure')!({ - entries: [createPerformanceEntry('measure', 'hydrate', 14, 25)], - }); - }); - }); + it('reads the latest entries immediately before the segment ends', () => { + userTimingIntegration().setup?.(client); + const parentSpan = new SentrySpan({ op: 'pageload', name: '/', sampled: true }); + + performanceEntries.push(createPerformanceEntry('measure', 'last-moment-work', 14, 25)); + client.emit('beforeIdleSpanEnd', parentSpan); - const measureSpan = spans.find(span => spanToJSON(span).description === 'hydrate'); - expect(measureSpan).toBeDefined(); - expect(spanToJSON(measureSpan!).parent_span_id).toBe(rootSpan.spanContext().spanId); + expect(spans).toHaveLength(1); + expect(spanToJSON(spans[0]!).description).toBe('last-moment-work'); }); - it('does not capture entries when the active span is not a pageload or navigation', () => { - userTimingIntegration().setup?.({} as never); - const rootSpan = new SentrySpan({ op: 'ui.action', name: 'click', sampled: true }); + it('does not capture entries for unrelated idle spans', () => { + userTimingIntegration().setup?.(client); + const idleSpan = new SentrySpan({ op: 'ui.action', name: 'click', sampled: true }); + performanceEntries.push(createPerformanceEntry('measure', 'work', 14, 25)); - withActiveSpan(rootSpan, () => { - handlers.get('measure')!({ - entries: [createPerformanceEntry('measure', 'work', 14, 25)], - }); - }); + client.emit('beforeIdleSpanEnd', idleSpan); expect(spans).toHaveLength(0); }); it('ignores entries matching strings and regular expressions', () => { - userTimingIntegration({ ignore: ['extension-mark', /^framework-/] }).setup?.({} as never); + userTimingIntegration({ ignore: ['extension-mark', /^framework-/] }).setup?.(client); const parentSpan = new SentrySpan({ op: 'pageload', name: '/', sampled: true }); - withActiveSpan(parentSpan, () => { - handlers.get('mark')!({ - entries: [ - createPerformanceEntry('mark', 'extension-mark', 10, 0), - createPerformanceEntry('mark', 'application-mark', 11, 0), - ], - }); - handlers.get('measure')!({ - entries: [ - createPerformanceEntry('measure', 'framework-render', 12, 10), - createPerformanceEntry('measure', 'application-render', 13, 10), - ], - }); - }); + performanceEntries.push( + createPerformanceEntry('mark', 'extension-mark', 10, 0), + createPerformanceEntry('mark', 'application-mark', 11, 0), + createPerformanceEntry('measure', 'framework-render', 12, 10), + createPerformanceEntry('measure', 'application-render', 13, 10), + ); + client.emit('beforeIdleSpanEnd', parentSpan); expect(spans).toHaveLength(2); expect(spans.map(span => spanToJSON(span).description)).toEqual(['application-mark', 'application-render']); }); it('does not attach entries preceding a navigation span', () => { - userTimingIntegration().setup?.({} as never); + userTimingIntegration().setup?.(client); const timeOrigin = performance.timeOrigin / 1000; const parentSpan = new SentrySpan({ op: 'navigation', @@ -134,14 +116,11 @@ describe('userTimingIntegration', () => { startTimestamp: timeOrigin + 0.02, }); - withActiveSpan(parentSpan, () => { - handlers.get('measure')!({ - entries: [ - createPerformanceEntry('measure', 'previous-route', 10, 5), - createPerformanceEntry('measure', 'current-route', 30, 5), - ], - }); - }); + performanceEntries.push( + createPerformanceEntry('measure', 'previous-route', 10, 5), + createPerformanceEntry('measure', 'current-route', 30, 5), + ); + client.emit('beforeIdleSpanEnd', parentSpan); expect(spans).toHaveLength(1); expect(spanToJSON(spans[0]!).description).toBe('current-route'); diff --git a/packages/core/src/client.ts b/packages/core/src/client.ts index 6a11959e9983..836b4ca521c0 100644 --- a/packages/core/src/client.ts +++ b/packages/core/src/client.ts @@ -662,6 +662,9 @@ export abstract class Client { */ public on(hook: 'spanEnd', callback: (span: Span) => void): () => void; + /** Register a callback before an idle span ends and its end timestamp is finalized. */ + public on(hook: 'beforeIdleSpanEnd', callback: (idleSpan: Span) => void): () => void; + /** * Register a callback for after a span is ended and the `spanEnd` hook has run. * NOTE: The span cannot be mutated anymore in this callback. @@ -975,6 +978,9 @@ export abstract class Client { /** Fire a hook whenever a span ends. */ public emit(hook: 'spanEnd', span: Span): void; + /** Fire a hook before an idle span ends and its end timestamp is finalized. */ + public emit(hook: 'beforeIdleSpanEnd', idleSpan: Span): void; + /** * Fire a hook event after a span ends and the `spanEnd` hook has run. */ diff --git a/packages/core/src/tracing/idleSpan.ts b/packages/core/src/tracing/idleSpan.ts index 7deb0fe94c96..d38576f15e47 100644 --- a/packages/core/src/tracing/idleSpan.ts +++ b/packages/core/src/tracing/idleSpan.ts @@ -140,6 +140,8 @@ export function startIdleSpan(startSpanOptions: StartSpanOptions, options: Parti // eslint-disable-next-line @typescript-eslint/unbound-method span.end = new Proxy(span.end, { apply(target, thisArg, args: Parameters) { + client.emit('beforeIdleSpanEnd', span); + if (beforeSpanEnd) { beforeSpanEnd(span); } diff --git a/packages/core/test/lib/tracing/idleSpan.test.ts b/packages/core/test/lib/tracing/idleSpan.test.ts index 9ef6c834d251..c7cd1433a230 100644 --- a/packages/core/test/lib/tracing/idleSpan.test.ts +++ b/packages/core/test/lib/tracing/idleSpan.test.ts @@ -282,6 +282,24 @@ describe('startIdleSpan', () => { expect(transaction.spans).toEqual([expect.objectContaining({ description: 'from beforeSpanEnd' })]); }); + it('runs beforeIdleSpanEnd before trimming the idle span', () => { + const baseTimeInSeconds = Math.floor(Date.now() / 1000) - 9999; + const beforeIdleSpanEnd = vi.fn((span: Span) => { + expect(spanToJSON(span).timestamp).toBeUndefined(); + const childSpan = startInactiveSpan({ name: 'last-moment child', startTime: baseTimeInSeconds }); + childSpan.end(baseTimeInSeconds + 1); + }); + getClient()!.on('beforeIdleSpanEnd', beforeIdleSpanEnd); + + const idleSpan = startIdleSpan({ name: 'idle span', startTime: baseTimeInSeconds }); + vi.advanceTimersByTime(TRACING_DEFAULTS.idleTimeout + 1); + vi.runOnlyPendingTimers(); + + expect(beforeIdleSpanEnd).toHaveBeenCalledOnce(); + expect(beforeIdleSpanEnd).toHaveBeenCalledWith(idleSpan); + expect(spanToJSON(idleSpan).timestamp).toBe(baseTimeInSeconds + 1); + }); + it('filters spans on end', () => { const transactions: Event[] = []; const beforeSendTransaction = vi.fn(event => { From 3b6cc68b5682031a88c4380fe674e53fba37d250 Mon Sep 17 00:00:00 2001 From: Martin Sonnberger Date: Tue, 28 Jul 2026 10:50:33 +0200 Subject: [PATCH 05/12] migrate tests to span streaming --- .../user-timing-spans-disabled/test.ts | 17 ++++---- .../user-timing-spans-streamed/init.js | 13 ------ .../user-timing-spans-streamed/test.ts | 22 ---------- .../suites/tracing/user-timing-spans/init.js | 1 - .../suites/tracing/user-timing-spans/test.ts | 42 +++++++------------ 5 files changed, 21 insertions(+), 74 deletions(-) delete mode 100644 dev-packages/browser-integration-tests/suites/tracing/user-timing-spans-streamed/init.js delete mode 100644 dev-packages/browser-integration-tests/suites/tracing/user-timing-spans-streamed/test.ts diff --git a/dev-packages/browser-integration-tests/suites/tracing/user-timing-spans-disabled/test.ts b/dev-packages/browser-integration-tests/suites/tracing/user-timing-spans-disabled/test.ts index ee71b159d2e6..f516e263ed3a 100644 --- a/dev-packages/browser-integration-tests/suites/tracing/user-timing-spans-disabled/test.ts +++ b/dev-packages/browser-integration-tests/suites/tracing/user-timing-spans-disabled/test.ts @@ -1,21 +1,18 @@ import { expect } from '@playwright/test'; import { sentryTest } from '../../../utils/fixtures'; -import { envelopeRequestParser, shouldSkipTracingTest, waitForTransactionRequest } from '../../../utils/helpers'; +import { shouldSkipTracingTest } from '../../../utils/helpers'; +import { getSpanOp, waitForStreamedSpans } from '../../../utils/spanUtils'; sentryTest('does not capture mark and measure spans by default', async ({ getLocalTestUrl, page }) => { - if (shouldSkipTracingTest()) { - sentryTest.skip(); - } + sentryTest.skip(shouldSkipTracingTest()); const url = await getLocalTestUrl({ testDir: __dirname }); - const transactionRequestPromise = waitForTransactionRequest( - page, - event => event.type === 'transaction' && event.contexts?.trace?.op === 'pageload', - ); + const spansPromise = waitForStreamedSpans(page, spans => spans.some(span => getSpanOp(span) === 'pageload')); await page.goto(url); - const transactionEvent = envelopeRequestParser(await transactionRequestPromise); - const userTimingSpans = transactionEvent.spans?.filter(({ op }) => op === 'mark' || op === 'measure'); + const spans = await spansPromise; + const userTimingSpans = spans.filter(span => ['mark', 'measure'].includes(getSpanOp(span) ?? '')); + expect(userTimingSpans).toHaveLength(0); }); diff --git a/dev-packages/browser-integration-tests/suites/tracing/user-timing-spans-streamed/init.js b/dev-packages/browser-integration-tests/suites/tracing/user-timing-spans-streamed/init.js deleted file mode 100644 index 862c83a1a783..000000000000 --- a/dev-packages/browser-integration-tests/suites/tracing/user-timing-spans-streamed/init.js +++ /dev/null @@ -1,13 +0,0 @@ -import * as Sentry from '@sentry/browser'; - -window.Sentry = Sentry; - -Sentry.init({ - dsn: 'https://public@dsn.ingest.sentry.io/1337', - integrations: [Sentry.browserTracingIntegration(), Sentry.userTimingIntegration(), Sentry.spanStreamingIntegration()], - traceLifecycle: 'stream', - tracesSampleRate: 1, -}); - -performance.mark('app-ready'); -performance.measure('app-initialization'); diff --git a/dev-packages/browser-integration-tests/suites/tracing/user-timing-spans-streamed/test.ts b/dev-packages/browser-integration-tests/suites/tracing/user-timing-spans-streamed/test.ts deleted file mode 100644 index 5049b86e6d0f..000000000000 --- a/dev-packages/browser-integration-tests/suites/tracing/user-timing-spans-streamed/test.ts +++ /dev/null @@ -1,22 +0,0 @@ -import { expect } from '@playwright/test'; -import { sentryTest } from '../../../utils/fixtures'; -import { shouldSkipTracingTest } from '../../../utils/helpers'; -import { getSpanOp, waitForStreamedSpans } from '../../../utils/spanUtils'; - -sentryTest('captures each mark and measure once with span streaming', async ({ getLocalTestUrl, page }) => { - sentryTest.skip(shouldSkipTracingTest()); - - const url = await getLocalTestUrl({ testDir: __dirname }); - const spansPromise = waitForStreamedSpans(page, spans => spans.some(span => getSpanOp(span) === 'pageload')); - - await page.goto(url); - - const spans = await spansPromise; - const userTimingSpans = spans.filter(span => ['mark', 'measure'].includes(getSpanOp(span) ?? '')); - expect(userTimingSpans).toHaveLength(3); - expect(userTimingSpans.map(span => span.name).sort()).toEqual([ - 'app-initialization', - 'app-ready', - 'sentry-tracing-init', - ]); -}); diff --git a/dev-packages/browser-integration-tests/suites/tracing/user-timing-spans/init.js b/dev-packages/browser-integration-tests/suites/tracing/user-timing-spans/init.js index 3bf7dcc8495e..00a357fe978d 100644 --- a/dev-packages/browser-integration-tests/suites/tracing/user-timing-spans/init.js +++ b/dev-packages/browser-integration-tests/suites/tracing/user-timing-spans/init.js @@ -3,7 +3,6 @@ import * as Sentry from '@sentry/browser'; window.Sentry = Sentry; Sentry.init({ - traceLifecycle: 'static', dsn: 'https://public@dsn.ingest.sentry.io/1337', integrations: [ Sentry.browserTracingIntegration(), diff --git a/dev-packages/browser-integration-tests/suites/tracing/user-timing-spans/test.ts b/dev-packages/browser-integration-tests/suites/tracing/user-timing-spans/test.ts index 86c79a9bf8d7..339aea5cc677 100644 --- a/dev-packages/browser-integration-tests/suites/tracing/user-timing-spans/test.ts +++ b/dev-packages/browser-integration-tests/suites/tracing/user-timing-spans/test.ts @@ -1,39 +1,25 @@ import { expect } from '@playwright/test'; import { sentryTest } from '../../../utils/fixtures'; -import { envelopeRequestParser, shouldSkipTracingTest, waitForTransactionRequest } from '../../../utils/helpers'; +import { shouldSkipTracingTest } from '../../../utils/helpers'; +import { getSpanOp, waitForStreamedSpans } from '../../../utils/spanUtils'; sentryTest('captures non-ignored mark and measure spans', async ({ getLocalTestUrl, page }) => { - if (shouldSkipTracingTest()) { - sentryTest.skip(); - } + sentryTest.skip(shouldSkipTracingTest()); const url = await getLocalTestUrl({ testDir: __dirname }); - - const transactionRequestPromise = waitForTransactionRequest( - page, - evt => evt.type === 'transaction' && evt.contexts?.trace?.op === 'pageload', - ); + const spansPromise = waitForStreamedSpans(page, spans => spans.some(span => getSpanOp(span) === 'pageload')); await page.goto(url); - const transactionEvent = envelopeRequestParser(await transactionRequestPromise); - const markAndMeasureSpans = transactionEvent.spans?.filter(({ op }) => op && ['mark', 'measure'].includes(op)); + const spans = await spansPromise; + const userTimingSpans = spans + .filter(span => ['mark', 'measure'].includes(getSpanOp(span) ?? '')) + .map(span => ({ name: span.name, op: getSpanOp(span) })) + .sort((a, b) => a.name.localeCompare(b.name)); - 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', - }), - ]), - ); + expect(userTimingSpans).toEqual([ + { name: 'mark-pass', op: 'mark' }, + { name: 'measure-pass', op: 'measure' }, + { name: 'sentry-tracing-init', op: 'mark' }, + ]); }); From df7f7ce073c47322dadf486785ec826ed1fa8bd3 Mon Sep 17 00:00:00 2001 From: Martin Sonnberger Date: Tue, 28 Jul 2026 10:55:05 +0200 Subject: [PATCH 06/12] remove sentry-tracing-init mark --- .../suites/tracing/user-timing-spans/test.ts | 1 - packages/browser-utils/src/metrics/browserMetrics.ts | 5 ----- 2 files changed, 6 deletions(-) diff --git a/dev-packages/browser-integration-tests/suites/tracing/user-timing-spans/test.ts b/dev-packages/browser-integration-tests/suites/tracing/user-timing-spans/test.ts index 339aea5cc677..da10ce76c474 100644 --- a/dev-packages/browser-integration-tests/suites/tracing/user-timing-spans/test.ts +++ b/dev-packages/browser-integration-tests/suites/tracing/user-timing-spans/test.ts @@ -20,6 +20,5 @@ sentryTest('captures non-ignored mark and measure spans', async ({ getLocalTestU expect(userTimingSpans).toEqual([ { name: 'mark-pass', op: 'mark' }, { name: 'measure-pass', op: 'measure' }, - { name: 'sentry-tracing-init', op: 'mark' }, ]); }); diff --git a/packages/browser-utils/src/metrics/browserMetrics.ts b/packages/browser-utils/src/metrics/browserMetrics.ts index dcac15fe9e0b..f1e28a7d50b5 100644 --- a/packages/browser-utils/src/metrics/browserMetrics.ts +++ b/packages/browser-utils/src/metrics/browserMetrics.ts @@ -88,11 +88,6 @@ interface StartTrackingWebVitalsOptions { export function startTrackingWebVitals({ trackCls, trackLcp }: StartTrackingWebVitalsOptions): () => void { const performance = getBrowserPerformanceAPI(); if (performance && browserPerformanceTimeOrigin()) { - // @ts-expect-error we want to make sure all of these are available, even if TS is sure they are - if (performance.mark) { - WINDOW.performance.mark('sentry-tracing-init'); - } - const lcpCleanupCallback = trackLcp ? _trackLCP() : undefined; const clsCleanupCallback = trackCls ? _trackCLS() : undefined; const ttfbCleanupCallback = _trackTtfb(); From 1fddf597200a0c2f15413e9e8c964f34e6ea821d Mon Sep 17 00:00:00 2001 From: Martin Sonnberger Date: Tue, 28 Jul 2026 11:17:19 +0200 Subject: [PATCH 07/12] remove non-recording guard --- packages/browser-utils/src/metrics/userTiming.ts | 4 ---- 1 file changed, 4 deletions(-) diff --git a/packages/browser-utils/src/metrics/userTiming.ts b/packages/browser-utils/src/metrics/userTiming.ts index 961534707340..106d1cb1e1a6 100644 --- a/packages/browser-utils/src/metrics/userTiming.ts +++ b/packages/browser-utils/src/metrics/userTiming.ts @@ -34,10 +34,6 @@ const _userTimingIntegration = ((options: UserTimingOptions = {}) => { let performanceCursor = 0; client.on('beforeIdleSpanEnd', idleSpan => { - if (!idleSpan.isRecording()) { - return; - } - const { op: parentOp, start_timestamp: parentStartTimestamp } = spanToJSON(idleSpan); if (parentOp !== 'pageload' && parentOp !== 'navigation') { return; From 62cd1ff9010dcd9bdc379d14aa12f783f6529118 Mon Sep 17 00:00:00 2001 From: Martin Sonnberger Date: Tue, 28 Jul 2026 13:01:04 +0200 Subject: [PATCH 08/12] sentry.origin --- .../test.ts | 6 +++--- .../tracing/metrics/pageload-measure-spans/test.ts | 2 +- .../suites/tracing/user-timing-spans/test.ts | 10 +++++++--- packages/browser-utils/src/metrics/userTiming.ts | 2 +- packages/browser-utils/test/metrics/userTiming.test.ts | 2 +- 5 files changed, 13 insertions(+), 9 deletions(-) diff --git a/dev-packages/browser-integration-tests/suites/tracing/metrics/pageload-measure-spans-domexception-details/test.ts b/dev-packages/browser-integration-tests/suites/tracing/metrics/pageload-measure-spans-domexception-details/test.ts index 036c8fb8e0a9..7a39e97420e3 100644 --- a/dev-packages/browser-integration-tests/suites/tracing/metrics/pageload-measure-spans-domexception-details/test.ts +++ b/dev-packages/browser-integration-tests/suites/tracing/metrics/pageload-measure-spans-domexception-details/test.ts @@ -25,7 +25,7 @@ sentryTest( expect(restrictedMeasure).toBeDefined(); expect(restrictedMeasure?.data).toMatchObject({ 'sentry.op': 'measure', - 'sentry.origin': 'auto.resource.browser.user_timing', + 'sentry.origin': 'auto.browser.user_timing.measure', }); // Verify no detail attributes were added due to the permission error @@ -39,7 +39,7 @@ sentryTest( expect(normalMeasure?.data).toMatchObject({ 'sentry.browser.measure.detail': 'this-should-work', 'sentry.op': 'measure', - 'sentry.origin': 'auto.resource.browser.user_timing', + 'sentry.origin': 'auto.browser.user_timing.measure', }); // Test 3: Verify the complex detail object is captured correctly @@ -47,7 +47,7 @@ sentryTest( expect(complexMeasure).toBeDefined(); expect(complexMeasure?.data).toMatchObject({ 'sentry.op': 'measure', - 'sentry.origin': 'auto.resource.browser.user_timing', + 'sentry.origin': 'auto.browser.user_timing.measure', // The entire nested object is stringified as a single value 'sentry.browser.measure.detail.nested': JSON.stringify({ array: [1, 2, 3], diff --git a/dev-packages/browser-integration-tests/suites/tracing/metrics/pageload-measure-spans/test.ts b/dev-packages/browser-integration-tests/suites/tracing/metrics/pageload-measure-spans/test.ts index 67a3dab51535..ade0c97799ef 100644 --- a/dev-packages/browser-integration-tests/suites/tracing/metrics/pageload-measure-spans/test.ts +++ b/dev-packages/browser-integration-tests/suites/tracing/metrics/pageload-measure-spans/test.ts @@ -30,6 +30,6 @@ sentryTest('should add browser-related spans to pageload transaction', async ({ 'sentry.browser.measure_happened_before_request': true, 'sentry.browser.measure_start_time': expect.any(Number), 'sentry.op': 'measure', - 'sentry.origin': 'auto.resource.browser.user_timing', + 'sentry.origin': 'auto.browser.user_timing.measure', }); }); diff --git a/dev-packages/browser-integration-tests/suites/tracing/user-timing-spans/test.ts b/dev-packages/browser-integration-tests/suites/tracing/user-timing-spans/test.ts index da10ce76c474..fc106f364945 100644 --- a/dev-packages/browser-integration-tests/suites/tracing/user-timing-spans/test.ts +++ b/dev-packages/browser-integration-tests/suites/tracing/user-timing-spans/test.ts @@ -14,11 +14,15 @@ sentryTest('captures non-ignored mark and measure spans', async ({ getLocalTestU const spans = await spansPromise; const userTimingSpans = spans .filter(span => ['mark', 'measure'].includes(getSpanOp(span) ?? '')) - .map(span => ({ name: span.name, op: getSpanOp(span) })) + .map(span => ({ + name: span.name, + op: getSpanOp(span), + origin: span.attributes['sentry.origin']?.value, + })) .sort((a, b) => a.name.localeCompare(b.name)); expect(userTimingSpans).toEqual([ - { name: 'mark-pass', op: 'mark' }, - { name: 'measure-pass', op: 'measure' }, + { name: 'mark-pass', op: 'mark', origin: 'auto.browser.user_timing.mark' }, + { name: 'measure-pass', op: 'measure', origin: 'auto.browser.user_timing.measure' }, ]); }); diff --git a/packages/browser-utils/src/metrics/userTiming.ts b/packages/browser-utils/src/metrics/userTiming.ts index 106d1cb1e1a6..822a47c13991 100644 --- a/packages/browser-utils/src/metrics/userTiming.ts +++ b/packages/browser-utils/src/metrics/userTiming.ts @@ -114,7 +114,7 @@ export function _addUserTimingSpan( const spanEndTimestamp = originalStartTimestamp + duration; const attributes: SpanAttributes = { - [SENTRY_ORIGIN]: 'auto.resource.browser.user_timing', + [SENTRY_ORIGIN]: `auto.browser.user_timing.${entry.entryType}`, }; if (spanStartTimestamp !== originalStartTimestamp) { diff --git a/packages/browser-utils/test/metrics/userTiming.test.ts b/packages/browser-utils/test/metrics/userTiming.test.ts index e79c3f31df9c..73e007f286ee 100644 --- a/packages/browser-utils/test/metrics/userTiming.test.ts +++ b/packages/browser-utils/test/metrics/userTiming.test.ts @@ -163,7 +163,7 @@ describe('_addUserTimingSpan', () => { 'sentry.browser.measure.detail.phase': 'client', 'sentry.browser.measure.detail.counts': '{"components":4}', 'sentry.op': 'measure', - 'sentry.origin': 'auto.resource.browser.user_timing', + 'sentry.origin': 'auto.browser.user_timing.measure', }); }); From 0b4721e70e9318f51b4bc3f6ab4fae4ff0810103 Mon Sep 17 00:00:00 2001 From: Martin Sonnberger Date: Tue, 28 Jul 2026 14:00:14 +0200 Subject: [PATCH 09/12] remove from bundles --- .../browser/src/index.bundle.tracing.logs.metrics.ts | 1 - .../index.bundle.tracing.replay.feedback.logs.metrics.ts | 1 - .../browser/src/index.bundle.tracing.replay.feedback.ts | 1 - .../src/index.bundle.tracing.replay.logs.metrics.ts | 1 - packages/browser/src/index.bundle.tracing.replay.ts | 1 - packages/browser/src/index.bundle.tracing.ts | 1 - .../test/index.bundle.tracing.logs.metrics.test.ts | 8 +------- ...x.bundle.tracing.replay.feedback.logs.metrics.test.ts | 2 -- .../test/index.bundle.tracing.replay.feedback.test.ts | 2 -- .../index.bundle.tracing.replay.logs.metrics.test.ts | 9 +-------- .../browser/test/index.bundle.tracing.replay.test.ts | 9 +-------- packages/browser/test/index.bundle.tracing.test.ts | 8 +------- 12 files changed, 4 insertions(+), 40 deletions(-) diff --git a/packages/browser/src/index.bundle.tracing.logs.metrics.ts b/packages/browser/src/index.bundle.tracing.logs.metrics.ts index ffe111648c40..1dd34bb2ff42 100644 --- a/packages/browser/src/index.bundle.tracing.logs.metrics.ts +++ b/packages/browser/src/index.bundle.tracing.logs.metrics.ts @@ -33,7 +33,6 @@ export { setActiveSpanInBrowser } from './tracing/setActiveSpan'; export { spanStreamingIntegration } from './integrations/spanstreaming'; export { fetchStreamPerformanceIntegration } from './integrations/fetchStreamPerformance'; export { webVitalsIntegration } from './integrations/webVitals'; -export { userTimingIntegration } from '@sentry/browser-utils'; export { feedbackIntegrationShim as feedbackAsyncIntegration, diff --git a/packages/browser/src/index.bundle.tracing.replay.feedback.logs.metrics.ts b/packages/browser/src/index.bundle.tracing.replay.feedback.logs.metrics.ts index 4c8365c28b95..cc7262dc9dbe 100644 --- a/packages/browser/src/index.bundle.tracing.replay.feedback.logs.metrics.ts +++ b/packages/browser/src/index.bundle.tracing.replay.feedback.logs.metrics.ts @@ -33,7 +33,6 @@ export { setActiveSpanInBrowser } from './tracing/setActiveSpan'; export { spanStreamingIntegration } from './integrations/spanstreaming'; export { fetchStreamPerformanceIntegration } from './integrations/fetchStreamPerformance'; export { webVitalsIntegration } from './integrations/webVitals'; -export { userTimingIntegration } from '@sentry/browser-utils'; export { getFeedback, sendFeedback } from '@sentry/feedback'; diff --git a/packages/browser/src/index.bundle.tracing.replay.feedback.ts b/packages/browser/src/index.bundle.tracing.replay.feedback.ts index 04c892f802df..1e821b38f824 100644 --- a/packages/browser/src/index.bundle.tracing.replay.feedback.ts +++ b/packages/browser/src/index.bundle.tracing.replay.feedback.ts @@ -39,7 +39,6 @@ export { reportPageLoaded } from './tracing/reportPageLoaded'; export { spanStreamingIntegration } from './integrations/spanstreaming'; export { fetchStreamPerformanceIntegration } from './integrations/fetchStreamPerformance'; export { webVitalsIntegration } from './integrations/webVitals'; -export { userTimingIntegration } from '@sentry/browser-utils'; export { getFeedback, sendFeedback } from '@sentry/feedback'; diff --git a/packages/browser/src/index.bundle.tracing.replay.logs.metrics.ts b/packages/browser/src/index.bundle.tracing.replay.logs.metrics.ts index 52ce1a4dc258..9abdd4675d5e 100644 --- a/packages/browser/src/index.bundle.tracing.replay.logs.metrics.ts +++ b/packages/browser/src/index.bundle.tracing.replay.logs.metrics.ts @@ -33,7 +33,6 @@ export { setActiveSpanInBrowser } from './tracing/setActiveSpan'; export { spanStreamingIntegration } from './integrations/spanstreaming'; export { fetchStreamPerformanceIntegration } from './integrations/fetchStreamPerformance'; export { webVitalsIntegration } from './integrations/webVitals'; -export { userTimingIntegration } from '@sentry/browser-utils'; export { feedbackIntegrationShim as feedbackAsyncIntegration, feedbackIntegrationShim as feedbackIntegration }; diff --git a/packages/browser/src/index.bundle.tracing.replay.ts b/packages/browser/src/index.bundle.tracing.replay.ts index b8dfe11158b2..50da2dac87c4 100644 --- a/packages/browser/src/index.bundle.tracing.replay.ts +++ b/packages/browser/src/index.bundle.tracing.replay.ts @@ -38,7 +38,6 @@ export { setActiveSpanInBrowser } from './tracing/setActiveSpan'; export { spanStreamingIntegration } from './integrations/spanstreaming'; export { fetchStreamPerformanceIntegration } from './integrations/fetchStreamPerformance'; export { webVitalsIntegration } from './integrations/webVitals'; -export { userTimingIntegration } from '@sentry/browser-utils'; export { feedbackIntegrationShim as feedbackAsyncIntegration, feedbackIntegrationShim as feedbackIntegration }; diff --git a/packages/browser/src/index.bundle.tracing.ts b/packages/browser/src/index.bundle.tracing.ts index b60f215d9be9..aac6825ecc65 100644 --- a/packages/browser/src/index.bundle.tracing.ts +++ b/packages/browser/src/index.bundle.tracing.ts @@ -40,7 +40,6 @@ export { reportPageLoaded } from './tracing/reportPageLoaded'; export { spanStreamingIntegration } from './integrations/spanstreaming'; export { fetchStreamPerformanceIntegration } from './integrations/fetchStreamPerformance'; export { webVitalsIntegration } from './integrations/webVitals'; -export { userTimingIntegration } from '@sentry/browser-utils'; export { feedbackIntegrationShim as feedbackAsyncIntegration, diff --git a/packages/browser/test/index.bundle.tracing.logs.metrics.test.ts b/packages/browser/test/index.bundle.tracing.logs.metrics.test.ts index 1a81580bc3fb..81d239c8ff2d 100644 --- a/packages/browser/test/index.bundle.tracing.logs.metrics.test.ts +++ b/packages/browser/test/index.bundle.tracing.logs.metrics.test.ts @@ -1,12 +1,7 @@ import { logger as coreLogger, metrics as coreMetrics } from '@sentry/core/browser'; import { feedbackIntegrationShim, replayIntegrationShim } from '@sentry-internal/integration-shims'; import { describe, expect, it } from 'vitest'; -import { - browserTracingIntegration, - spanStreamingIntegration, - userTimingIntegration, - webVitalsIntegration, -} from '../src'; +import { browserTracingIntegration, spanStreamingIntegration, webVitalsIntegration } from '../src'; import * as TracingLogsMetricsBundle from '../src/index.bundle.tracing.logs.metrics'; describe('index.bundle.tracing.logs.metrics', () => { @@ -16,7 +11,6 @@ describe('index.bundle.tracing.logs.metrics', () => { expect(TracingLogsMetricsBundle.feedbackIntegration).toBe(feedbackIntegrationShim); expect(TracingLogsMetricsBundle.replayIntegration).toBe(replayIntegrationShim); expect(TracingLogsMetricsBundle.spanStreamingIntegration).toBe(spanStreamingIntegration); - expect(TracingLogsMetricsBundle.userTimingIntegration).toBe(userTimingIntegration); expect(TracingLogsMetricsBundle.webVitalsIntegration).toBe(webVitalsIntegration); expect(TracingLogsMetricsBundle.logger).toBe(coreLogger); diff --git a/packages/browser/test/index.bundle.tracing.replay.feedback.logs.metrics.test.ts b/packages/browser/test/index.bundle.tracing.replay.feedback.logs.metrics.test.ts index c13709e995f3..dcb7b869aa4d 100644 --- a/packages/browser/test/index.bundle.tracing.replay.feedback.logs.metrics.test.ts +++ b/packages/browser/test/index.bundle.tracing.replay.feedback.logs.metrics.test.ts @@ -5,7 +5,6 @@ import { feedbackAsyncIntegration, replayIntegration, spanStreamingIntegration, - userTimingIntegration, webVitalsIntegration, } from '../src'; import * as TracingReplayFeedbackLogsMetricsBundle from '../src/index.bundle.tracing.replay.feedback.logs.metrics'; @@ -17,7 +16,6 @@ describe('index.bundle.tracing.replay.feedback.logs.metrics', () => { expect(TracingReplayFeedbackLogsMetricsBundle.feedbackIntegration).toBe(feedbackAsyncIntegration); expect(TracingReplayFeedbackLogsMetricsBundle.replayIntegration).toBe(replayIntegration); expect(TracingReplayFeedbackLogsMetricsBundle.spanStreamingIntegration).toBe(spanStreamingIntegration); - expect(TracingReplayFeedbackLogsMetricsBundle.userTimingIntegration).toBe(userTimingIntegration); expect(TracingReplayFeedbackLogsMetricsBundle.webVitalsIntegration).toBe(webVitalsIntegration); expect(TracingReplayFeedbackLogsMetricsBundle.logger).toBe(coreLogger); diff --git a/packages/browser/test/index.bundle.tracing.replay.feedback.test.ts b/packages/browser/test/index.bundle.tracing.replay.feedback.test.ts index bdc494e8fd86..1bb8fffbeef9 100644 --- a/packages/browser/test/index.bundle.tracing.replay.feedback.test.ts +++ b/packages/browser/test/index.bundle.tracing.replay.feedback.test.ts @@ -5,7 +5,6 @@ import { feedbackAsyncIntegration, replayIntegration, spanStreamingIntegration, - userTimingIntegration, webVitalsIntegration, } from '../src'; import * as TracingReplayFeedbackBundle from '../src/index.bundle.tracing.replay.feedback'; @@ -17,7 +16,6 @@ describe('index.bundle.tracing.replay.feedback', () => { expect(TracingReplayFeedbackBundle.feedbackIntegration).toBe(feedbackAsyncIntegration); expect(TracingReplayFeedbackBundle.replayIntegration).toBe(replayIntegration); expect(TracingReplayFeedbackBundle.spanStreamingIntegration).toBe(spanStreamingIntegration); - expect(TracingReplayFeedbackBundle.userTimingIntegration).toBe(userTimingIntegration); expect(TracingReplayFeedbackBundle.webVitalsIntegration).toBe(webVitalsIntegration); expect(TracingReplayFeedbackBundle.logger).toBe(loggerShim); diff --git a/packages/browser/test/index.bundle.tracing.replay.logs.metrics.test.ts b/packages/browser/test/index.bundle.tracing.replay.logs.metrics.test.ts index 6caf07d28ab6..aecd1c995dda 100644 --- a/packages/browser/test/index.bundle.tracing.replay.logs.metrics.test.ts +++ b/packages/browser/test/index.bundle.tracing.replay.logs.metrics.test.ts @@ -1,13 +1,7 @@ import { logger as coreLogger, metrics as coreMetrics } from '@sentry/core/browser'; import { feedbackIntegrationShim } from '@sentry-internal/integration-shims'; import { describe, expect, it } from 'vitest'; -import { - browserTracingIntegration, - replayIntegration, - spanStreamingIntegration, - userTimingIntegration, - webVitalsIntegration, -} from '../src'; +import { browserTracingIntegration, replayIntegration, spanStreamingIntegration, webVitalsIntegration } from '../src'; import * as TracingReplayLogsMetricsBundle from '../src/index.bundle.tracing.replay.logs.metrics'; describe('index.bundle.tracing.replay.logs.metrics', () => { @@ -17,7 +11,6 @@ describe('index.bundle.tracing.replay.logs.metrics', () => { expect(TracingReplayLogsMetricsBundle.feedbackIntegration).toBe(feedbackIntegrationShim); expect(TracingReplayLogsMetricsBundle.replayIntegration).toBe(replayIntegration); expect(TracingReplayLogsMetricsBundle.spanStreamingIntegration).toBe(spanStreamingIntegration); - expect(TracingReplayLogsMetricsBundle.userTimingIntegration).toBe(userTimingIntegration); expect(TracingReplayLogsMetricsBundle.webVitalsIntegration).toBe(webVitalsIntegration); expect(TracingReplayLogsMetricsBundle.logger).toBe(coreLogger); diff --git a/packages/browser/test/index.bundle.tracing.replay.test.ts b/packages/browser/test/index.bundle.tracing.replay.test.ts index 780b56b9552d..847e572be009 100644 --- a/packages/browser/test/index.bundle.tracing.replay.test.ts +++ b/packages/browser/test/index.bundle.tracing.replay.test.ts @@ -1,12 +1,6 @@ import { consoleLoggingIntegrationShim, feedbackIntegrationShim, loggerShim } from '@sentry-internal/integration-shims'; import { describe, expect, it } from 'vitest'; -import { - browserTracingIntegration, - replayIntegration, - spanStreamingIntegration, - userTimingIntegration, - webVitalsIntegration, -} from '../src'; +import { browserTracingIntegration, replayIntegration, spanStreamingIntegration, webVitalsIntegration } from '../src'; import * as TracingReplayBundle from '../src/index.bundle.tracing.replay'; describe('index.bundle.tracing.replay', () => { @@ -16,7 +10,6 @@ describe('index.bundle.tracing.replay', () => { expect(TracingReplayBundle.feedbackIntegration).toBe(feedbackIntegrationShim); expect(TracingReplayBundle.replayIntegration).toBe(replayIntegration); expect(TracingReplayBundle.spanStreamingIntegration).toBe(spanStreamingIntegration); - expect(TracingReplayBundle.userTimingIntegration).toBe(userTimingIntegration); expect(TracingReplayBundle.webVitalsIntegration).toBe(webVitalsIntegration); expect(TracingReplayBundle.logger).toBe(loggerShim); diff --git a/packages/browser/test/index.bundle.tracing.test.ts b/packages/browser/test/index.bundle.tracing.test.ts index 71d4387d3b3b..9d41fcd10e42 100644 --- a/packages/browser/test/index.bundle.tracing.test.ts +++ b/packages/browser/test/index.bundle.tracing.test.ts @@ -5,12 +5,7 @@ import { replayIntegrationShim, } from '@sentry-internal/integration-shims'; import { describe, expect, it } from 'vitest'; -import { - browserTracingIntegration, - spanStreamingIntegration, - userTimingIntegration, - webVitalsIntegration, -} from '../src'; +import { browserTracingIntegration, spanStreamingIntegration, webVitalsIntegration } from '../src'; import * as TracingBundle from '../src/index.bundle.tracing'; describe('index.bundle.tracing', () => { @@ -20,7 +15,6 @@ describe('index.bundle.tracing', () => { expect(TracingBundle.feedbackIntegration).toBe(feedbackIntegrationShim); expect(TracingBundle.replayIntegration).toBe(replayIntegrationShim); expect(TracingBundle.spanStreamingIntegration).toBe(spanStreamingIntegration); - expect(TracingBundle.userTimingIntegration).toBe(userTimingIntegration); expect(TracingBundle.webVitalsIntegration).toBe(webVitalsIntegration); expect(TracingBundle.logger).toBe(loggerShim); From 8f7fa80227fb361976c4ddab415cebb7c57a7e62 Mon Sep 17 00:00:00 2001 From: Martin Sonnberger Date: Tue, 28 Jul 2026 14:20:04 +0200 Subject: [PATCH 10/12] make usertiming a pluggable integration --- .../suites/tracing/user-timing-spans/init.js | 6 ++---- .../browser-integration-tests/utils/generatePlugin.ts | 1 + packages/browser/rollup.bundle.config.mjs | 8 +++++++- packages/browser/src/index.ts | 2 +- packages/browser/src/integrations/usertiming.ts | 1 + packages/browser/src/utils/lazyLoadIntegration.ts | 1 + 6 files changed, 13 insertions(+), 6 deletions(-) create mode 100644 packages/browser/src/integrations/usertiming.ts diff --git a/dev-packages/browser-integration-tests/suites/tracing/user-timing-spans/init.js b/dev-packages/browser-integration-tests/suites/tracing/user-timing-spans/init.js index 00a357fe978d..c908c7d2c9c2 100644 --- a/dev-packages/browser-integration-tests/suites/tracing/user-timing-spans/init.js +++ b/dev-packages/browser-integration-tests/suites/tracing/user-timing-spans/init.js @@ -1,13 +1,11 @@ import * as Sentry from '@sentry/browser'; +import { userTimingIntegration } from '@sentry/browser'; window.Sentry = Sentry; Sentry.init({ dsn: 'https://public@dsn.ingest.sentry.io/1337', - integrations: [ - Sentry.browserTracingIntegration(), - Sentry.userTimingIntegration({ ignore: ['measure-ignore', /mark-i/] }), - ], + integrations: [Sentry.browserTracingIntegration(), userTimingIntegration({ ignore: ['measure-ignore', /mark-i/] })], tracesSampleRate: 1, }); diff --git a/dev-packages/browser-integration-tests/utils/generatePlugin.ts b/dev-packages/browser-integration-tests/utils/generatePlugin.ts index 084629a9d437..df6916a6ffa4 100644 --- a/dev-packages/browser-integration-tests/utils/generatePlugin.ts +++ b/dev-packages/browser-integration-tests/utils/generatePlugin.ts @@ -33,6 +33,7 @@ const IMPORTED_INTEGRATION_CDN_BUNDLE_PATHS: Record = { contextLinesIntegration: 'contextlines', extraErrorDataIntegration: 'extraerrordata', reportingObserverIntegration: 'reportingobserver', + userTimingIntegration: 'usertiming', feedbackIntegration: 'feedback', moduleMetadataIntegration: 'modulemetadata', graphqlClientIntegration: 'graphqlclient', diff --git a/packages/browser/rollup.bundle.config.mjs b/packages/browser/rollup.bundle.config.mjs index 3306278fe2ae..e779f0cba66d 100644 --- a/packages/browser/rollup.bundle.config.mjs +++ b/packages/browser/rollup.bundle.config.mjs @@ -2,7 +2,13 @@ import { makeBaseBundleConfig, makeBundleConfigVariants } from '@sentry-internal const builds = []; -const browserPluggableIntegrationFiles = ['contextlines', 'httpclient', 'reportingobserver', 'browserprofiling']; +const browserPluggableIntegrationFiles = [ + 'contextlines', + 'httpclient', + 'reportingobserver', + 'browserprofiling', + 'usertiming', +]; const reexportedPluggableIntegrationFiles = [ 'captureconsole', diff --git a/packages/browser/src/index.ts b/packages/browser/src/index.ts index e93c74997a0b..9a08d33f25e6 100644 --- a/packages/browser/src/index.ts +++ b/packages/browser/src/index.ts @@ -48,7 +48,7 @@ export { setActiveSpanInBrowser } from './tracing/setActiveSpan'; export { spanStreamingIntegration } from './integrations/spanstreaming'; export { fetchStreamPerformanceIntegration } from './integrations/fetchStreamPerformance'; export { webVitalsIntegration } from './integrations/webVitals'; -export { userTimingIntegration } from '@sentry/browser-utils'; +export { userTimingIntegration } from './integrations/usertiming'; export type { RequestInstrumentationOptions } from './tracing/request'; export { diff --git a/packages/browser/src/integrations/usertiming.ts b/packages/browser/src/integrations/usertiming.ts new file mode 100644 index 000000000000..788dddf6929a --- /dev/null +++ b/packages/browser/src/integrations/usertiming.ts @@ -0,0 +1 @@ +export { userTimingIntegration } from '@sentry/browser-utils'; diff --git a/packages/browser/src/utils/lazyLoadIntegration.ts b/packages/browser/src/utils/lazyLoadIntegration.ts index 6b93c3f3a00b..2e33db8ea6ea 100644 --- a/packages/browser/src/utils/lazyLoadIntegration.ts +++ b/packages/browser/src/utils/lazyLoadIntegration.ts @@ -22,6 +22,7 @@ const LAZY_LOADABLE_NAMES = [ 'reportingObserverIntegration', 'rewriteFramesIntegration', 'browserProfilingIntegration', + 'userTimingIntegration', 'moduleMetadataIntegration', ] as const; From 101614cff90aa1ceb17419c60e273670f47c9065 Mon Sep 17 00:00:00 2001 From: Martin Sonnberger Date: Tue, 28 Jul 2026 14:41:35 +0200 Subject: [PATCH 11/12] fix tests --- .../pageload-measure-spans-domexception-details/init.js | 4 ++-- .../suites/tracing/metrics/pageload-measure-spans/init.js | 3 ++- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/dev-packages/browser-integration-tests/suites/tracing/metrics/pageload-measure-spans-domexception-details/init.js b/dev-packages/browser-integration-tests/suites/tracing/metrics/pageload-measure-spans-domexception-details/init.js index 6d8ddc4c1830..d96c6f65b682 100644 --- a/dev-packages/browser-integration-tests/suites/tracing/metrics/pageload-measure-spans-domexception-details/init.js +++ b/dev-packages/browser-integration-tests/suites/tracing/metrics/pageload-measure-spans-domexception-details/init.js @@ -1,5 +1,5 @@ import * as Sentry from '@sentry/browser'; - +import { userTimingIntegration } from '@sentry/browser'; // Create measures BEFORE SDK initializes // Create a measure with detail @@ -24,7 +24,7 @@ window.Sentry = Sentry; Sentry.init({ traceLifecycle: 'static', dsn: 'https://public@dsn.ingest.sentry.io/1337', - integrations: [Sentry.browserTracingIntegration(), Sentry.userTimingIntegration()], + integrations: [Sentry.browserTracingIntegration(), userTimingIntegration()], tracesSampleRate: 1, }); diff --git a/dev-packages/browser-integration-tests/suites/tracing/metrics/pageload-measure-spans/init.js b/dev-packages/browser-integration-tests/suites/tracing/metrics/pageload-measure-spans/init.js index 1b14ef4fc428..3c2db904fabe 100644 --- a/dev-packages/browser-integration-tests/suites/tracing/metrics/pageload-measure-spans/init.js +++ b/dev-packages/browser-integration-tests/suites/tracing/metrics/pageload-measure-spans/init.js @@ -1,5 +1,6 @@ // Add measure before SDK initializes import * as Sentry from '@sentry/browser'; +import { userTimingIntegration } from '@sentry/browser'; const end = performance.now(); performance.measure('Next.js-before-hydration', { @@ -12,6 +13,6 @@ window.Sentry = Sentry; Sentry.init({ traceLifecycle: 'static', dsn: 'https://public@dsn.ingest.sentry.io/1337', - integrations: [Sentry.browserTracingIntegration(), Sentry.userTimingIntegration()], + integrations: [Sentry.browserTracingIntegration(), userTimingIntegration()], tracesSampleRate: 1, }); From e93a46472d188a0c167b3e6cc185ca28f230bb34 Mon Sep 17 00:00:00 2001 From: Martin Sonnberger Date: Tue, 28 Jul 2026 17:23:18 +0200 Subject: [PATCH 12/12] move tests --- .../disabled}/init.js | 0 .../disabled}/test.ts | 6 +++--- .../pageload-measure-spans-domexception-details/init.js | 0 .../pageload-measure-spans-domexception-details/test.ts | 0 .../{metrics => user-timing}/pageload-measure-spans/init.js | 0 .../{metrics => user-timing}/pageload-measure-spans/test.ts | 0 .../{user-timing-spans => user-timing/spans}/init.js | 0 .../{user-timing-spans => user-timing/spans}/test.ts | 6 +++--- 8 files changed, 6 insertions(+), 6 deletions(-) rename dev-packages/browser-integration-tests/suites/tracing/{user-timing-spans-disabled => user-timing/disabled}/init.js (100%) rename dev-packages/browser-integration-tests/suites/tracing/{user-timing-spans-disabled => user-timing/disabled}/test.ts (73%) rename dev-packages/browser-integration-tests/suites/tracing/{metrics => user-timing}/pageload-measure-spans-domexception-details/init.js (100%) rename dev-packages/browser-integration-tests/suites/tracing/{metrics => user-timing}/pageload-measure-spans-domexception-details/test.ts (100%) rename dev-packages/browser-integration-tests/suites/tracing/{metrics => user-timing}/pageload-measure-spans/init.js (100%) rename dev-packages/browser-integration-tests/suites/tracing/{metrics => user-timing}/pageload-measure-spans/test.ts (100%) rename dev-packages/browser-integration-tests/suites/tracing/{user-timing-spans => user-timing/spans}/init.js (100%) rename dev-packages/browser-integration-tests/suites/tracing/{user-timing-spans => user-timing/spans}/test.ts (82%) diff --git a/dev-packages/browser-integration-tests/suites/tracing/user-timing-spans-disabled/init.js b/dev-packages/browser-integration-tests/suites/tracing/user-timing/disabled/init.js similarity index 100% rename from dev-packages/browser-integration-tests/suites/tracing/user-timing-spans-disabled/init.js rename to dev-packages/browser-integration-tests/suites/tracing/user-timing/disabled/init.js diff --git a/dev-packages/browser-integration-tests/suites/tracing/user-timing-spans-disabled/test.ts b/dev-packages/browser-integration-tests/suites/tracing/user-timing/disabled/test.ts similarity index 73% rename from dev-packages/browser-integration-tests/suites/tracing/user-timing-spans-disabled/test.ts rename to dev-packages/browser-integration-tests/suites/tracing/user-timing/disabled/test.ts index f516e263ed3a..8d69125cbd1c 100644 --- a/dev-packages/browser-integration-tests/suites/tracing/user-timing-spans-disabled/test.ts +++ b/dev-packages/browser-integration-tests/suites/tracing/user-timing/disabled/test.ts @@ -1,7 +1,7 @@ import { expect } from '@playwright/test'; -import { sentryTest } from '../../../utils/fixtures'; -import { shouldSkipTracingTest } from '../../../utils/helpers'; -import { getSpanOp, waitForStreamedSpans } from '../../../utils/spanUtils'; +import { sentryTest } from '../../../../utils/fixtures'; +import { shouldSkipTracingTest } from '../../../../utils/helpers'; +import { getSpanOp, waitForStreamedSpans } from '../../../../utils/spanUtils'; sentryTest('does not capture mark and measure spans by default', async ({ getLocalTestUrl, page }) => { sentryTest.skip(shouldSkipTracingTest()); diff --git a/dev-packages/browser-integration-tests/suites/tracing/metrics/pageload-measure-spans-domexception-details/init.js b/dev-packages/browser-integration-tests/suites/tracing/user-timing/pageload-measure-spans-domexception-details/init.js similarity index 100% rename from dev-packages/browser-integration-tests/suites/tracing/metrics/pageload-measure-spans-domexception-details/init.js rename to dev-packages/browser-integration-tests/suites/tracing/user-timing/pageload-measure-spans-domexception-details/init.js diff --git a/dev-packages/browser-integration-tests/suites/tracing/metrics/pageload-measure-spans-domexception-details/test.ts b/dev-packages/browser-integration-tests/suites/tracing/user-timing/pageload-measure-spans-domexception-details/test.ts similarity index 100% rename from dev-packages/browser-integration-tests/suites/tracing/metrics/pageload-measure-spans-domexception-details/test.ts rename to dev-packages/browser-integration-tests/suites/tracing/user-timing/pageload-measure-spans-domexception-details/test.ts diff --git a/dev-packages/browser-integration-tests/suites/tracing/metrics/pageload-measure-spans/init.js b/dev-packages/browser-integration-tests/suites/tracing/user-timing/pageload-measure-spans/init.js similarity index 100% rename from dev-packages/browser-integration-tests/suites/tracing/metrics/pageload-measure-spans/init.js rename to dev-packages/browser-integration-tests/suites/tracing/user-timing/pageload-measure-spans/init.js diff --git a/dev-packages/browser-integration-tests/suites/tracing/metrics/pageload-measure-spans/test.ts b/dev-packages/browser-integration-tests/suites/tracing/user-timing/pageload-measure-spans/test.ts similarity index 100% rename from dev-packages/browser-integration-tests/suites/tracing/metrics/pageload-measure-spans/test.ts rename to dev-packages/browser-integration-tests/suites/tracing/user-timing/pageload-measure-spans/test.ts diff --git a/dev-packages/browser-integration-tests/suites/tracing/user-timing-spans/init.js b/dev-packages/browser-integration-tests/suites/tracing/user-timing/spans/init.js similarity index 100% rename from dev-packages/browser-integration-tests/suites/tracing/user-timing-spans/init.js rename to dev-packages/browser-integration-tests/suites/tracing/user-timing/spans/init.js diff --git a/dev-packages/browser-integration-tests/suites/tracing/user-timing-spans/test.ts b/dev-packages/browser-integration-tests/suites/tracing/user-timing/spans/test.ts similarity index 82% rename from dev-packages/browser-integration-tests/suites/tracing/user-timing-spans/test.ts rename to dev-packages/browser-integration-tests/suites/tracing/user-timing/spans/test.ts index fc106f364945..4e01eb11d375 100644 --- a/dev-packages/browser-integration-tests/suites/tracing/user-timing-spans/test.ts +++ b/dev-packages/browser-integration-tests/suites/tracing/user-timing/spans/test.ts @@ -1,7 +1,7 @@ import { expect } from '@playwright/test'; -import { sentryTest } from '../../../utils/fixtures'; -import { shouldSkipTracingTest } from '../../../utils/helpers'; -import { getSpanOp, waitForStreamedSpans } from '../../../utils/spanUtils'; +import { sentryTest } from '../../../../utils/fixtures'; +import { shouldSkipTracingTest } from '../../../../utils/helpers'; +import { getSpanOp, waitForStreamedSpans } from '../../../../utils/spanUtils'; sentryTest('captures non-ignored mark and measure spans', async ({ getLocalTestUrl, page }) => { sentryTest.skip(shouldSkipTracingTest());