diff --git a/MIGRATION.md b/MIGRATION.md index 753a28e30733..fc15da18d6d8 100644 --- a/MIGRATION.md +++ b/MIGRATION.md @@ -342,6 +342,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. +- `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/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/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 89b50cfb1661..4a78bf7f3a65 100644 --- a/packages/browser-utils/src/index.ts +++ b/packages/browser-utils/src/index.ts @@ -21,6 +21,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 25b1a449f613..a9cf413e854f 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.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/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/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); 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); }); });