From 98dbc85673480cb8ea1577031af4b085069cc64a Mon Sep 17 00:00:00 2001 From: Martin Sonnberger Date: Wed, 30 Sep 2026 13:46:30 +0200 Subject: [PATCH 1/6] test(browser): port requests and web vitals to span streaming Co-Authored-By: GPT-6 --- .../tracing/interactions/disabled/init.js | 1 - .../tracing/interactions/disabled/test.ts | 17 +- .../interactions/navigation-click/init.js | 1 - .../interactions/navigation-click/test.ts | 63 +-- .../suites/tracing/interactions/spans/init.js | 2 +- .../suites/tracing/interactions/spans/test.ts | 18 +- .../metrics/connection-rtt-navigation/init.js | 1 - .../metrics/connection-rtt-navigation/test.ts | 41 +- .../tracing/metrics/connection-rtt/test.ts | 66 +-- .../tracing/metrics/element-timing/init.js | 1 - .../tracing/metrics/handlers-lcp/test.ts | 53 +- .../suites/tracing/metrics/init.js | 1 - .../metrics/pageload-browser-spans/test.ts | 46 +- .../pageload-resource-spans-streamed/init.js | 10 - .../template.html | 12 - .../pageload-resource-spans-streamed/test.ts | 68 --- .../pageload-resource-spans}/init.js | 1 - .../metrics/pageload-resource-spans/test.ts | 460 ++++++++++-------- .../web-vitals-cls-streamed-spans/init.js | 11 - .../web-vitals-cls-streamed-spans/subject.js | 17 - .../template.html | 10 - .../web-vitals-cls-streamed-spans/test.ts | 79 --- .../tracing/metrics/web-vitals-cls/init.js | 8 +- .../tracing/metrics/web-vitals-cls/subject.js | 18 +- .../metrics/web-vitals-cls/template.html | 1 + .../tracing/metrics/web-vitals-cls/test.ts | 88 ++-- .../tracing/metrics/web-vitals-fp-fcp/test.ts | 61 +-- .../web-vitals-inp-before-send-span/init.js | 13 +- .../web-vitals-inp-before-send-span/test.ts | 6 +- .../metrics/web-vitals-inp-late/init.js | 1 - .../metrics/web-vitals-inp-late/test.ts | 4 +- .../metrics/web-vitals-inp-navigate/init.js | 1 - .../metrics/web-vitals-inp-navigate/test.ts | 8 +- .../web-vitals-inp-parametrized-late/init.js | 1 - .../web-vitals-inp-parametrized-late/test.ts | 4 +- .../web-vitals-inp-parametrized/init.js | 1 - .../web-vitals-inp-parametrized/test.ts | 4 +- .../web-vitals-inp-streamed-spans/init.js | 15 - .../web-vitals-inp-streamed-spans/subject.js | 19 - .../template.html | 10 - .../web-vitals-inp-streamed-spans/test.ts | 87 ---- .../tracing/metrics/web-vitals-inp/init.js | 19 +- .../tracing/metrics/web-vitals-inp/subject.js | 5 +- .../metrics/web-vitals-inp/template.html | 2 - .../tracing/metrics/web-vitals-inp/test.ts | 253 ++++------ .../assets/sentry-logo-600x179.png | Bin 16118 -> 0 bytes .../template.html | 10 - .../web-vitals-lcp-streamed-spans/test.ts | 69 --- .../init.js | 3 +- .../tracing/metrics/web-vitals-lcp/test.ts | 66 ++- .../metrics/web-vitals-ttfb-streamed/test.ts | 36 -- .../init.js | 3 +- .../tracing/metrics/web-vitals-ttfb/test.ts | 40 +- .../suites/tracing/metrics/web-vitals/test.ts | 41 +- .../tracing/request/fetch-data-url/init.js | 1 - .../tracing/request/fetch-data-url/test.ts | 47 +- .../tracing/request/fetch-immediate/init.js | 1 - .../tracing/request/fetch-immediate/test.ts | 36 +- .../tracing/request/fetch-no-tracing/init.js | 1 - .../fetch-propagateTraceparent/init.js | 1 - .../fetch-relative-url-streamed/subject.js | 1 - .../fetch-relative-url-streamed/test.ts | 39 -- .../request/fetch-relative-url/init.js | 2 +- .../request/fetch-relative-url/test.ts | 53 +- .../init.js | 15 - .../tracing/request/fetch-streamed/subject.js | 5 - .../tracing/request/fetch-streamed/test.ts | 48 -- .../fetch-strip-query-and-fragment/init.js | 1 - .../fetch-strip-query-and-fragment/test.ts | 159 +++--- .../fetch-trace-header-merging/init.js | 1 - .../init.js | 1 - .../request/fetch-tracing-unsampled/init.js | 1 - .../init.js | 1 - .../fetch-tracing-without-performance/init.js | 1 - .../init.js | 2 +- .../subject.js | 0 .../test.ts | 4 +- .../init.js | 1 - .../fetch-with-custom-sentry-headers/init.js | 1 - .../request/fetch-with-request/init.js | 1 - .../init.js | 1 + .../suites/tracing/request/fetch/test.ts | 52 +- .../suites/tracing/request/init.js | 1 - .../tracing/request/xhr-data-url/init.js | 1 - .../tracing/request/xhr-data-url/test.ts | 44 +- .../tracing/request/xhr-no-tracing/init.js | 1 - .../request/xhr-propagateTraceparent/init.js | 1 - .../xhr-relative-url-streamed/subject.js | 11 - .../request/xhr-relative-url-streamed/test.ts | 39 -- .../tracing/request/xhr-relative-url/init.js | 2 +- .../tracing/request/xhr-relative-url/test.ts | 53 +- .../tracing/request/xhr-streamed/subject.js | 12 - .../tracing/request/xhr-streamed/test.ts | 48 -- .../xhr-strip-query-and-fragment/init.js | 1 - .../xhr-strip-query-and-fragment/test.ts | 151 +++--- .../init.js | 1 - .../request/xhr-tracing-unsampled/init.js | 1 - .../init.js | 1 - .../xhr-tracing-without-performance/init.js | 1 - .../init.js | 1 - .../request/{xhr-streamed => xhr}/init.js | 2 +- .../suites/tracing/request/xhr/test.ts | 49 +- .../tracing/user-timing/disabled/test.ts | 8 +- .../init.js | 1 - .../test.ts | 78 +-- .../pageload-measure-spans/init.js | 1 - .../pageload-measure-spans/test.ts | 55 +-- .../suites/tracing/user-timing/spans/test.ts | 8 +- 108 files changed, 1079 insertions(+), 1846 deletions(-) delete mode 100644 dev-packages/browser-integration-tests/suites/tracing/metrics/pageload-resource-spans-streamed/init.js delete mode 100644 dev-packages/browser-integration-tests/suites/tracing/metrics/pageload-resource-spans-streamed/template.html delete mode 100644 dev-packages/browser-integration-tests/suites/tracing/metrics/pageload-resource-spans-streamed/test.ts rename dev-packages/browser-integration-tests/suites/tracing/{request/fetch-relative-url-streamed => metrics/pageload-resource-spans}/init.js (87%) delete mode 100644 dev-packages/browser-integration-tests/suites/tracing/metrics/web-vitals-cls-streamed-spans/init.js delete mode 100644 dev-packages/browser-integration-tests/suites/tracing/metrics/web-vitals-cls-streamed-spans/subject.js delete mode 100644 dev-packages/browser-integration-tests/suites/tracing/metrics/web-vitals-cls-streamed-spans/template.html delete mode 100644 dev-packages/browser-integration-tests/suites/tracing/metrics/web-vitals-cls-streamed-spans/test.ts delete mode 100644 dev-packages/browser-integration-tests/suites/tracing/metrics/web-vitals-inp-streamed-spans/init.js delete mode 100644 dev-packages/browser-integration-tests/suites/tracing/metrics/web-vitals-inp-streamed-spans/subject.js delete mode 100644 dev-packages/browser-integration-tests/suites/tracing/metrics/web-vitals-inp-streamed-spans/template.html delete mode 100644 dev-packages/browser-integration-tests/suites/tracing/metrics/web-vitals-inp-streamed-spans/test.ts delete mode 100644 dev-packages/browser-integration-tests/suites/tracing/metrics/web-vitals-lcp-streamed-spans/assets/sentry-logo-600x179.png delete mode 100644 dev-packages/browser-integration-tests/suites/tracing/metrics/web-vitals-lcp-streamed-spans/template.html delete mode 100644 dev-packages/browser-integration-tests/suites/tracing/metrics/web-vitals-lcp-streamed-spans/test.ts rename dev-packages/browser-integration-tests/suites/tracing/metrics/{web-vitals-lcp-streamed-spans => web-vitals-lcp}/init.js (65%) delete mode 100644 dev-packages/browser-integration-tests/suites/tracing/metrics/web-vitals-ttfb-streamed/test.ts rename dev-packages/browser-integration-tests/suites/tracing/metrics/{web-vitals-ttfb-streamed => web-vitals-ttfb}/init.js (66%) delete mode 100644 dev-packages/browser-integration-tests/suites/tracing/request/fetch-relative-url-streamed/subject.js delete mode 100644 dev-packages/browser-integration-tests/suites/tracing/request/fetch-relative-url-streamed/test.ts delete mode 100644 dev-packages/browser-integration-tests/suites/tracing/request/fetch-streamed-track-stream-performance/init.js delete mode 100644 dev-packages/browser-integration-tests/suites/tracing/request/fetch-streamed/subject.js delete mode 100644 dev-packages/browser-integration-tests/suites/tracing/request/fetch-streamed/test.ts rename dev-packages/browser-integration-tests/suites/tracing/request/{fetch-streamed => fetch-track-stream-performance}/init.js (72%) rename dev-packages/browser-integration-tests/suites/tracing/request/{fetch-streamed-track-stream-performance => fetch-track-stream-performance}/subject.js (100%) rename dev-packages/browser-integration-tests/suites/tracing/request/{fetch-streamed-track-stream-performance => fetch-track-stream-performance}/test.ts (91%) rename dev-packages/browser-integration-tests/suites/tracing/request/{xhr-relative-url-streamed => fetch}/init.js (79%) delete mode 100644 dev-packages/browser-integration-tests/suites/tracing/request/xhr-relative-url-streamed/subject.js delete mode 100644 dev-packages/browser-integration-tests/suites/tracing/request/xhr-relative-url-streamed/test.ts delete mode 100644 dev-packages/browser-integration-tests/suites/tracing/request/xhr-streamed/subject.js delete mode 100644 dev-packages/browser-integration-tests/suites/tracing/request/xhr-streamed/test.ts rename dev-packages/browser-integration-tests/suites/tracing/request/{xhr-streamed => xhr}/init.js (74%) diff --git a/dev-packages/browser-integration-tests/suites/tracing/interactions/disabled/init.js b/dev-packages/browser-integration-tests/suites/tracing/interactions/disabled/init.js index bb6d4918d1bf..83076460599f 100644 --- a/dev-packages/browser-integration-tests/suites/tracing/interactions/disabled/init.js +++ b/dev-packages/browser-integration-tests/suites/tracing/interactions/disabled/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()], tracesSampleRate: 1, diff --git a/dev-packages/browser-integration-tests/suites/tracing/interactions/disabled/test.ts b/dev-packages/browser-integration-tests/suites/tracing/interactions/disabled/test.ts index 5a163c067969..6589fc67ed3a 100644 --- a/dev-packages/browser-integration-tests/suites/tracing/interactions/disabled/test.ts +++ b/dev-packages/browser-integration-tests/suites/tracing/interactions/disabled/test.ts @@ -1,19 +1,18 @@ import { expect } from '@playwright/test'; -import type { Event as SentryEvent } from '@sentry/core'; import { sentryTest } from '../../../../utils/fixtures'; -import { countEnvelopes, getFirstSentryEnvelopeRequest, shouldSkipTracingTest } from '../../../../utils/helpers'; +import { hidePage, shouldSkipTracingTest } from '../../../../utils/helpers'; +import { collectStreamedSpans, getSpanOp, waitForStreamedSpan } from '../../../../utils/spanUtils'; sentryTest('does not capture interaction spans without the integration', async ({ getLocalTestUrl, page }) => { sentryTest.skip(shouldSkipTracingTest()); - + const spans = collectStreamedSpans(page); + const pageloadPromise = waitForStreamedSpan(page, span => span.is_segment && getSpanOp(span) === 'pageload'); const url = await getLocalTestUrl({ testDir: __dirname }); - await page.goto(url); - await getFirstSentryEnvelopeRequest(page); - - const countPromise = countEnvelopes(page, { envelopeType: 'transaction', timeout: 2000 }); - + await pageloadPromise; await page.locator('[data-test-id=interaction-button]').click(); + await hidePage(page); + await page.evaluate(() => (window as any).Sentry.flush()); - expect(await countPromise).toBe(0); + expect(spans.filter(span => span.attributes['sentry.origin']?.value === 'auto.browser.interactions')).toHaveLength(0); }); diff --git a/dev-packages/browser-integration-tests/suites/tracing/interactions/navigation-click/init.js b/dev-packages/browser-integration-tests/suites/tracing/interactions/navigation-click/init.js index 2c3e9264d882..96386e1d38d5 100644 --- a/dev-packages/browser-integration-tests/suites/tracing/interactions/navigation-click/init.js +++ b/dev-packages/browser-integration-tests/suites/tracing/interactions/navigation-click/init.js @@ -4,7 +4,6 @@ import { interactionsIntegration } from '@sentry/browser'; window.Sentry = Sentry; Sentry.init({ - traceLifecycle: 'static', dsn: 'https://public@dsn.ingest.sentry.io/1337', tracesSampleRate: 1, integrations: [Sentry.browserTracingIntegration(), interactionsIntegration()], diff --git a/dev-packages/browser-integration-tests/suites/tracing/interactions/navigation-click/test.ts b/dev-packages/browser-integration-tests/suites/tracing/interactions/navigation-click/test.ts index 4931aaa4b172..b9913d3a5739 100644 --- a/dev-packages/browser-integration-tests/suites/tracing/interactions/navigation-click/test.ts +++ b/dev-packages/browser-integration-tests/suites/tracing/interactions/navigation-click/test.ts @@ -1,39 +1,28 @@ import { expect } from '@playwright/test'; import { sentryTest } from '../../../../utils/fixtures'; -import { envelopeRequestParser, shouldSkipTracingTest, waitForTransactionRequest } from '../../../../utils/helpers'; - -sentryTest( - 'click-triggered navigation should produce a root navigation transaction', - async ({ getLocalTestUrl, page }) => { - if (shouldSkipTracingTest()) { - sentryTest.skip(); - } - - const url = await getLocalTestUrl({ testDir: __dirname }); - - await page.goto(url); - await waitForTransactionRequest(page); // "pageload" root span - - const interactionRequestPromise = waitForTransactionRequest( - page, - evt => evt.contexts?.trace?.op === 'ui.action.click', - ); - const navigationRequestPromise = waitForTransactionRequest(page, evt => evt.contexts?.trace?.op === 'navigation'); - - await page.locator('[data-test-id=navigate-button]').click(); - - const interactionEvent = envelopeRequestParser(await interactionRequestPromise); - const navigationEvent = envelopeRequestParser(await navigationRequestPromise); - - // Navigation is root span, not a child span on the interaction - expect(interactionEvent.contexts?.trace?.op).toBe('ui.action.click'); - expect(navigationEvent.contexts?.trace?.op).toBe('navigation'); - - expect(interactionEvent.contexts?.trace?.trace_id).not.toEqual(navigationEvent.contexts?.trace?.trace_id); - - // does not contain a child navigation span - const interactionSpans = interactionEvent.spans || []; - const hasNavigationChild = interactionSpans.some(span => span.op === 'navigation' || span.op === 'http.server'); - expect(hasNavigationChild).toBeFalsy(); - }, -); +import { shouldSkipTracingTest } from '../../../../utils/helpers'; +import { collectStreamedSpans, getSpanOp, waitForStreamedSpan } from '../../../../utils/spanUtils'; + +sentryTest('starts a root navigation segment when a click triggers navigation', async ({ getLocalTestUrl, page }) => { + sentryTest.skip(shouldSkipTracingTest()); + const spans = collectStreamedSpans(page); + const pageloadPromise = waitForStreamedSpan(page, span => span.is_segment && getSpanOp(span) === 'pageload'); + const url = await getLocalTestUrl({ testDir: __dirname }); + await page.goto(url); + await pageloadPromise; + + const interactionPromise = waitForStreamedSpan( + page, + span => span.is_segment && getSpanOp(span) === 'ui.action.click', + ); + const navigationPromise = waitForStreamedSpan(page, span => span.is_segment && getSpanOp(span) === 'navigation'); + await page.locator('[data-test-id=navigate-button]').click(); + const [interaction, navigation] = await Promise.all([interactionPromise, navigationPromise]); + await page.evaluate(() => (window as any).Sentry.flush()); + + expect(navigation.is_segment).toBe(true); + expect(navigation).not.toHaveProperty('parent_span_id'); + expect(navigation.trace_id).not.toBe(interaction.trace_id); + const children = spans.filter(span => span.parent_span_id === interaction.span_id); + expect(children.filter(span => ['navigation', 'http.server'].includes(getSpanOp(span) ?? ''))).toHaveLength(0); +}); diff --git a/dev-packages/browser-integration-tests/suites/tracing/interactions/spans/init.js b/dev-packages/browser-integration-tests/suites/tracing/interactions/spans/init.js index af4b7b3e3483..c2d6aa640c21 100644 --- a/dev-packages/browser-integration-tests/suites/tracing/interactions/spans/init.js +++ b/dev-packages/browser-integration-tests/suites/tracing/interactions/spans/init.js @@ -5,6 +5,6 @@ window.Sentry = Sentry; Sentry.init({ dsn: 'https://public@dsn.ingest.sentry.io/1337', - integrations: [Sentry.browserTracingIntegration(), interactionsIntegration(), Sentry.spanStreamingIntegration()], + integrations: [Sentry.browserTracingIntegration(), interactionsIntegration()], tracesSampleRate: 1, }); diff --git a/dev-packages/browser-integration-tests/suites/tracing/interactions/spans/test.ts b/dev-packages/browser-integration-tests/suites/tracing/interactions/spans/test.ts index 0870974ba825..08c475e10551 100644 --- a/dev-packages/browser-integration-tests/suites/tracing/interactions/spans/test.ts +++ b/dev-packages/browser-integration-tests/suites/tracing/interactions/spans/test.ts @@ -17,7 +17,7 @@ import { } from '@sentry/conventions/attributes'; import { sentryTest } from '../../../../utils/fixtures'; import { shouldSkipTracingTest } from '../../../../utils/helpers'; -import { getSpanOp, waitForStreamedSpan, waitForStreamedSpans } from '../../../../utils/spanUtils'; +import { collectStreamedSpans, getSpanOp, waitForStreamedSpan } from '../../../../utils/spanUtils'; sentryTest('captures streamed interaction span tree. @firefox', async ({ browserName, getLocalTestUrl, page }) => { const supportedBrowsers = ['chromium', 'firefox']; @@ -25,8 +25,10 @@ sentryTest('captures streamed interaction span tree. @firefox', async ({ browser sentryTest.skip(shouldSkipTracingTest() || !supportedBrowsers.includes(browserName)); const url = await getLocalTestUrl({ testDir: __dirname }); - const interactionSpansPromise = waitForStreamedSpans(page, spans => - spans.some(span => getSpanOp(span) === 'ui.action.click'), + const spans = collectStreamedSpans(page); + const interactionPromise = waitForStreamedSpan( + page, + span => span.is_segment && getSpanOp(span) === 'ui.action.click', ); const pageloadSpanPromise = waitForStreamedSpan(page, span => getSpanOp(span) === 'pageload'); @@ -39,9 +41,13 @@ sentryTest('captures streamed interaction span tree. @firefox', async ({ browser await page.locator('[data-test-id=interaction-button]').click(); await page.locator('.clicked[data-test-id=interaction-button]').isVisible(); - const interactionSpanTree = await interactionSpansPromise; - - const interactionSegmentSpan = interactionSpanTree.find(span => !!span.is_segment); + const interactionSegmentSpan = await interactionPromise; + await page.evaluate(() => (window as any).Sentry.flush()); + const interactionSpanTree = spans.filter( + span => + span.span_id === interactionSegmentSpan.span_id || + span.attributes[SENTRY_SEGMENT_ID]?.value === interactionSegmentSpan.span_id, + ); expect(interactionSegmentSpan).toEqual({ attributes: { diff --git a/dev-packages/browser-integration-tests/suites/tracing/metrics/connection-rtt-navigation/init.js b/dev-packages/browser-integration-tests/suites/tracing/metrics/connection-rtt-navigation/init.js index 7ebcfe76faab..3e7a3d67bff0 100644 --- a/dev-packages/browser-integration-tests/suites/tracing/metrics/connection-rtt-navigation/init.js +++ b/dev-packages/browser-integration-tests/suites/tracing/metrics/connection-rtt-navigation/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/metrics/connection-rtt-navigation/test.ts b/dev-packages/browser-integration-tests/suites/tracing/metrics/connection-rtt-navigation/test.ts index c071cf089d71..0bb6a442459a 100644 --- a/dev-packages/browser-integration-tests/suites/tracing/metrics/connection-rtt-navigation/test.ts +++ b/dev-packages/browser-integration-tests/suites/tracing/metrics/connection-rtt-navigation/test.ts @@ -1,37 +1,20 @@ import { expect } from '@playwright/test'; -import type { Event } from '@sentry/core'; import { sentryTest } from '../../../../utils/fixtures'; -import { envelopeRequestParser, shouldSkipTracingTest, waitForTransactionRequest } from '../../../../utils/helpers'; +import { shouldSkipTracingTest } from '../../../../utils/helpers'; +import { waitForStreamedSpanAndTraceHeaderOnUrl } from '../../../../utils/spanUtils'; +import { NETWORK_CONNECTION_RTT } from '@sentry/conventions/attributes'; -sentryTest.beforeEach(({ browserName }) => { - if (shouldSkipTracingTest() || browserName !== 'chromium') { - sentryTest.skip(); - } -}); - -// `connection.rtt` is recorded as a measurement, which is only flushed on the pageload -// transaction. It must not leak onto navigation transactions. sentryTest( - 'records `connection.rtt` as a measurement on pageload but not on navigation transactions', - async ({ getLocalTestUrl, page }) => { - const pageloadRequestPromise = waitForTransactionRequest(page, event => event.contexts?.trace?.op === 'pageload'); + 'records connection RTT on pageload and navigation spans', + async ({ getLocalTestUrl, page, browserName }) => { + sentryTest.skip(shouldSkipTracingTest() || browserName !== 'chromium'); const url = await getLocalTestUrl({ testDir: __dirname }); - await page.goto(url); - - const pageloadRequest = envelopeRequestParser(await pageloadRequestPromise) as Event; - - const navigationRequestPromise = waitForTransactionRequest( - page, - event => event.contexts?.trace?.op === 'navigation', - ); - await page.goto(`${url}#foo`); - - const navigationRequest = envelopeRequestParser(await navigationRequestPromise) as Event; - - expect(pageloadRequest.contexts?.trace?.op).toBe('pageload'); - expect(navigationRequest.contexts?.trace?.op).toBe('navigation'); + const [pageload] = await waitForStreamedSpanAndTraceHeaderOnUrl(page, url); + const [navigation] = await waitForStreamedSpanAndTraceHeaderOnUrl(page, `${url}#foo`); - expect(pageloadRequest.measurements?.['connection.rtt']?.value).toBeDefined(); - expect(navigationRequest.measurements?.['connection.rtt']).toBeUndefined(); + expect(pageload.attributes[NETWORK_CONNECTION_RTT]).toEqual({ type: 'integer', value: 0 }); + expect(navigation.attributes[NETWORK_CONNECTION_RTT]).toEqual(pageload.attributes[NETWORK_CONNECTION_RTT]); + expect(navigation.attributes['browser.web_vital.fcp.value']).toBeUndefined(); + expect(navigation.attributes['browser.web_vital.ttfb.value']).toBeUndefined(); }, ); diff --git a/dev-packages/browser-integration-tests/suites/tracing/metrics/connection-rtt/test.ts b/dev-packages/browser-integration-tests/suites/tracing/metrics/connection-rtt/test.ts index 6f4b885e71d1..492027a2fead 100644 --- a/dev-packages/browser-integration-tests/suites/tracing/metrics/connection-rtt/test.ts +++ b/dev-packages/browser-integration-tests/suites/tracing/metrics/connection-rtt/test.ts @@ -1,8 +1,9 @@ import type { Page } from '@playwright/test'; import { expect } from '@playwright/test'; -import type { Event } from '@sentry/core'; import { sentryTest } from '../../../../utils/fixtures'; -import { getFirstSentryEnvelopeRequest, shouldSkipTracingTest } from '../../../../utils/helpers'; +import { shouldSkipTracingTest } from '../../../../utils/helpers'; +import { waitForStreamedSpanAndTraceHeaderOnUrl } from '../../../../utils/spanUtils'; +import { NETWORK_CONNECTION_RTT } from '@sentry/conventions/attributes'; sentryTest.beforeEach(({ browserName }) => { if (shouldSkipTracingTest() || browserName !== 'chromium') { @@ -22,55 +23,42 @@ async function createSessionWithLatency(page: Page, latency: number) { return session; } -sentryTest('should capture a `connection.rtt` metric. xxx', async ({ getLocalTestUrl, page }) => { +sentryTest('captures connection RTT without emulation', async ({ getLocalTestUrl, page }) => { const url = await getLocalTestUrl({ testDir: __dirname }); - const eventData = await getFirstSentryEnvelopeRequest(page, url); + const eventData = await waitForStreamedSpanAndTraceHeaderOnUrl(page, url).then(([span]) => span); - expect(eventData.measurements).toBeDefined(); - expect(eventData.measurements?.['connection.rtt']?.value).toBe(0); + expect(eventData.attributes[NETWORK_CONNECTION_RTT]?.value).toBe(0); }); -sentryTest( - 'should capture a `connection.rtt` metric with emulated value 200ms on Chromium.', - async ({ getLocalTestUrl, page }) => { - const session = await createSessionWithLatency(page, 200); +sentryTest('captures connection RTT with emulated value 200ms on Chromium.', async ({ getLocalTestUrl, page }) => { + const session = await createSessionWithLatency(page, 200); - const url = await getLocalTestUrl({ testDir: __dirname }); - const eventData = await getFirstSentryEnvelopeRequest(page, url); + const url = await getLocalTestUrl({ testDir: __dirname }); + const eventData = await waitForStreamedSpanAndTraceHeaderOnUrl(page, url).then(([span]) => span); - await session.detach(); + await session.detach(); - expect(eventData.measurements).toBeDefined(); - expect(eventData.measurements?.['connection.rtt']?.value).toBe(200); - }, -); + expect(eventData.attributes[NETWORK_CONNECTION_RTT]?.value).toBe(200); +}); -sentryTest( - 'should capture a `connection.rtt` metric with emulated value 100ms on Chromium.', - async ({ getLocalTestUrl, page }) => { - const session = await createSessionWithLatency(page, 100); +sentryTest('captures connection RTT with emulated value 100ms on Chromium.', async ({ getLocalTestUrl, page }) => { + const session = await createSessionWithLatency(page, 100); - const url = await getLocalTestUrl({ testDir: __dirname }); - const eventData = await getFirstSentryEnvelopeRequest(page, url); + const url = await getLocalTestUrl({ testDir: __dirname }); + const eventData = await waitForStreamedSpanAndTraceHeaderOnUrl(page, url).then(([span]) => span); - await session.detach(); + await session.detach(); - expect(eventData.measurements).toBeDefined(); - expect(eventData.measurements?.['connection.rtt']?.value).toBe(100); - }, -); + expect(eventData.attributes[NETWORK_CONNECTION_RTT]?.value).toBe(100); +}); -sentryTest( - 'should capture a `connection.rtt` metric with emulated value 50ms on Chromium.', - async ({ getLocalTestUrl, page }) => { - const session = await createSessionWithLatency(page, 50); +sentryTest('captures connection RTT with emulated value 50ms on Chromium.', async ({ getLocalTestUrl, page }) => { + const session = await createSessionWithLatency(page, 50); - const url = await getLocalTestUrl({ testDir: __dirname }); - const eventData = await getFirstSentryEnvelopeRequest(page, url); + const url = await getLocalTestUrl({ testDir: __dirname }); + const eventData = await waitForStreamedSpanAndTraceHeaderOnUrl(page, url).then(([span]) => span); - await session.detach(); + await session.detach(); - expect(eventData.measurements).toBeDefined(); - expect(eventData.measurements?.['connection.rtt']?.value).toBe(50); - }, -); + expect(eventData.attributes[NETWORK_CONNECTION_RTT]?.value).toBe(50); +}); diff --git a/dev-packages/browser-integration-tests/suites/tracing/metrics/element-timing/init.js b/dev-packages/browser-integration-tests/suites/tracing/metrics/element-timing/init.js index 8c8e5e033d47..40253c296af1 100644 --- a/dev-packages/browser-integration-tests/suites/tracing/metrics/element-timing/init.js +++ b/dev-packages/browser-integration-tests/suites/tracing/metrics/element-timing/init.js @@ -3,7 +3,6 @@ import * as Sentry from '@sentry/browser'; window.Sentry = Sentry; Sentry.init({ - traceLifecycle: 'static', debug: true, dsn: 'https://public@dsn.ingest.sentry.io/1337', integrations: [Sentry.browserTracingIntegration(), Sentry.elementTimingIntegration()], diff --git a/dev-packages/browser-integration-tests/suites/tracing/metrics/handlers-lcp/test.ts b/dev-packages/browser-integration-tests/suites/tracing/metrics/handlers-lcp/test.ts index b40899e37521..f54522520037 100644 --- a/dev-packages/browser-integration-tests/suites/tracing/metrics/handlers-lcp/test.ts +++ b/dev-packages/browser-integration-tests/suites/tracing/metrics/handlers-lcp/test.ts @@ -1,8 +1,8 @@ import type { Route } from '@playwright/test'; import { expect } from '@playwright/test'; -import type { Event } from '@sentry/core'; import { sentryTest } from '../../../../utils/fixtures'; -import { getFirstSentryEnvelopeRequest, shouldSkipTracingTest } from '../../../../utils/helpers'; +import { shouldSkipTracingTest } from '../../../../utils/helpers'; +import { getSpanOp, waitForStreamedSpan } from '../../../../utils/spanUtils'; const bundle = process.env.PW_BUNDLE || ''; @@ -20,38 +20,21 @@ sentryTest( const url = await getLocalTestUrl({ testDir: __dirname }); - const [eventData] = await Promise.all([ - getFirstSentryEnvelopeRequest(page), - page.goto(url), - page.locator('button').click(), - ]); - - expect(eventData.measurements).toBeDefined(); - expect(eventData.measurements?.lcp?.value).toBeDefined(); - - // This should be body > img, but it can be flakey as sometimes it will report - // the button as LCP. - expect(eventData.contexts?.trace?.data?.['lcp.element'].startsWith('body >')).toBe(true); - - // Working around flakiness - // Only testing this when the LCP element is an image, not a button - if (eventData.contexts?.trace?.data?.['lcp.element'] === 'body > img') { - expect(eventData.contexts?.trace?.data?.['lcp.size']).toBe(107400); - - const lcp = await (await page.waitForFunction('window._LCP')).jsonValue(); - const lcp2 = await (await page.waitForFunction('window._LCP2')).jsonValue(); - const lcp3 = await page.evaluate('window._LCP3'); - - expect(lcp).toEqual(107400); - expect(lcp2).toEqual(107400); - // this has not been triggered yet - expect(lcp3).toEqual(undefined); - - // Adding a handler after LCP is completed still triggers the handler - await page.evaluate('window.ADD_HANDLER()'); - const lcp3_2 = await (await page.waitForFunction('window._LCP3')).jsonValue(); - - expect(lcp3_2).toEqual(107400); - } + const lcpPromise = waitForStreamedSpan(page, span => getSpanOp(span) === 'ui.webvital.lcp'); + await page.goto(url); + await page.waitForFunction('window._LCP === 107400 && window._LCP2 === 107400'); + await page.locator('button').click(); + const lcpSpan = await lcpPromise; + + expect(lcpSpan.attributes['browser.web_vital.lcp.value']?.value).toBeGreaterThan(0); + expect(lcpSpan.attributes['browser.web_vital.lcp.element']).toEqual({ type: 'string', value: 'body > img' }); + expect(lcpSpan.attributes['browser.web_vital.lcp.size']).toEqual({ type: 'integer', value: 107400 }); + expect(await page.evaluate('window._LCP')).toBe(107400); + expect(await page.evaluate('window._LCP2')).toBe(107400); + expect(await page.evaluate('window._LCP3')).toBeUndefined(); + + await page.evaluate('window.ADD_HANDLER()'); + await page.waitForFunction('window._LCP3 === 107400'); + expect(await page.evaluate('window._LCP3')).toBe(107400); }, ); diff --git a/dev-packages/browser-integration-tests/suites/tracing/metrics/init.js b/dev-packages/browser-integration-tests/suites/tracing/metrics/init.js index bb6d4918d1bf..83076460599f 100644 --- a/dev-packages/browser-integration-tests/suites/tracing/metrics/init.js +++ b/dev-packages/browser-integration-tests/suites/tracing/metrics/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()], tracesSampleRate: 1, diff --git a/dev-packages/browser-integration-tests/suites/tracing/metrics/pageload-browser-spans/test.ts b/dev-packages/browser-integration-tests/suites/tracing/metrics/pageload-browser-spans/test.ts index 5e85698e709d..80aab1164446 100644 --- a/dev-packages/browser-integration-tests/suites/tracing/metrics/pageload-browser-spans/test.ts +++ b/dev-packages/browser-integration-tests/suites/tracing/metrics/pageload-browser-spans/test.ts @@ -1,29 +1,29 @@ import { expect } from '@playwright/test'; -import type { Event } from '@sentry/core'; import { sentryTest } from '../../../../utils/fixtures'; -import { getFirstSentryEnvelopeRequest, shouldSkipTracingTest } from '../../../../utils/helpers'; - -sentryTest('should add browser-related spans to pageload transaction', async ({ getLocalTestUrl, page }) => { - if (shouldSkipTracingTest()) { - sentryTest.skip(); - } +import { shouldSkipTracingTest } from '../../../../utils/helpers'; +import { collectStreamedSpans, getSpanOp, waitForStreamedSpan } from '../../../../utils/spanUtils'; +sentryTest('adds browser performance spans to the pageload segment', async ({ getLocalTestUrl, page }) => { + sentryTest.skip(shouldSkipTracingTest()); + const spans = collectStreamedSpans(page); + const pageloadPromise = waitForStreamedSpan(page, span => span.is_segment && getSpanOp(span) === 'pageload'); const url = await getLocalTestUrl({ testDir: __dirname }); + await page.goto(url); + const pageload = await pageloadPromise; + await page.evaluate(() => (window as any).Sentry.flush()); - const eventData = await getFirstSentryEnvelopeRequest(page, url); - const browserSpans = eventData.spans?.filter(({ op }) => op?.startsWith('browser')); - - // Spans `dom_content_loaded_event`, `connect`, `cache` and `dns` are not - // always inside `pageload` transaction. - expect(browserSpans?.length).toBeGreaterThanOrEqual(4); - - ['load_event', 'request', 'response'].forEach(eventDesc => - expect(browserSpans).toContainEqual( - expect.objectContaining({ - op: `browser.${eventDesc}`, - description: page.url(), - parent_span_id: eventData.contexts?.trace?.span_id, - }), - ), - ); + const browserSpans = spans.filter(span => getSpanOp(span)?.startsWith('browser')); + expect(browserSpans.length).toBeGreaterThanOrEqual(4); + [ + { op: 'browser.load_event', name: 'Load event' }, + { op: 'browser.request', name: 'Request' }, + { op: 'browser.response', name: 'Response' }, + ].forEach(({ op, name }) => { + const matches = browserSpans.filter(span => getSpanOp(span) === op); + expect(matches).toHaveLength(1); + expect(matches[0].name).toBe(name); + expect(matches[0].attributes['url.full']).toEqual({ type: 'string', value: page.url() }); + expect(matches[0].parent_span_id).toBe(pageload.span_id); + expect(matches[0].trace_id).toBe(pageload.trace_id); + }); }); diff --git a/dev-packages/browser-integration-tests/suites/tracing/metrics/pageload-resource-spans-streamed/init.js b/dev-packages/browser-integration-tests/suites/tracing/metrics/pageload-resource-spans-streamed/init.js deleted file mode 100644 index 4deba4c452dd..000000000000 --- a/dev-packages/browser-integration-tests/suites/tracing/metrics/pageload-resource-spans-streamed/init.js +++ /dev/null @@ -1,10 +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.spanStreamingIntegration()], - traceLifecycle: 'stream', - tracesSampleRate: 1, -}); diff --git a/dev-packages/browser-integration-tests/suites/tracing/metrics/pageload-resource-spans-streamed/template.html b/dev-packages/browser-integration-tests/suites/tracing/metrics/pageload-resource-spans-streamed/template.html deleted file mode 100644 index 8491137339f2..000000000000 --- a/dev-packages/browser-integration-tests/suites/tracing/metrics/pageload-resource-spans-streamed/template.html +++ /dev/null @@ -1,12 +0,0 @@ - - - - - - - - - - Rendered - - diff --git a/dev-packages/browser-integration-tests/suites/tracing/metrics/pageload-resource-spans-streamed/test.ts b/dev-packages/browser-integration-tests/suites/tracing/metrics/pageload-resource-spans-streamed/test.ts deleted file mode 100644 index a0a1e863137c..000000000000 --- a/dev-packages/browser-integration-tests/suites/tracing/metrics/pageload-resource-spans-streamed/test.ts +++ /dev/null @@ -1,68 +0,0 @@ -import type { Route } from '@playwright/test'; -import { expect } from '@playwright/test'; -import { sentryTest } from '../../../../utils/fixtures'; -import { shouldSkipTracingTest } from '../../../../utils/helpers'; -import { getSpanOp, getSpansFromEnvelope, waitForStreamedSpanEnvelope } from '../../../../utils/spanUtils'; - -const assetsDir = `${__dirname}/../pageload-resource-spans/assets`; - -sentryTest('names streamed resource spans after the resource domain', async ({ getLocalTestUrl, page }) => { - sentryTest.skip(shouldSkipTracingTest()); - - // Intercepting asset requests to avoid network-related flakiness and random retries (on Firefox). - await page.route('https://sentry-test-site.example/path/to/image.svg', (route: Route) => - route.fulfill({ - path: `${assetsDir}/image.svg`, - headers: { - 'Timing-Allow-Origin': '*', - 'Content-Type': 'image/svg+xml', - }, - }), - ); - await page.route('https://sentry-test-site.example/path/to/script.js', (route: Route) => - route.fulfill({ - path: `${assetsDir}/script.js`, - headers: { - 'Timing-Allow-Origin': '*', - 'Content-Type': 'application/javascript', - }, - }), - ); - await page.route('https://sentry-test-site.example/path/to/style.css', (route: Route) => - route.fulfill({ - path: `${assetsDir}/style.css`, - headers: { - 'Timing-Allow-Origin': '*', - 'Content-Type': 'text/css', - }, - }), - ); - - const spanEnvelopePromise = waitForStreamedSpanEnvelope( - page, - env => !!getSpansFromEnvelope(env).find(s => getSpanOp(s) === 'resource.img'), - ); - - const url = await getLocalTestUrl({ testDir: __dirname }); - await page.goto(url); - - const spans = getSpansFromEnvelope(await spanEnvelopePromise); - - const imgSpan = spans.find(s => getSpanOp(s) === 'resource.img'); - const linkSpan = spans.find(s => getSpanOp(s) === 'resource.link'); - - expect(imgSpan?.name).toBe('sentry-test-site.example'); - expect(imgSpan?.attributes['url.domain']).toEqual({ type: 'string', value: 'sentry-test-site.example' }); - expect(imgSpan?.attributes['url.full']).toEqual({ - type: 'string', - value: 'https://sentry-test-site.example/path/to/image.svg', - }); - - expect(linkSpan?.name).toBe('sentry-test-site.example'); - - // Same-origin resources used to be named by their origin-relative path, they now carry the test host. - const sameOriginScriptSpan = spans.find( - s => getSpanOp(s) === 'resource.script' && s.name !== 'sentry-test-site.example', - ); - expect(sameOriginScriptSpan?.name).toBe('sentry-test.io'); -}); diff --git a/dev-packages/browser-integration-tests/suites/tracing/request/fetch-relative-url-streamed/init.js b/dev-packages/browser-integration-tests/suites/tracing/metrics/pageload-resource-spans/init.js similarity index 87% rename from dev-packages/browser-integration-tests/suites/tracing/request/fetch-relative-url-streamed/init.js rename to dev-packages/browser-integration-tests/suites/tracing/metrics/pageload-resource-spans/init.js index 5ab240338c8c..83076460599f 100644 --- a/dev-packages/browser-integration-tests/suites/tracing/request/fetch-relative-url-streamed/init.js +++ b/dev-packages/browser-integration-tests/suites/tracing/metrics/pageload-resource-spans/init.js @@ -6,5 +6,4 @@ Sentry.init({ dsn: 'https://public@dsn.ingest.sentry.io/1337', integrations: [Sentry.browserTracingIntegration()], tracesSampleRate: 1, - autoSessionTracking: false, }); diff --git a/dev-packages/browser-integration-tests/suites/tracing/metrics/pageload-resource-spans/test.ts b/dev-packages/browser-integration-tests/suites/tracing/metrics/pageload-resource-spans/test.ts index 276234385fae..02749e913747 100644 --- a/dev-packages/browser-integration-tests/suites/tracing/metrics/pageload-resource-spans/test.ts +++ b/dev-packages/browser-integration-tests/suites/tracing/metrics/pageload-resource-spans/test.ts @@ -1,223 +1,263 @@ import { SENTRY_OP, SENTRY_ORIGIN } from '@sentry/conventions/attributes'; import type { Route } from '@playwright/test'; import { expect } from '@playwright/test'; -import { type Event } from '@sentry/core'; import { sentryTest } from '../../../../utils/fixtures'; -import { getFirstSentryEnvelopeRequest, shouldSkipTracingTest } from '../../../../utils/helpers'; +import { shouldSkipTracingTest } from '../../../../utils/helpers'; +import { collectStreamedSpans, getSpanOp, waitForStreamedSpan } from '../../../../utils/spanUtils'; -sentryTest('adds resource spans to pageload transaction', async ({ getLocalTestUrl, page, browserName }) => { - if (shouldSkipTracingTest()) { - sentryTest.skip(); - } +const assetsDir = `${__dirname}/assets`; - const isWebkitRun = browserName === 'webkit'; +sentryTest( + 'names streamed resource spans after the resource domain', + async ({ getLocalTestUrl, page, browserName }) => { + sentryTest.skip(shouldSkipTracingTest()); - // Intercepting asset requests to avoid network-related flakiness and random retries (on Firefox). - await page.route('https://sentry-test-site.example/path/to/image.svg', (route: Route) => - route.fulfill({ - path: `${__dirname}/assets/image.svg`, - headers: { - 'Timing-Allow-Origin': '*', - 'Content-Type': 'image/svg+xml', - }, - }), - ); - await page.route('https://sentry-test-site.example/path/to/script.js', (route: Route) => - route.fulfill({ - path: `${__dirname}/assets/script.js`, - headers: { - 'Timing-Allow-Origin': '*', - 'Content-Type': 'application/javascript', - }, - }), - ); - await page.route('https://sentry-test-site.example/path/to/style.css', (route: Route) => - route.fulfill({ - path: `${__dirname}/assets/style.css`, - headers: { - 'Timing-Allow-Origin': '*', - 'Content-Type': 'text/css', - }, - }), - ); - - const url = await getLocalTestUrl({ testDir: __dirname }); - - const eventData = await getFirstSentryEnvelopeRequest(page, url); - const resourceSpans = eventData.spans?.filter(({ op }) => op?.startsWith('resource')); - - const scriptSpans = resourceSpans?.filter(({ op }) => op === 'resource.script'); - const linkSpan = resourceSpans?.filter(({ op }) => op === 'resource.link')[0]; - const imgSpan = resourceSpans?.filter(({ op }) => op === 'resource.img')[0]; - - const spanId = eventData.contexts?.trace?.span_id; - const traceId = eventData.contexts?.trace?.trace_id; - - expect(spanId).toBeDefined(); - expect(traceId).toBeDefined(); - - const hasCdnBundle = (process.env.PW_BUNDLE || '').startsWith('bundle'); - - const expectedScripts = ['/init.bundle.js', 'https://sentry-test-site.example/path/to/script.js']; - if (hasCdnBundle) { - expectedScripts.unshift('/cdn.bundle.js'); - } - - expect(scriptSpans?.map(({ description }) => description).sort()).toEqual(expectedScripts); - expect(scriptSpans?.map(({ parent_span_id }) => parent_span_id)).toEqual(expectedScripts.map(() => spanId)); + // Intercepting asset requests to avoid network-related flakiness and random retries (on Firefox). + await page.route('https://sentry-test-site.example/path/to/image.svg', (route: Route) => + route.fulfill({ + path: `${assetsDir}/image.svg`, + headers: { + 'Timing-Allow-Origin': '*', + 'Content-Type': 'image/svg+xml', + }, + }), + ); + await page.route('https://sentry-test-site.example/path/to/script.js', (route: Route) => + route.fulfill({ + path: `${assetsDir}/script.js`, + headers: { + 'Timing-Allow-Origin': '*', + 'Content-Type': 'application/javascript', + }, + }), + ); + await page.route('https://sentry-test-site.example/path/to/style.css', (route: Route) => + route.fulfill({ + path: `${assetsDir}/style.css`, + headers: { + 'Timing-Allow-Origin': '*', + 'Content-Type': 'text/css', + }, + }), + ); - // The init bundle script is served from the test origin: its description is origin-relative, - // but `url.full` retains the full absolute URL (needed for span description inference). - const sameOriginScriptSpan = scriptSpans?.find(({ description }) => description === '/init.bundle.js'); - expect(sameOriginScriptSpan?.data?.['http.request.same_origin']).toBe(true); - expect(sameOriginScriptSpan?.data?.['url.full']).toMatch(/^https?:\/\/.+\/init\.bundle\.js$/); + const spans = collectStreamedSpans(page); + const pageloadPromise = waitForStreamedSpan(page, span => span.is_segment && getSpanOp(span) === 'pageload'); + const url = await getLocalTestUrl({ testDir: __dirname }); + await page.goto(url); + const pageload = await pageloadPromise; + await page.evaluate(() => (window as any).Sentry.flush()); + const externalResources = spans.filter(span => span.attributes['url.domain']?.value === 'sentry-test-site.example'); + expect(externalResources).toHaveLength(3); + externalResources.forEach(span => { + expect(span.parent_span_id).toBe(pageload.span_id); + expect(span.trace_id).toBe(pageload.trace_id); + }); - const customScriptSpan = scriptSpans?.find( - ({ description }) => description === 'https://sentry-test-site.example/path/to/script.js', - ); + const isWebkitRun = browserName === 'webkit'; + const resourceSpans = spans.filter(span => getSpanOp(span)?.startsWith('resource')); + const scriptSpans = resourceSpans.filter(span => getSpanOp(span) === 'resource.script'); + const imgSpan = resourceSpans.find(span => getSpanOp(span) === 'resource.img'); + const linkSpan = resourceSpans.find(span => getSpanOp(span) === 'resource.link'); + const customScriptSpan = scriptSpans.find( + span => span.attributes['url.domain']?.value === 'sentry-test-site.example', + ); + const sameOriginScriptSpan = scriptSpans.find( + span => span.attributes['url.full']?.value === new URL('init.bundle.js', url).href, + ); + const spanId = pageload.span_id; + const traceId = pageload.trace_id; + const expectedScripts = [new URL('init.bundle.js', url).href, 'https://sentry-test-site.example/path/to/script.js']; + if ((process.env.PW_BUNDLE || '').startsWith('bundle')) { + expectedScripts.push(new URL('cdn.bundle.js', url).href); + } + expect(scriptSpans.map(span => span.attributes['url.full']?.value).sort()).toEqual(expectedScripts.sort()); + expect(scriptSpans.map(span => span.parent_span_id)).toEqual(expectedScripts.map(() => spanId)); + expect(sameOriginScriptSpan?.name).toBe('sentry-test.io'); + expect(sameOriginScriptSpan?.attributes['http.request.same_origin']).toEqual({ type: 'boolean', value: true }); - expect(imgSpan).toEqual({ - data: { - 'http.response.body.decoded_size': expect.any(Number), - 'http.response.body.size': expect.any(Number), - 'http.response.size': expect.any(Number), - 'http.request.connect_start': expect.any(Number), - 'http.request.connection_end': expect.any(Number), - 'http.request.domain_lookup_end': expect.any(Number), - 'http.request.domain_lookup_start': expect.any(Number), - 'http.request.fetch_start': expect.any(Number), - 'http.request.redirect_end': expect.any(Number), - 'http.request.redirect_start': expect.any(Number), - 'http.request.request_start': expect.any(Number), - 'http.request.secure_connection_start': expect.any(Number), - 'http.request.worker_start': expect.any(Number), - 'http.request.response_end': expect.any(Number), - 'http.request.response_start': expect.any(Number), - 'http.request.time_to_first_byte': expect.any(Number), - 'network.protocol.name': '', - 'network.protocol.version': 'unknown', - [SENTRY_OP]: 'resource.img', - [SENTRY_ORIGIN]: 'auto.resource.browser.metrics', - 'server.address': 'sentry-test-site.example', - 'url.domain': 'sentry-test-site.example', - 'http.request.same_origin': false, - 'url.scheme': 'https', - 'url.full': 'https://sentry-test-site.example/path/to/image.svg', - // WebKit reports `deliveryType` as of Playwright 1.63's build, but still no response status - // or render blocking status. - 'http.response_delivery_type': '', - ...(!isWebkitRun && { - 'http.response.status_code': expect.any(Number), - 'resource.render_blocking_status': 'non-blocking', - }), - }, - description: 'https://sentry-test-site.example/path/to/image.svg', - op: 'resource.img', - origin: 'auto.resource.browser.metrics', - status: 'ok', - parent_span_id: spanId, - span_id: expect.stringMatching(/^[a-f\d]{16}$/), - start_timestamp: expect.any(Number), - timestamp: expect.any(Number), - trace_id: traceId, - }); + expect(imgSpan).toMatchObject({ + attributes: { + 'http.response.body.decoded_size': { + type: expect.stringMatching(/^(integer|double)$/), + value: expect.any(Number), + }, + 'http.response.body.size': { type: expect.stringMatching(/^(integer|double)$/), value: expect.any(Number) }, + 'http.response.size': { type: expect.stringMatching(/^(integer|double)$/), value: expect.any(Number) }, + 'http.request.connect_start': { type: expect.stringMatching(/^(integer|double)$/), value: expect.any(Number) }, + 'http.request.connection_end': { type: expect.stringMatching(/^(integer|double)$/), value: expect.any(Number) }, + 'http.request.domain_lookup_end': { + type: expect.stringMatching(/^(integer|double)$/), + value: expect.any(Number), + }, + 'http.request.domain_lookup_start': { + type: expect.stringMatching(/^(integer|double)$/), + value: expect.any(Number), + }, + 'http.request.fetch_start': { type: expect.stringMatching(/^(integer|double)$/), value: expect.any(Number) }, + 'http.request.redirect_end': { type: expect.stringMatching(/^(integer|double)$/), value: expect.any(Number) }, + 'http.request.redirect_start': { type: expect.stringMatching(/^(integer|double)$/), value: expect.any(Number) }, + 'http.request.request_start': { type: expect.stringMatching(/^(integer|double)$/), value: expect.any(Number) }, + 'http.request.secure_connection_start': { + type: expect.stringMatching(/^(integer|double)$/), + value: expect.any(Number), + }, + 'http.request.worker_start': { type: expect.stringMatching(/^(integer|double)$/), value: expect.any(Number) }, + 'http.request.response_end': { type: expect.stringMatching(/^(integer|double)$/), value: expect.any(Number) }, + 'http.request.response_start': { type: expect.stringMatching(/^(integer|double)$/), value: expect.any(Number) }, + 'http.request.time_to_first_byte': { + type: expect.stringMatching(/^(integer|double)$/), + value: expect.any(Number), + }, + 'network.protocol.name': { type: 'string', value: '' }, + 'network.protocol.version': { type: 'string', value: 'unknown' }, + [SENTRY_OP]: { type: 'string', value: 'resource.img' }, + [SENTRY_ORIGIN]: { type: 'string', value: 'auto.resource.browser.metrics' }, + 'server.address': { type: 'string', value: 'sentry-test-site.example' }, + 'url.domain': { type: 'string', value: 'sentry-test-site.example' }, + 'http.request.same_origin': { type: 'boolean', value: false }, + 'url.scheme': { type: 'string', value: 'https' }, + 'url.full': { type: 'string', value: 'https://sentry-test-site.example/path/to/image.svg' }, + // WebKit reports `deliveryType` as of Playwright 1.63's build, but still no response status + // or render blocking status. + 'http.response_delivery_type': { type: 'string', value: '' }, + ...(!isWebkitRun && { + 'http.response.status_code': { type: expect.stringMatching(/^(integer|double)$/), value: expect.any(Number) }, + 'resource.render_blocking_status': { type: 'string', value: 'non-blocking' }, + }), + }, + name: 'sentry-test-site.example', + status: 'ok', + parent_span_id: spanId, + span_id: expect.stringMatching(/^[a-f\d]{16}$/), + start_timestamp: expect.any(Number), + end_timestamp: expect.any(Number), + trace_id: traceId, + }); - // range check: TTFB is reasonably <10 seconds, which is really a check that we report it in - // seconds rather than milliseconds. WebKit resolves these intercepted routes without measurable - // delay, so only the other engines are held to a non-zero value. - const imgSpanTtfb = imgSpan?.data['http.request.time_to_first_byte']; - expect(imgSpanTtfb).toBeGreaterThan(isWebkitRun ? -1 : 0); - expect(imgSpanTtfb).toBeLessThan(10); + // range check: TTFB is reasonably <10 seconds, which is really a check that we report it in + // seconds rather than milliseconds. WebKit resolves these intercepted routes without measurable + // delay, so only the other engines are held to a non-zero value. + const imgSpanTtfb = imgSpan?.attributes['http.request.time_to_first_byte']?.value; + expect(imgSpanTtfb).toBeGreaterThan(isWebkitRun ? -1 : 0); + expect(imgSpanTtfb).toBeLessThan(10); - expect(linkSpan).toEqual({ - data: { - 'http.response.body.decoded_size': expect.any(Number), - 'http.response.body.size': expect.any(Number), - 'http.response.size': expect.any(Number), - 'http.request.connect_start': expect.any(Number), - 'http.request.connection_end': expect.any(Number), - 'http.request.domain_lookup_end': expect.any(Number), - 'http.request.domain_lookup_start': expect.any(Number), - 'http.request.fetch_start': expect.any(Number), - 'http.request.redirect_end': expect.any(Number), - 'http.request.redirect_start': expect.any(Number), - 'http.request.request_start': expect.any(Number), - 'http.request.secure_connection_start': expect.any(Number), - 'http.request.worker_start': expect.any(Number), - 'http.request.response_end': expect.any(Number), - 'http.request.response_start': expect.any(Number), - 'http.request.time_to_first_byte': expect.any(Number), - 'network.protocol.name': '', - 'network.protocol.version': 'unknown', - [SENTRY_OP]: 'resource.link', - [SENTRY_ORIGIN]: 'auto.resource.browser.metrics', - 'server.address': 'sentry-test-site.example', - 'url.domain': 'sentry-test-site.example', - 'http.request.same_origin': false, - 'url.scheme': 'https', - 'url.full': 'https://sentry-test-site.example/path/to/style.css', - // WebKit reports `deliveryType` as of Playwright 1.63's build, but still no response status - // or render blocking status. - 'http.response_delivery_type': '', - ...(!isWebkitRun && { - 'http.response.status_code': expect.any(Number), - 'resource.render_blocking_status': 'non-blocking', - }), - }, - description: 'https://sentry-test-site.example/path/to/style.css', - op: 'resource.link', - origin: 'auto.resource.browser.metrics', - status: 'ok', - parent_span_id: spanId, - span_id: expect.stringMatching(/^[a-f\d]{16}$/), - start_timestamp: expect.any(Number), - timestamp: expect.any(Number), - trace_id: traceId, - }); + expect(linkSpan).toMatchObject({ + attributes: { + 'http.response.body.decoded_size': { + type: expect.stringMatching(/^(integer|double)$/), + value: expect.any(Number), + }, + 'http.response.body.size': { type: expect.stringMatching(/^(integer|double)$/), value: expect.any(Number) }, + 'http.response.size': { type: expect.stringMatching(/^(integer|double)$/), value: expect.any(Number) }, + 'http.request.connect_start': { type: expect.stringMatching(/^(integer|double)$/), value: expect.any(Number) }, + 'http.request.connection_end': { type: expect.stringMatching(/^(integer|double)$/), value: expect.any(Number) }, + 'http.request.domain_lookup_end': { + type: expect.stringMatching(/^(integer|double)$/), + value: expect.any(Number), + }, + 'http.request.domain_lookup_start': { + type: expect.stringMatching(/^(integer|double)$/), + value: expect.any(Number), + }, + 'http.request.fetch_start': { type: expect.stringMatching(/^(integer|double)$/), value: expect.any(Number) }, + 'http.request.redirect_end': { type: expect.stringMatching(/^(integer|double)$/), value: expect.any(Number) }, + 'http.request.redirect_start': { type: expect.stringMatching(/^(integer|double)$/), value: expect.any(Number) }, + 'http.request.request_start': { type: expect.stringMatching(/^(integer|double)$/), value: expect.any(Number) }, + 'http.request.secure_connection_start': { + type: expect.stringMatching(/^(integer|double)$/), + value: expect.any(Number), + }, + 'http.request.worker_start': { type: expect.stringMatching(/^(integer|double)$/), value: expect.any(Number) }, + 'http.request.response_end': { type: expect.stringMatching(/^(integer|double)$/), value: expect.any(Number) }, + 'http.request.response_start': { type: expect.stringMatching(/^(integer|double)$/), value: expect.any(Number) }, + 'http.request.time_to_first_byte': { + type: expect.stringMatching(/^(integer|double)$/), + value: expect.any(Number), + }, + 'network.protocol.name': { type: 'string', value: '' }, + 'network.protocol.version': { type: 'string', value: 'unknown' }, + [SENTRY_OP]: { type: 'string', value: 'resource.link' }, + [SENTRY_ORIGIN]: { type: 'string', value: 'auto.resource.browser.metrics' }, + 'server.address': { type: 'string', value: 'sentry-test-site.example' }, + 'url.domain': { type: 'string', value: 'sentry-test-site.example' }, + 'http.request.same_origin': { type: 'boolean', value: false }, + 'url.scheme': { type: 'string', value: 'https' }, + 'url.full': { type: 'string', value: 'https://sentry-test-site.example/path/to/style.css' }, + // WebKit reports `deliveryType` as of Playwright 1.63's build, but still no response status + // or render blocking status. + 'http.response_delivery_type': { type: 'string', value: '' }, + ...(!isWebkitRun && { + 'http.response.status_code': { type: expect.stringMatching(/^(integer|double)$/), value: expect.any(Number) }, + 'resource.render_blocking_status': { type: 'string', value: 'non-blocking' }, + }), + }, + name: 'sentry-test-site.example', + status: 'ok', + parent_span_id: spanId, + span_id: expect.stringMatching(/^[a-f\d]{16}$/), + start_timestamp: expect.any(Number), + end_timestamp: expect.any(Number), + trace_id: traceId, + }); - expect(customScriptSpan).toEqual({ - data: { - 'http.response.body.decoded_size': expect.any(Number), - 'http.response.body.size': expect.any(Number), - 'http.response.size': expect.any(Number), - 'http.request.connection_end': expect.any(Number), - 'http.request.connect_start': expect.any(Number), - 'http.request.domain_lookup_end': expect.any(Number), - 'http.request.domain_lookup_start': expect.any(Number), - 'http.request.fetch_start': expect.any(Number), - 'http.request.redirect_end': expect.any(Number), - 'http.request.redirect_start': expect.any(Number), - 'http.request.request_start': expect.any(Number), - 'http.request.secure_connection_start': expect.any(Number), - 'http.request.worker_start': expect.any(Number), - 'http.request.response_end': expect.any(Number), - 'http.request.response_start': expect.any(Number), - 'http.request.time_to_first_byte': expect.any(Number), - 'network.protocol.name': '', - 'network.protocol.version': 'unknown', - 'sentry.op': 'resource.script', - 'sentry.origin': 'auto.resource.browser.metrics', - 'server.address': 'sentry-test-site.example', - 'url.domain': 'sentry-test-site.example', - 'http.request.same_origin': false, - 'url.scheme': 'https', - 'url.full': 'https://sentry-test-site.example/path/to/script.js', - // WebKit reports `deliveryType` as of Playwright 1.63's build, but still no response status - // or render blocking status. - 'http.response_delivery_type': '', - ...(!isWebkitRun && { - 'http.response.status_code': expect.any(Number), - 'resource.render_blocking_status': 'non-blocking', - }), - }, - description: 'https://sentry-test-site.example/path/to/script.js', - op: 'resource.script', - origin: 'auto.resource.browser.metrics', - status: 'ok', - parent_span_id: spanId, - span_id: expect.stringMatching(/^[a-f\d]{16}$/), - start_timestamp: expect.any(Number), - timestamp: expect.any(Number), - trace_id: traceId, - }); -}); + expect(customScriptSpan).toMatchObject({ + attributes: { + 'http.response.body.decoded_size': { + type: expect.stringMatching(/^(integer|double)$/), + value: expect.any(Number), + }, + 'http.response.body.size': { type: expect.stringMatching(/^(integer|double)$/), value: expect.any(Number) }, + 'http.response.size': { type: expect.stringMatching(/^(integer|double)$/), value: expect.any(Number) }, + 'http.request.connection_end': { type: expect.stringMatching(/^(integer|double)$/), value: expect.any(Number) }, + 'http.request.connect_start': { type: expect.stringMatching(/^(integer|double)$/), value: expect.any(Number) }, + 'http.request.domain_lookup_end': { + type: expect.stringMatching(/^(integer|double)$/), + value: expect.any(Number), + }, + 'http.request.domain_lookup_start': { + type: expect.stringMatching(/^(integer|double)$/), + value: expect.any(Number), + }, + 'http.request.fetch_start': { type: expect.stringMatching(/^(integer|double)$/), value: expect.any(Number) }, + 'http.request.redirect_end': { type: expect.stringMatching(/^(integer|double)$/), value: expect.any(Number) }, + 'http.request.redirect_start': { type: expect.stringMatching(/^(integer|double)$/), value: expect.any(Number) }, + 'http.request.request_start': { type: expect.stringMatching(/^(integer|double)$/), value: expect.any(Number) }, + 'http.request.secure_connection_start': { + type: expect.stringMatching(/^(integer|double)$/), + value: expect.any(Number), + }, + 'http.request.worker_start': { type: expect.stringMatching(/^(integer|double)$/), value: expect.any(Number) }, + 'http.request.response_end': { type: expect.stringMatching(/^(integer|double)$/), value: expect.any(Number) }, + 'http.request.response_start': { type: expect.stringMatching(/^(integer|double)$/), value: expect.any(Number) }, + 'http.request.time_to_first_byte': { + type: expect.stringMatching(/^(integer|double)$/), + value: expect.any(Number), + }, + 'network.protocol.name': { type: 'string', value: '' }, + 'network.protocol.version': { type: 'string', value: 'unknown' }, + 'sentry.op': { type: 'string', value: 'resource.script' }, + 'sentry.origin': { type: 'string', value: 'auto.resource.browser.metrics' }, + 'server.address': { type: 'string', value: 'sentry-test-site.example' }, + 'url.domain': { type: 'string', value: 'sentry-test-site.example' }, + 'http.request.same_origin': { type: 'boolean', value: false }, + 'url.scheme': { type: 'string', value: 'https' }, + 'url.full': { type: 'string', value: 'https://sentry-test-site.example/path/to/script.js' }, + // WebKit reports `deliveryType` as of Playwright 1.63's build, but still no response status + // or render blocking status. + 'http.response_delivery_type': { type: 'string', value: '' }, + ...(!isWebkitRun && { + 'http.response.status_code': { type: expect.stringMatching(/^(integer|double)$/), value: expect.any(Number) }, + 'resource.render_blocking_status': { type: 'string', value: 'non-blocking' }, + }), + }, + name: 'sentry-test-site.example', + status: 'ok', + parent_span_id: spanId, + span_id: expect.stringMatching(/^[a-f\d]{16}$/), + start_timestamp: expect.any(Number), + end_timestamp: expect.any(Number), + trace_id: traceId, + }); + }, +); diff --git a/dev-packages/browser-integration-tests/suites/tracing/metrics/web-vitals-cls-streamed-spans/init.js b/dev-packages/browser-integration-tests/suites/tracing/metrics/web-vitals-cls-streamed-spans/init.js deleted file mode 100644 index bd3b6ed17872..000000000000 --- a/dev-packages/browser-integration-tests/suites/tracing/metrics/web-vitals-cls-streamed-spans/init.js +++ /dev/null @@ -1,11 +0,0 @@ -import * as Sentry from '@sentry/browser'; - -window.Sentry = Sentry; -window._testBaseTimestamp = performance.timeOrigin / 1000; - -Sentry.init({ - dsn: 'https://public@dsn.ingest.sentry.io/1337', - integrations: [Sentry.browserTracingIntegration(), Sentry.spanStreamingIntegration()], - traceLifecycle: 'stream', - tracesSampleRate: 1, -}); diff --git a/dev-packages/browser-integration-tests/suites/tracing/metrics/web-vitals-cls-streamed-spans/subject.js b/dev-packages/browser-integration-tests/suites/tracing/metrics/web-vitals-cls-streamed-spans/subject.js deleted file mode 100644 index 9742a4a5cc29..000000000000 --- a/dev-packages/browser-integration-tests/suites/tracing/metrics/web-vitals-cls-streamed-spans/subject.js +++ /dev/null @@ -1,17 +0,0 @@ -import { simulateCLS } from '../../../../utils/web-vitals/cls.ts'; - -// Simulate Layout shift right at the beginning of the page load, depending on the URL hash -// don't run if expected CLS is NaN -const expectedCLS = Number(location.hash.slice(1)); -if (expectedCLS && expectedCLS >= 0) { - simulateCLS(expectedCLS).then(() => window.dispatchEvent(new Event('cls-done'))); -} - -// Simulate layout shift whenever the trigger-cls event is dispatched -// Cannot trigger via a button click because expected layout shift after -// an interaction doesn't contribute to CLS. -window.addEventListener('trigger-cls', () => { - simulateCLS(0.1).then(() => { - window.dispatchEvent(new Event('cls-done')); - }); -}); diff --git a/dev-packages/browser-integration-tests/suites/tracing/metrics/web-vitals-cls-streamed-spans/template.html b/dev-packages/browser-integration-tests/suites/tracing/metrics/web-vitals-cls-streamed-spans/template.html deleted file mode 100644 index 10e2e22f7d6a..000000000000 --- a/dev-packages/browser-integration-tests/suites/tracing/metrics/web-vitals-cls-streamed-spans/template.html +++ /dev/null @@ -1,10 +0,0 @@ - - - - - - -
-

Some content

- - diff --git a/dev-packages/browser-integration-tests/suites/tracing/metrics/web-vitals-cls-streamed-spans/test.ts b/dev-packages/browser-integration-tests/suites/tracing/metrics/web-vitals-cls-streamed-spans/test.ts deleted file mode 100644 index 02ce7b981ed9..000000000000 --- a/dev-packages/browser-integration-tests/suites/tracing/metrics/web-vitals-cls-streamed-spans/test.ts +++ /dev/null @@ -1,79 +0,0 @@ -import type { Page } from '@playwright/test'; -import { expect } from '@playwright/test'; -import { sentryTest } from '../../../../utils/fixtures'; -import { hidePage, shouldSkipTracingTest } from '../../../../utils/helpers'; -import { getSpanOp, waitForStreamedSpan } from '../../../../utils/spanUtils'; - -sentryTest.beforeEach(async ({ browserName, page }) => { - if (shouldSkipTracingTest() || browserName !== 'chromium') { - sentryTest.skip(); - } - - await page.setViewportSize({ width: 800, height: 1200 }); -}); - -function waitForLayoutShift(page: Page): Promise { - return page.evaluate(() => { - return new Promise(resolve => { - window.addEventListener('cls-done', () => resolve()); - }); - }); -} - -sentryTest('captures CLS as a streamed span with source attributes', async ({ getLocalTestUrl, page }) => { - const url = await getLocalTestUrl({ testDir: __dirname }); - - const clsSpanPromise = waitForStreamedSpan(page, span => getSpanOp(span) === 'ui.webvital.cls'); - const pageloadSpanPromise = waitForStreamedSpan(page, span => getSpanOp(span) === 'pageload'); - - await page.goto(`${url}#0.15`); - await waitForLayoutShift(page); - await hidePage(page); - - const clsSpan = await clsSpanPromise; - const pageloadSpan = await pageloadSpanPromise; - - expect(clsSpan.attributes['sentry.op']).toEqual({ type: 'string', value: 'ui.webvital.cls' }); - expect(clsSpan.attributes['sentry.origin']).toEqual({ type: 'string', value: 'auto.http.browser.cls' }); - expect(clsSpan.attributes['sentry.exclusive_time']).toEqual({ type: 'integer', value: 0 }); - expect(clsSpan.attributes['user_agent.original']?.value).toEqual(expect.stringContaining('Chrome')); - - // Check the CLS span carries the segment name it belongs to - expect(clsSpan.attributes['sentry.segment.name']).toEqual({ type: 'string', value: 'Pageload' }); - - // Check browser.web_vital.cls.source attributes - expect(clsSpan.attributes['browser.web_vital.cls.source.1']?.value).toEqual( - expect.stringContaining('body > div#content > p'), - ); - - // Check pageload span id is present - expect(clsSpan.attributes['sentry.pageload.span_id']?.value).toBe(pageloadSpan.span_id); - - // CLS is a point-in-time metric - expect(clsSpan.start_timestamp).toEqual(clsSpan.end_timestamp); - - expect(clsSpan.span_id).toMatch(/^[\da-f]{16}$/); - expect(clsSpan.trace_id).toMatch(/^[\da-f]{32}$/); - - expect(clsSpan.parent_span_id).toBe(pageloadSpan.span_id); - expect(clsSpan.trace_id).toBe(pageloadSpan.trace_id); -}); - -sentryTest('CLS streamed span has web vital value attribute', async ({ getLocalTestUrl, page }) => { - const url = await getLocalTestUrl({ testDir: __dirname }); - - const clsSpanPromise = waitForStreamedSpan(page, span => getSpanOp(span) === 'ui.webvital.cls'); - - await page.goto(`${url}#0.1`); - await waitForLayoutShift(page); - await hidePage(page); - - const clsSpan = await clsSpanPromise; - - // The CLS value should be set as a browser.web_vital.cls.value attribute - expect(clsSpan.attributes['browser.web_vital.cls.value']?.type).toBe('double'); - // Flakey value dependent on timings -> we check for a range - const clsValue = clsSpan.attributes['browser.web_vital.cls.value']?.value as number; - expect(clsValue).toBeGreaterThan(0.05); - expect(clsValue).toBeLessThan(0.15); -}); diff --git a/dev-packages/browser-integration-tests/suites/tracing/metrics/web-vitals-cls/init.js b/dev-packages/browser-integration-tests/suites/tracing/metrics/web-vitals-cls/init.js index 38429010914d..1f0b64911a75 100644 --- a/dev-packages/browser-integration-tests/suites/tracing/metrics/web-vitals-cls/init.js +++ b/dev-packages/browser-integration-tests/suites/tracing/metrics/web-vitals-cls/init.js @@ -1,14 +1,10 @@ import * as Sentry from '@sentry/browser'; window.Sentry = Sentry; +window._testBaseTimestamp = performance.timeOrigin / 1000; Sentry.init({ - traceLifecycle: 'static', dsn: 'https://public@dsn.ingest.sentry.io/1337', - integrations: [ - Sentry.browserTracingIntegration({ - idleTimeout: 5000, - }), - ], + integrations: [Sentry.browserTracingIntegration()], tracesSampleRate: 1, }); diff --git a/dev-packages/browser-integration-tests/suites/tracing/metrics/web-vitals-cls/subject.js b/dev-packages/browser-integration-tests/suites/tracing/metrics/web-vitals-cls/subject.js index b33aea72d7c9..9742a4a5cc29 100644 --- a/dev-packages/browser-integration-tests/suites/tracing/metrics/web-vitals-cls/subject.js +++ b/dev-packages/browser-integration-tests/suites/tracing/metrics/web-vitals-cls/subject.js @@ -1,9 +1,17 @@ import { simulateCLS } from '../../../../utils/web-vitals/cls.ts'; -// Getting expected CLS parameter from URL hash +// Simulate Layout shift right at the beginning of the page load, depending on the URL hash +// don't run if expected CLS is NaN const expectedCLS = Number(location.hash.slice(1)); +if (expectedCLS && expectedCLS >= 0) { + simulateCLS(expectedCLS).then(() => window.dispatchEvent(new Event('cls-done'))); +} -// CLS lands on the pageload span when it ends on the idle timeout, so nothing has to force the -// page away to finalize it. Reloading here used to do that, but it raced the envelope: on a fast -// browser the reload cancelled the in-flight send before it left the page. -simulateCLS(expectedCLS); +// Simulate layout shift whenever the trigger-cls event is dispatched +// Cannot trigger via a button click because expected layout shift after +// an interaction doesn't contribute to CLS. +window.addEventListener('trigger-cls', () => { + simulateCLS(0.1).then(() => { + window.dispatchEvent(new Event('cls-done')); + }); +}); diff --git a/dev-packages/browser-integration-tests/suites/tracing/metrics/web-vitals-cls/template.html b/dev-packages/browser-integration-tests/suites/tracing/metrics/web-vitals-cls/template.html index 72c22be8e49a..10e2e22f7d6a 100644 --- a/dev-packages/browser-integration-tests/suites/tracing/metrics/web-vitals-cls/template.html +++ b/dev-packages/browser-integration-tests/suites/tracing/metrics/web-vitals-cls/template.html @@ -5,5 +5,6 @@
+

Some content

diff --git a/dev-packages/browser-integration-tests/suites/tracing/metrics/web-vitals-cls/test.ts b/dev-packages/browser-integration-tests/suites/tracing/metrics/web-vitals-cls/test.ts index f00a2b60f100..853bc078697f 100644 --- a/dev-packages/browser-integration-tests/suites/tracing/metrics/web-vitals-cls/test.ts +++ b/dev-packages/browser-integration-tests/suites/tracing/metrics/web-vitals-cls/test.ts @@ -1,7 +1,8 @@ +import type { Page } from '@playwright/test'; import { expect } from '@playwright/test'; -import type { Event } from '@sentry/core'; import { sentryTest } from '../../../../utils/fixtures'; -import { getFirstSentryEnvelopeRequest, shouldSkipTracingTest } from '../../../../utils/helpers'; +import { hidePage, shouldSkipTracingTest } from '../../../../utils/helpers'; +import { getSpanOp, waitForStreamedSpan } from '../../../../utils/spanUtils'; sentryTest.beforeEach(async ({ browserName, page }) => { if (shouldSkipTracingTest() || browserName !== 'chromium') { @@ -11,43 +12,70 @@ sentryTest.beforeEach(async ({ browserName, page }) => { await page.setViewportSize({ width: 800, height: 1200 }); }); -sentryTest('should capture a "GOOD" CLS vital with its source(s).', async ({ getLocalTestUrl, page }) => { +function waitForLayoutShift(page: Page): Promise { + return page.evaluate(() => { + return new Promise(resolve => { + window.addEventListener('cls-done', () => resolve()); + }); + }); +} + +sentryTest('captures CLS as a streamed span with source attributes', async ({ getLocalTestUrl, page }) => { const url = await getLocalTestUrl({ testDir: __dirname }); - const eventData = await getFirstSentryEnvelopeRequest(page, `${url}#0.05`); - expect(eventData.measurements).toBeDefined(); - expect(eventData.measurements?.cls?.value).toBeDefined(); + const clsSpanPromise = waitForStreamedSpan(page, span => getSpanOp(span) === 'ui.webvital.cls'); + const pageloadSpanPromise = waitForStreamedSpan(page, span => getSpanOp(span) === 'pageload'); - // Flakey value dependent on timings -> we check for a range - expect(eventData.measurements?.cls?.value).toBeGreaterThan(0.03); - expect(eventData.measurements?.cls?.value).toBeLessThan(0.07); + await page.goto(`${url}#0.15`); + await waitForLayoutShift(page); + await hidePage(page); - expect(eventData.contexts?.trace?.data?.['cls.source.1']).toBe('body > div#content > p#partial'); -}); + const clsSpan = await clsSpanPromise; + const pageloadSpan = await pageloadSpanPromise; -sentryTest('should capture a "MEH" CLS vital with its source(s).', async ({ getLocalTestUrl, page }) => { - const url = await getLocalTestUrl({ testDir: __dirname }); - const eventData = await getFirstSentryEnvelopeRequest(page, `${url}#0.21`); + expect(clsSpan.attributes['sentry.op']).toEqual({ type: 'string', value: 'ui.webvital.cls' }); + expect(clsSpan.attributes['sentry.origin']).toEqual({ type: 'string', value: 'auto.http.browser.cls' }); + expect(clsSpan.attributes['sentry.exclusive_time']).toEqual({ type: 'integer', value: 0 }); + expect(clsSpan.attributes['user_agent.original']?.value).toEqual(expect.stringContaining('Chrome')); - expect(eventData.measurements).toBeDefined(); - expect(eventData.measurements?.cls?.value).toBeDefined(); + // Check the CLS span carries the segment name it belongs to + expect(clsSpan.attributes['sentry.segment.name']).toEqual({ type: 'string', value: 'Pageload' }); - // Flakey value dependent on timings -> we check for a range - expect(eventData.measurements?.cls?.value).toBeGreaterThan(0.18); - expect(eventData.measurements?.cls?.value).toBeLessThan(0.23); + // Check browser.web_vital.cls.source attributes + expect(clsSpan.attributes['browser.web_vital.cls.source.1']?.value).toEqual( + expect.stringContaining('body > div#content > p'), + ); - expect(eventData.contexts?.trace?.data?.['cls.source.1']).toBe('body > div#content > p'); -}); + // Check pageload span id is present + expect(clsSpan.attributes['sentry.pageload.span_id']?.value).toBe(pageloadSpan.span_id); -sentryTest('should capture a "POOR" CLS vital with its source(s).', async ({ getLocalTestUrl, page }) => { - const url = await getLocalTestUrl({ testDir: __dirname }); - const eventData = await getFirstSentryEnvelopeRequest(page, `${url}#0.35`); + // CLS is a point-in-time metric + expect(clsSpan.start_timestamp).toEqual(clsSpan.end_timestamp); + + expect(clsSpan.span_id).toMatch(/^[\da-f]{16}$/); + expect(clsSpan.trace_id).toMatch(/^[\da-f]{32}$/); + + expect(clsSpan.parent_span_id).toBe(pageloadSpan.span_id); + expect(clsSpan.trace_id).toBe(pageloadSpan.trace_id); +}); - expect(eventData.measurements).toBeDefined(); - expect(eventData.measurements?.cls?.value).toBeDefined(); +[ + { cls: 0.05, min: 0.03, max: 0.07, source: 'body > div#content > p#partial' }, + { cls: 0.21, min: 0.18, max: 0.23, source: 'body > div#content > p' }, + { cls: 0.35, min: 0.34, max: 0.36, source: 'body > div#content > p' }, +].forEach(({ cls, min, max, source }) => { + sentryTest(`captures CLS ${cls} with its source`, async ({ getLocalTestUrl, page }) => { + const url = await getLocalTestUrl({ testDir: __dirname }); + const clsPromise = waitForStreamedSpan(page, span => getSpanOp(span) === 'ui.webvital.cls'); + await page.goto(`${url}#${cls}`); + await waitForLayoutShift(page); + await hidePage(page); + const span = await clsPromise; - // Flakey value dependent on timings -> we check for a range - expect(eventData.measurements?.cls?.value).toBeGreaterThan(0.34); - expect(eventData.measurements?.cls?.value).toBeLessThan(0.36); - expect(eventData.contexts?.trace?.data?.['cls.source.1']).toBe('body > div#content > p'); + expect(span.attributes['browser.web_vital.cls.value']?.type).toBe('double'); + const value = span.attributes['browser.web_vital.cls.value']?.value; + expect(value).toBeGreaterThan(min); + expect(value).toBeLessThan(max); + expect(span.attributes['browser.web_vital.cls.source.1']).toEqual({ type: 'string', value: source }); + }); }); diff --git a/dev-packages/browser-integration-tests/suites/tracing/metrics/web-vitals-fp-fcp/test.ts b/dev-packages/browser-integration-tests/suites/tracing/metrics/web-vitals-fp-fcp/test.ts index cd63b699cc06..93329de1675d 100644 --- a/dev-packages/browser-integration-tests/suites/tracing/metrics/web-vitals-fp-fcp/test.ts +++ b/dev-packages/browser-integration-tests/suites/tracing/metrics/web-vitals-fp-fcp/test.ts @@ -1,41 +1,26 @@ import { expect } from '@playwright/test'; -import type { Event } from '@sentry/core'; import { sentryTest } from '../../../../utils/fixtures'; -import { getFirstSentryEnvelopeRequest, shouldSkipTracingTest } from '../../../../utils/helpers'; - -sentryTest('should capture FP vital.', async ({ browserName, getLocalTestUrl, page }) => { - // FP is not generated on webkit or firefox - if (shouldSkipTracingTest() || browserName !== 'chromium') { - sentryTest.skip(); - } - - const url = await getLocalTestUrl({ testDir: __dirname }); - const eventData = await getFirstSentryEnvelopeRequest(page, url); - - expect(eventData.measurements).toBeDefined(); - expect(eventData.measurements?.fp?.value).toBeDefined(); - - const fpSpan = eventData.spans?.filter(({ description }) => description === 'first-paint')[0]; - - expect(fpSpan).toBeDefined(); - expect(fpSpan?.op).toBe('browser.paint'); - expect(fpSpan?.parent_span_id).toBe(eventData.contexts?.trace?.span_id); -}); - -sentryTest('should capture FCP vital.', async ({ getLocalTestUrl, page }) => { - if (shouldSkipTracingTest()) { - sentryTest.skip(); - } - - const url = await getLocalTestUrl({ testDir: __dirname }); - const eventData = await getFirstSentryEnvelopeRequest(page, url); - - expect(eventData.measurements).toBeDefined(); - expect(eventData.measurements?.fcp?.value).toBeDefined(); - - const fcpSpan = eventData.spans?.filter(({ description }) => description === 'first-contentful-paint')[0]; - - expect(fcpSpan).toBeDefined(); - expect(fcpSpan?.op).toBe('browser.paint'); - expect(fcpSpan?.parent_span_id).toBe(eventData.contexts?.trace?.span_id); +import { shouldSkipTracingTest } from '../../../../utils/helpers'; +import { collectStreamedSpans, getSpanOp, waitForStreamedSpan } from '../../../../utils/spanUtils'; + +[ + { vital: 'fp', name: 'first-paint', chromiumOnly: true }, + { vital: 'fcp', name: 'first-contentful-paint', chromiumOnly: false }, +].forEach(({ vital, name, chromiumOnly }) => { + sentryTest(`captures ${vital.toUpperCase()} and its paint span`, async ({ getLocalTestUrl, page, browserName }) => { + sentryTest.skip(shouldSkipTracingTest() || (chromiumOnly && browserName !== 'chromium')); + const spans = collectStreamedSpans(page); + const pageloadPromise = waitForStreamedSpan(page, span => span.is_segment && getSpanOp(span) === 'pageload'); + const url = await getLocalTestUrl({ testDir: __dirname }); + await page.goto(url); + const pageload = await pageloadPromise; + await page.evaluate(() => (window as any).Sentry.flush()); + + expect(pageload.attributes[`browser.web_vital.${vital}.value`]?.value).toBeGreaterThan(0); + const paintSpans = spans.filter(span => span.name === name); + expect(paintSpans).toHaveLength(1); + expect(getSpanOp(paintSpans[0])).toBe('browser.paint'); + expect(paintSpans[0].parent_span_id).toBe(pageload.span_id); + expect(paintSpans[0].trace_id).toBe(pageload.trace_id); + }); }); diff --git a/dev-packages/browser-integration-tests/suites/tracing/metrics/web-vitals-inp-before-send-span/init.js b/dev-packages/browser-integration-tests/suites/tracing/metrics/web-vitals-inp-before-send-span/init.js index 5dd891ccc2f8..b320fb9f5984 100644 --- a/dev-packages/browser-integration-tests/suites/tracing/metrics/web-vitals-inp-before-send-span/init.js +++ b/dev-packages/browser-integration-tests/suites/tracing/metrics/web-vitals-inp-before-send-span/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({ @@ -15,16 +14,14 @@ Sentry.init({ }), ], tracesSampleRate: 1, - // A plain (non-streamed) `beforeSendSpan` operates on the v1 `SpanJSON`. INP is sent as a v2 span, - // so this verifies the static callback still runs and its changes are carried into the v2 span. - beforeSendSpan: Sentry.withStaticSpan(span => { - if (span.op === 'ui.interaction.click') { - span.description = 'scrubbed'; - span.data['custom.attribute'] = 'from-before-send-span'; + beforeSendSpan: span => { + if (span.attributes['sentry.op'] === 'ui.interaction.click') { + span.name = 'scrubbed'; + span.attributes['custom.attribute'] = 'from-before-send-span'; } return span; - }), + }, debug: true, }); diff --git a/dev-packages/browser-integration-tests/suites/tracing/metrics/web-vitals-inp-before-send-span/test.ts b/dev-packages/browser-integration-tests/suites/tracing/metrics/web-vitals-inp-before-send-span/test.ts index add8a0ac71c3..8f62e9e283ce 100644 --- a/dev-packages/browser-integration-tests/suites/tracing/metrics/web-vitals-inp-before-send-span/test.ts +++ b/dev-packages/browser-integration-tests/suites/tracing/metrics/web-vitals-inp-before-send-span/test.ts @@ -3,11 +3,7 @@ import { sentryTest } from '../../../../utils/fixtures'; import { hidePage, shouldSkipTracingTest } from '../../../../utils/helpers'; import { getSpanOp, getSpansFromEnvelope, waitForStreamedSpanEnvelope } from '../../../../utils/spanUtils'; -// This app does not enable span streaming (`traceLifecycle: 'static'`) and defines a plain, non-streamed -// `beforeSendSpan` callback (operating on the v1 `SpanJSON`). INP is still emitted as a v2 span, so this -// verifies the static callback runs for INP and its modifications are carried into the v2 span. - -sentryTest('runs a non-streamed `beforeSendSpan` for the INP span', async ({ browserName, getLocalTestUrl, page }) => { +sentryTest('runs `beforeSendSpan` for the streamed INP span', async ({ browserName, getLocalTestUrl, page }) => { const supportedBrowsers = ['chromium']; if (shouldSkipTracingTest() || !supportedBrowsers.includes(browserName)) { diff --git a/dev-packages/browser-integration-tests/suites/tracing/metrics/web-vitals-inp-late/init.js b/dev-packages/browser-integration-tests/suites/tracing/metrics/web-vitals-inp-late/init.js index 398edbd14fe2..9ae2938e2969 100644 --- a/dev-packages/browser-integration-tests/suites/tracing/metrics/web-vitals-inp-late/init.js +++ b/dev-packages/browser-integration-tests/suites/tracing/metrics/web-vitals-inp-late/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/metrics/web-vitals-inp-late/test.ts b/dev-packages/browser-integration-tests/suites/tracing/metrics/web-vitals-inp-late/test.ts index 10073a27e212..0312fd23d23f 100644 --- a/dev-packages/browser-integration-tests/suites/tracing/metrics/web-vitals-inp-late/test.ts +++ b/dev-packages/browser-integration-tests/suites/tracing/metrics/web-vitals-inp-late/test.ts @@ -54,7 +54,7 @@ sentryTest( expect(itemHeader).toEqual({ type: 'span', - item_count: 1, + item_count: getSpansFromEnvelope(spanEnvelope).length, content_type: 'application/vnd.sentry.items.span.v2+json', }); @@ -64,7 +64,7 @@ sentryTest( const pageloadSpanId = inpSpan.parent_span_id; expect(inpSpan).toEqual({ - name: 'body > NormalButton', + name: 'NormalButton', span_id: expect.stringMatching(/^[\da-f]{16}$/), trace_id: traceId, parent_span_id: expect.stringMatching(/^[\da-f]{16}$/), diff --git a/dev-packages/browser-integration-tests/suites/tracing/metrics/web-vitals-inp-navigate/init.js b/dev-packages/browser-integration-tests/suites/tracing/metrics/web-vitals-inp-navigate/init.js index 822c40214adb..cf36be344e01 100644 --- a/dev-packages/browser-integration-tests/suites/tracing/metrics/web-vitals-inp-navigate/init.js +++ b/dev-packages/browser-integration-tests/suites/tracing/metrics/web-vitals-inp-navigate/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/metrics/web-vitals-inp-navigate/test.ts b/dev-packages/browser-integration-tests/suites/tracing/metrics/web-vitals-inp-navigate/test.ts index c7a29f406c1e..57174bc4d23f 100644 --- a/dev-packages/browser-integration-tests/suites/tracing/metrics/web-vitals-inp-navigate/test.ts +++ b/dev-packages/browser-integration-tests/suites/tracing/metrics/web-vitals-inp-navigate/test.ts @@ -54,7 +54,7 @@ sentryTest( expect(itemHeader).toEqual({ type: 'span', - item_count: 1, + item_count: getSpansFromEnvelope(spanEnvelope).length, content_type: 'application/vnd.sentry.items.span.v2+json', }); @@ -64,7 +64,7 @@ sentryTest( const pageloadSpanId = inpSpan.parent_span_id; expect(inpSpan).toEqual({ - name: 'body > nav#navigation > NavigationLink', + name: 'NavigationLink', span_id: expect.stringMatching(/^[\da-f]{16}$/), trace_id: traceId, parent_span_id: expect.stringMatching(/^[\da-f]{16}$/), @@ -144,7 +144,7 @@ sentryTest( expect(itemHeader).toEqual({ type: 'span', - item_count: 1, + item_count: getSpansFromEnvelope(spanEnvelope).length, content_type: 'application/vnd.sentry.items.span.v2+json', }); @@ -154,7 +154,7 @@ sentryTest( const pageloadSpanId = inpSpan.parent_span_id; expect(inpSpan).toEqual({ - name: 'body > nav#navigation > NavigationLink', + name: 'Click', span_id: expect.stringMatching(/^[\da-f]{16}$/), trace_id: expect.stringMatching(/^[\da-f]{32}$/), parent_span_id: expect.stringMatching(/^[\da-f]{16}$/), diff --git a/dev-packages/browser-integration-tests/suites/tracing/metrics/web-vitals-inp-parametrized-late/init.js b/dev-packages/browser-integration-tests/suites/tracing/metrics/web-vitals-inp-parametrized-late/init.js index 4745a0f370de..8234edd4039b 100644 --- a/dev-packages/browser-integration-tests/suites/tracing/metrics/web-vitals-inp-parametrized-late/init.js +++ b/dev-packages/browser-integration-tests/suites/tracing/metrics/web-vitals-inp-parametrized-late/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/metrics/web-vitals-inp-parametrized-late/test.ts b/dev-packages/browser-integration-tests/suites/tracing/metrics/web-vitals-inp-parametrized-late/test.ts index 197c89ec78fb..2d951480c402 100644 --- a/dev-packages/browser-integration-tests/suites/tracing/metrics/web-vitals-inp-parametrized-late/test.ts +++ b/dev-packages/browser-integration-tests/suites/tracing/metrics/web-vitals-inp-parametrized-late/test.ts @@ -55,7 +55,7 @@ sentryTest( expect(itemHeader).toEqual({ type: 'span', - item_count: 1, + item_count: getSpansFromEnvelope(spanEnvelope).length, content_type: 'application/vnd.sentry.items.span.v2+json', }); @@ -65,7 +65,7 @@ sentryTest( const pageloadSpanId = inpSpan.parent_span_id; expect(inpSpan).toEqual({ - name: 'body > NormalButton', + name: 'NormalButton', span_id: expect.stringMatching(/^[\da-f]{16}$/), trace_id: traceId, parent_span_id: expect.stringMatching(/^[\da-f]{16}$/), diff --git a/dev-packages/browser-integration-tests/suites/tracing/metrics/web-vitals-inp-parametrized/init.js b/dev-packages/browser-integration-tests/suites/tracing/metrics/web-vitals-inp-parametrized/init.js index 3adb365703b9..1b55260e60d3 100644 --- a/dev-packages/browser-integration-tests/suites/tracing/metrics/web-vitals-inp-parametrized/init.js +++ b/dev-packages/browser-integration-tests/suites/tracing/metrics/web-vitals-inp-parametrized/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/metrics/web-vitals-inp-parametrized/test.ts b/dev-packages/browser-integration-tests/suites/tracing/metrics/web-vitals-inp-parametrized/test.ts index 83b3695628f3..bd3ceb1c19c4 100644 --- a/dev-packages/browser-integration-tests/suites/tracing/metrics/web-vitals-inp-parametrized/test.ts +++ b/dev-packages/browser-integration-tests/suites/tracing/metrics/web-vitals-inp-parametrized/test.ts @@ -55,7 +55,7 @@ sentryTest( expect(itemHeader).toEqual({ type: 'span', - item_count: 1, + item_count: getSpansFromEnvelope(spanEnvelope).length, content_type: 'application/vnd.sentry.items.span.v2+json', }); @@ -65,7 +65,7 @@ sentryTest( const pageloadSpanId = inpSpan.parent_span_id; expect(inpSpan).toEqual({ - name: 'body > NormalButton', + name: 'NormalButton', span_id: expect.stringMatching(/^[\da-f]{16}$/), trace_id: traceId, parent_span_id: expect.stringMatching(/^[\da-f]{16}$/), diff --git a/dev-packages/browser-integration-tests/suites/tracing/metrics/web-vitals-inp-streamed-spans/init.js b/dev-packages/browser-integration-tests/suites/tracing/metrics/web-vitals-inp-streamed-spans/init.js deleted file mode 100644 index 8be8d8420f85..000000000000 --- a/dev-packages/browser-integration-tests/suites/tracing/metrics/web-vitals-inp-streamed-spans/init.js +++ /dev/null @@ -1,15 +0,0 @@ -import * as Sentry from '@sentry/browser'; - -window.Sentry = Sentry; -window._testBaseTimestamp = performance.timeOrigin / 1000; - -Sentry.init({ - dsn: 'https://public@dsn.ingest.sentry.io/1337', - integrations: [ - Sentry.browserTracingIntegration({ - idleTimeout: 5000, - }), - Sentry.spanStreamingIntegration(), - ], - tracesSampleRate: 1, -}); diff --git a/dev-packages/browser-integration-tests/suites/tracing/metrics/web-vitals-inp-streamed-spans/subject.js b/dev-packages/browser-integration-tests/suites/tracing/metrics/web-vitals-inp-streamed-spans/subject.js deleted file mode 100644 index f4ea1cd46d67..000000000000 --- a/dev-packages/browser-integration-tests/suites/tracing/metrics/web-vitals-inp-streamed-spans/subject.js +++ /dev/null @@ -1,19 +0,0 @@ -const blockUI = - (delay = 70) => - e => { - const startTime = Date.now(); - - function getElapsed() { - const time = Date.now(); - return time - startTime; - } - - while (getElapsed() < delay) { - // - } - - e.target.classList.add('clicked'); - }; - -document.querySelector('[data-test-id=slow-button]').addEventListener('click', blockUI(450)); -document.querySelector('[data-test-id=normal-button]').addEventListener('click', blockUI()); diff --git a/dev-packages/browser-integration-tests/suites/tracing/metrics/web-vitals-inp-streamed-spans/template.html b/dev-packages/browser-integration-tests/suites/tracing/metrics/web-vitals-inp-streamed-spans/template.html deleted file mode 100644 index d5f28c7c8847..000000000000 --- a/dev-packages/browser-integration-tests/suites/tracing/metrics/web-vitals-inp-streamed-spans/template.html +++ /dev/null @@ -1,10 +0,0 @@ - - - - - - - - - - diff --git a/dev-packages/browser-integration-tests/suites/tracing/metrics/web-vitals-inp-streamed-spans/test.ts b/dev-packages/browser-integration-tests/suites/tracing/metrics/web-vitals-inp-streamed-spans/test.ts deleted file mode 100644 index f1ba7c9138f7..000000000000 --- a/dev-packages/browser-integration-tests/suites/tracing/metrics/web-vitals-inp-streamed-spans/test.ts +++ /dev/null @@ -1,87 +0,0 @@ -import { expect } from '@playwright/test'; -import { sentryTest } from '../../../../utils/fixtures'; -import { hidePage, shouldSkipTracingTest } from '../../../../utils/helpers'; -import { getSpanOp, waitForStreamedSpan } from '../../../../utils/spanUtils'; - -sentryTest.beforeEach(async ({ browserName }) => { - if (shouldSkipTracingTest() || browserName !== 'chromium') { - sentryTest.skip(); - } -}); - -sentryTest('captures INP click as a streamed span', async ({ getLocalTestUrl, page }) => { - const url = await getLocalTestUrl({ testDir: __dirname }); - - const pageloadSpanPromise = waitForStreamedSpan(page, span => getSpanOp(span) === 'pageload'); - const inpSpanPromise = waitForStreamedSpan(page, span => getSpanOp(span) === 'ui.interaction.click'); - - await page.goto(url); - - await page.locator('[data-test-id=normal-button]').click(); - await page.locator('.clicked[data-test-id=normal-button]').isVisible(); - - await page.waitForTimeout(500); - - await hidePage(page); - - const inpSpan = await inpSpanPromise; - const pageloadSpan = await pageloadSpanPromise; - - expect(inpSpan.attributes['sentry.op']).toEqual({ type: 'string', value: 'ui.interaction.click' }); - expect(inpSpan.attributes['sentry.origin']).toEqual({ type: 'string', value: 'auto.http.browser.inp' }); - expect(inpSpan.attributes['user_agent.original']?.value).toEqual(expect.stringContaining('Chrome')); - - // Check the INP span carries the transaction/segment name it belongs to - expect(inpSpan.attributes['sentry.transaction']).toEqual({ type: 'string', value: 'Pageload' }); - expect(inpSpan.attributes['sentry.segment.name']).toEqual({ type: 'string', value: 'Pageload' }); - - const inpValue = inpSpan.attributes['browser.web_vital.inp.value']?.value as number; - expect(inpValue).toBeGreaterThan(0); - - expect(inpSpan.attributes['sentry.exclusive_time']?.value).toBeGreaterThan(0); - - expect(inpSpan.name).toBe('NormalButton'); - expect(inpSpan.attributes['ui.component_name']).toEqual({ type: 'string', value: 'NormalButton' }); - expect(inpSpan.attributes['browser.web_vital.inp.target']).toEqual({ type: 'string', value: 'body > NormalButton' }); - - expect(inpSpan.end_timestamp).toBeGreaterThan(inpSpan.start_timestamp); - - expect(inpSpan.span_id).toMatch(/^[\da-f]{16}$/); - expect(inpSpan.trace_id).toMatch(/^[\da-f]{32}$/); - - expect(inpSpan.parent_span_id).toBe(pageloadSpan.span_id); - expect(inpSpan.trace_id).toBe(pageloadSpan.trace_id); -}); - -sentryTest('captures the slowest interaction as streamed INP span', async ({ getLocalTestUrl, page }) => { - const url = await getLocalTestUrl({ testDir: __dirname }); - - await page.goto(url); - - await page.locator('[data-test-id=normal-button]').click(); - await page.locator('.clicked[data-test-id=normal-button]').isVisible(); - - await page.waitForTimeout(500); - - const inpSpanPromise = waitForStreamedSpan(page, span => { - const op = getSpanOp(span); - return op === 'ui.interaction.click'; - }); - - await page.locator('[data-test-id=slow-button]').click(); - await page.locator('.clicked[data-test-id=slow-button]').isVisible(); - - await page.waitForTimeout(500); - - await hidePage(page); - - const inpSpan = await inpSpanPromise; - - expect(inpSpan.name).toBe('SlowButton'); - expect(inpSpan.attributes['ui.component_name']).toEqual({ type: 'string', value: 'SlowButton' }); - expect(inpSpan.attributes['browser.web_vital.inp.target']).toEqual({ type: 'string', value: 'body > SlowButton' }); - expect(inpSpan.attributes['sentry.exclusive_time']?.value).toBeGreaterThan(400); - - const inpValue = inpSpan.attributes['browser.web_vital.inp.value']?.value as number; - expect(inpValue).toBeGreaterThan(400); -}); diff --git a/dev-packages/browser-integration-tests/suites/tracing/metrics/web-vitals-inp/init.js b/dev-packages/browser-integration-tests/suites/tracing/metrics/web-vitals-inp/init.js index 44acb2f6c147..6ebfc6540022 100644 --- a/dev-packages/browser-integration-tests/suites/tracing/metrics/web-vitals-inp/init.js +++ b/dev-packages/browser-integration-tests/suites/tracing/metrics/web-vitals-inp/init.js @@ -1,29 +1,14 @@ import * as Sentry from '@sentry/browser'; window.Sentry = Sentry; +window._testBaseTimestamp = performance.timeOrigin / 1000; Sentry.init({ - traceLifecycle: 'static', dsn: 'https://public@dsn.ingest.sentry.io/1337', integrations: [ Sentry.browserTracingIntegration({ - idleTimeout: 4000, - enableLongTask: false, - enableInp: true, - instrumentPageLoad: false, - instrumentNavigation: false, + idleTimeout: 5000, }), ], tracesSampleRate: 1, - debug: true, -}); - -const client = Sentry.getClient(); - -// Force page load transaction name to a testable value -Sentry.startBrowserTracingPageLoadSpan(client, { - name: 'test-url', - attributes: { - ['sentry.segment.name.source']: 'url', - }, }); diff --git a/dev-packages/browser-integration-tests/suites/tracing/metrics/web-vitals-inp/subject.js b/dev-packages/browser-integration-tests/suites/tracing/metrics/web-vitals-inp/subject.js index 64524952dfa7..f4ea1cd46d67 100644 --- a/dev-packages/browser-integration-tests/suites/tracing/metrics/web-vitals-inp/subject.js +++ b/dev-packages/browser-integration-tests/suites/tracing/metrics/web-vitals-inp/subject.js @@ -3,18 +3,17 @@ const blockUI = e => { const startTime = Date.now(); - function getElasped() { + function getElapsed() { const time = Date.now(); return time - startTime; } - while (getElasped() < delay) { + while (getElapsed() < delay) { // } e.target.classList.add('clicked'); }; -document.querySelector('[data-test-id=not-so-slow-button]').addEventListener('click', blockUI(300)); document.querySelector('[data-test-id=slow-button]').addEventListener('click', blockUI(450)); document.querySelector('[data-test-id=normal-button]').addEventListener('click', blockUI()); diff --git a/dev-packages/browser-integration-tests/suites/tracing/metrics/web-vitals-inp/template.html b/dev-packages/browser-integration-tests/suites/tracing/metrics/web-vitals-inp/template.html index 437426e9ab01..d5f28c7c8847 100644 --- a/dev-packages/browser-integration-tests/suites/tracing/metrics/web-vitals-inp/template.html +++ b/dev-packages/browser-integration-tests/suites/tracing/metrics/web-vitals-inp/template.html @@ -4,9 +4,7 @@ -
Rendered Before Long Task
- diff --git a/dev-packages/browser-integration-tests/suites/tracing/metrics/web-vitals-inp/test.ts b/dev-packages/browser-integration-tests/suites/tracing/metrics/web-vitals-inp/test.ts index f47e7dcd6f09..f1ba7c9138f7 100644 --- a/dev-packages/browser-integration-tests/suites/tracing/metrics/web-vitals-inp/test.ts +++ b/dev-packages/browser-integration-tests/suites/tracing/metrics/web-vitals-inp/test.ts @@ -1,172 +1,87 @@ import { expect } from '@playwright/test'; -import { SDK_VERSION } from '@sentry/core'; import { sentryTest } from '../../../../utils/fixtures'; import { hidePage, shouldSkipTracingTest } from '../../../../utils/helpers'; -import { getSpanOp, getSpansFromEnvelope, waitForStreamedSpanEnvelope } from '../../../../utils/spanUtils'; - -// This app does not enable span streaming (no `traceLifecycle: 'stream'`). INP is still emitted as a -// streamed span, because INP overrides the static trace lifecycle for itself (it would otherwise be -// dropped as a late child of the already-ended pageload span). - -sentryTest( - 'captures an INP click as a streamed span during pageload', - async ({ browserName, getLocalTestUrl, page }) => { - const supportedBrowsers = ['chromium']; - - if (shouldSkipTracingTest() || !supportedBrowsers.includes(browserName)) { - sentryTest.skip(); - } - - const url = await getLocalTestUrl({ testDir: __dirname }); - - const spanEnvelopePromise = waitForStreamedSpanEnvelope( - page, - env => !!getSpansFromEnvelope(env).find(s => getSpanOp(s) === 'ui.interaction.click'), - ); - - await page.goto(url); - - await page.locator('[data-test-id=normal-button]').click(); - await page.locator('.clicked[data-test-id=normal-button]').isVisible(); - - await page.waitForTimeout(500); - - // Page hide to trigger INP - await hidePage(page); - - const spanEnvelope = await spanEnvelopePromise; - const envelopeHeader = spanEnvelope[0]; - const itemHeader = spanEnvelope[1][0][0]; - const inpSpan = getSpansFromEnvelope(spanEnvelope).find(s => getSpanOp(s) === 'ui.interaction.click')!; - - const traceId = envelopeHeader.trace!.trace_id; - expect(traceId).toMatch(/^[\da-f]{32}$/); - - expect(envelopeHeader).toEqual({ - sdk: { name: 'sentry.javascript.browser', version: SDK_VERSION }, - sent_at: expect.any(String), - trace: { - environment: 'production', - public_key: 'public', - sample_rand: expect.any(String), - sample_rate: '1', - sampled: 'true', - trace_id: traceId, - // no `transaction`, because the span source is the URL - }, - }); - - expect(itemHeader).toEqual({ - type: 'span', - item_count: 1, - content_type: 'application/vnd.sentry.items.span.v2+json', - }); - - const inpValue = inpSpan.attributes['browser.web_vital.inp.value']?.value as number; - expect(inpValue).toBeGreaterThan(0); - - const pageloadSpanId = inpSpan.parent_span_id; - - expect(inpSpan).toEqual({ - name: 'body > NormalButton', - span_id: expect.stringMatching(/^[\da-f]{16}$/), - trace_id: traceId, - parent_span_id: expect.stringMatching(/^[\da-f]{16}$/), - start_timestamp: expect.any(Number), - end_timestamp: expect.any(Number), - is_segment: false, - status: 'ok', - attributes: { - 'sentry.is_localhost': { value: false, type: 'boolean' }, - 'sentry.origin': { value: 'auto.http.browser.inp', type: 'string' }, - 'sentry.op': { value: 'ui.interaction.click', type: 'string' }, - 'ui.component_name': { value: 'NormalButton', type: 'string' }, - 'browser.web_vital.inp.target': { value: 'body > NormalButton', type: 'string' }, - 'sentry.exclusive_time': { value: inpValue, type: expect.stringMatching(/^(integer)|(double)$/) }, - 'browser.web_vital.inp.value': { value: inpValue, type: expect.stringMatching(/^(integer)|(double)$/) }, - 'browser.web_vital.inp.interaction_type': { value: 'click', type: 'string' }, - 'browser.navigation.type': { value: 'navigate', type: 'string' }, - 'sentry.transaction': { value: 'test-url', type: 'string' }, - 'sentry.segment.name': { value: 'test-url', type: 'string' }, - 'user_agent.original': { value: expect.stringContaining('Chrome'), type: 'string' }, - 'sentry.pageload.span_id': { value: pageloadSpanId, type: 'string' }, - 'sentry.trace_lifecycle': { value: 'stream', type: 'string' }, - 'sentry.segment.id': { value: pageloadSpanId, type: 'string' }, - 'sentry.sdk.name': { value: 'sentry.javascript.browser', type: 'string' }, - 'sentry.sdk.version': { value: SDK_VERSION, type: 'string' }, - 'sentry.environment': { value: 'production', type: 'string' }, - }, - }); - }, -); - -sentryTest( - 'chooses the slowest interaction click event when INP is triggered', - async ({ browserName, getLocalTestUrl, page }) => { - const supportedBrowsers = ['chromium']; - - if (shouldSkipTracingTest() || !supportedBrowsers.includes(browserName)) { - sentryTest.skip(); - } - - const url = await getLocalTestUrl({ testDir: __dirname }); - - await page.goto(url); - - await page.locator('[data-test-id=normal-button]').click(); - await page.locator('.clicked[data-test-id=normal-button]').isVisible(); - - await page.waitForTimeout(500); - - const spanEnvelopePromise = waitForStreamedSpanEnvelope( - page, - env => !!getSpansFromEnvelope(env).find(s => getSpanOp(s) === 'ui.interaction.click'), - ); - - await page.locator('[data-test-id=slow-button]').click(); - await page.locator('.clicked[data-test-id=slow-button]').isVisible(); - - await page.waitForTimeout(500); - - // Page hide to trigger INP reporting - await hidePage(page); - - const inpSpan = getSpansFromEnvelope(await spanEnvelopePromise).find(s => getSpanOp(s) === 'ui.interaction.click')!; - - const inpValue = inpSpan.attributes['browser.web_vital.inp.value']?.value as number; - expect(inpValue).toBeGreaterThan(400); - - const pageloadSpanId = inpSpan.parent_span_id; - - expect(inpSpan).toEqual({ - name: 'body > SlowButton', - span_id: expect.stringMatching(/^[\da-f]{16}$/), - trace_id: expect.stringMatching(/^[\da-f]{32}$/), - parent_span_id: expect.stringMatching(/^[\da-f]{16}$/), - start_timestamp: expect.any(Number), - end_timestamp: expect.any(Number), - is_segment: false, - status: 'ok', - attributes: { - 'sentry.is_localhost': { value: false, type: 'boolean' }, - 'sentry.origin': { value: 'auto.http.browser.inp', type: 'string' }, - 'sentry.op': { value: 'ui.interaction.click', type: 'string' }, - 'ui.component_name': { value: 'SlowButton', type: 'string' }, - 'browser.web_vital.inp.target': { value: 'body > SlowButton', type: 'string' }, - 'sentry.exclusive_time': { value: inpValue, type: expect.stringMatching(/^(integer)|(double)$/) }, - 'browser.web_vital.inp.value': { value: inpValue, type: expect.stringMatching(/^(integer)|(double)$/) }, - 'browser.web_vital.inp.interaction_type': { value: 'click', type: 'string' }, - 'browser.navigation.type': { value: 'navigate', type: 'string' }, - 'sentry.transaction': { value: 'test-url', type: 'string' }, - 'sentry.segment.name': { value: 'test-url', type: 'string' }, - 'user_agent.original': { value: expect.stringContaining('Chrome'), type: 'string' }, - 'sentry.pageload.span_id': { value: pageloadSpanId, type: 'string' }, - 'sentry.trace_lifecycle': { value: 'stream', type: 'string' }, - 'sentry.segment.id': { value: pageloadSpanId, type: 'string' }, - 'sentry.sdk.name': { value: 'sentry.javascript.browser', type: 'string' }, - 'sentry.sdk.version': { value: SDK_VERSION, type: 'string' }, - 'sentry.environment': { value: 'production', type: 'string' }, - }, - }); - }, -); +import { getSpanOp, waitForStreamedSpan } from '../../../../utils/spanUtils'; + +sentryTest.beforeEach(async ({ browserName }) => { + if (shouldSkipTracingTest() || browserName !== 'chromium') { + sentryTest.skip(); + } +}); + +sentryTest('captures INP click as a streamed span', async ({ getLocalTestUrl, page }) => { + const url = await getLocalTestUrl({ testDir: __dirname }); + + const pageloadSpanPromise = waitForStreamedSpan(page, span => getSpanOp(span) === 'pageload'); + const inpSpanPromise = waitForStreamedSpan(page, span => getSpanOp(span) === 'ui.interaction.click'); + + await page.goto(url); + + await page.locator('[data-test-id=normal-button]').click(); + await page.locator('.clicked[data-test-id=normal-button]').isVisible(); + + await page.waitForTimeout(500); + + await hidePage(page); + + const inpSpan = await inpSpanPromise; + const pageloadSpan = await pageloadSpanPromise; + + expect(inpSpan.attributes['sentry.op']).toEqual({ type: 'string', value: 'ui.interaction.click' }); + expect(inpSpan.attributes['sentry.origin']).toEqual({ type: 'string', value: 'auto.http.browser.inp' }); + expect(inpSpan.attributes['user_agent.original']?.value).toEqual(expect.stringContaining('Chrome')); + + // Check the INP span carries the transaction/segment name it belongs to + expect(inpSpan.attributes['sentry.transaction']).toEqual({ type: 'string', value: 'Pageload' }); + expect(inpSpan.attributes['sentry.segment.name']).toEqual({ type: 'string', value: 'Pageload' }); + + const inpValue = inpSpan.attributes['browser.web_vital.inp.value']?.value as number; + expect(inpValue).toBeGreaterThan(0); + + expect(inpSpan.attributes['sentry.exclusive_time']?.value).toBeGreaterThan(0); + + expect(inpSpan.name).toBe('NormalButton'); + expect(inpSpan.attributes['ui.component_name']).toEqual({ type: 'string', value: 'NormalButton' }); + expect(inpSpan.attributes['browser.web_vital.inp.target']).toEqual({ type: 'string', value: 'body > NormalButton' }); + + expect(inpSpan.end_timestamp).toBeGreaterThan(inpSpan.start_timestamp); + + expect(inpSpan.span_id).toMatch(/^[\da-f]{16}$/); + expect(inpSpan.trace_id).toMatch(/^[\da-f]{32}$/); + + expect(inpSpan.parent_span_id).toBe(pageloadSpan.span_id); + expect(inpSpan.trace_id).toBe(pageloadSpan.trace_id); +}); + +sentryTest('captures the slowest interaction as streamed INP span', async ({ getLocalTestUrl, page }) => { + const url = await getLocalTestUrl({ testDir: __dirname }); + + await page.goto(url); + + await page.locator('[data-test-id=normal-button]').click(); + await page.locator('.clicked[data-test-id=normal-button]').isVisible(); + + await page.waitForTimeout(500); + + const inpSpanPromise = waitForStreamedSpan(page, span => { + const op = getSpanOp(span); + return op === 'ui.interaction.click'; + }); + + await page.locator('[data-test-id=slow-button]').click(); + await page.locator('.clicked[data-test-id=slow-button]').isVisible(); + + await page.waitForTimeout(500); + + await hidePage(page); + + const inpSpan = await inpSpanPromise; + + expect(inpSpan.name).toBe('SlowButton'); + expect(inpSpan.attributes['ui.component_name']).toEqual({ type: 'string', value: 'SlowButton' }); + expect(inpSpan.attributes['browser.web_vital.inp.target']).toEqual({ type: 'string', value: 'body > SlowButton' }); + expect(inpSpan.attributes['sentry.exclusive_time']?.value).toBeGreaterThan(400); + + const inpValue = inpSpan.attributes['browser.web_vital.inp.value']?.value as number; + expect(inpValue).toBeGreaterThan(400); +}); diff --git a/dev-packages/browser-integration-tests/suites/tracing/metrics/web-vitals-lcp-streamed-spans/assets/sentry-logo-600x179.png b/dev-packages/browser-integration-tests/suites/tracing/metrics/web-vitals-lcp-streamed-spans/assets/sentry-logo-600x179.png deleted file mode 100644 index 353b7233d6bfa4f026f9998cacfa4add4bba9274..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 16118 zcmeHu^+S})7xyl#gwjYzhlrF&cOzZW-Q5k+0s<-_QZ6msEZvPD-L-UgN%#AB@AcmA z-|)V#KOoD_GtbPKGc)IWKA-bZNkJ0*G0|fX2!t*zC9VttA!q`>SD+vPKh?a9D}n#O zF3OUkpprq7Z4ih8BrPtY>Z!k<;+?FcR@FVl+at$>|46kYBS2WzfXAwjotEiQCvxwN z*ZVTy(;HRsciU<{RKJwo3YUIbdh#fEBMa>kME){Q_YpkV)ix;xAF}p61LdWZjgI-( ziUSKpHf}qMuT?`l(C1@rf@${?i~8P&hde#E&gTm@ls{=8!2wuU2%^F$C}2t~I&e69 zA_VuJj}*VZ1F>lS!UDeg>jTh`!Wh9AMukMb{6Ei8Ajziw*FqRYTuvBGy;GPb`QM%? zAf1LiY=8OyUiLl=HLUFuVP5e6zKoFZ9n$}Fi^T$F(ZoiKbNqXBe?J{WGxXnpDIioY zD3BP8({U7jkN=5Eg~Km*M`? z2k>23+gFRf%<@mS*tl3hcq~e&w2=SC2p9+LzZd^sV;uD-sqa-;Pt2e5Kxz=48N4hp z(6`Nsdmupg(;EI6u(eYy`IF7l=E>5o7DbeJa}PlZ-L3fKAu!0Q8n5nOw;84aoGlSe z(9AqaI70`#97kDSZ&G8OeQ|*9n=>=o%tcy%=UB3PaBaM54l$KLL4 zAtzmPFy6nj7R3R)2WKgAz}wr~BuWVD0%U03`P~35RM>)`@pfY16SLEYKaTz05isY6 z)fyEW2BS1Y6oUxvy~yR%eI))ZGUf~0n49RMf9EX>0xX0eOk60f74dRD@yd68J zMy{IQCq*xb@e;2@FC~i3@O++)+WK#D$JD^C5uLOeyT=DNhLSpW<_j~_?SayJsrRZu zScuehqdFz=)ay9^jvgNNdwUNEeq-f^&l7kr66aujn7~_*?J;rr%nnWyu_^}yZ z#iu0I-F^CR8(Rwg-u$ntzs{(<-$Jr~38^hy;*pHVQKVrFrCMp4(*zw&SzqMj-`&+X zOj=Sa|GRP1KmImeaD(Obb_1rn!xbW^@zXPFn9P0u{jbLp=Hm=@a4wuyWpjSvblE5R z_o5~GuK!un1vr%+_i)Aa>%SK#-v|*2X2zFM(7Vfc2lYi_=n?}}60;pVQEPE_e7J5F zVm+h!@AOKP0?s^?+A8BgOv~WQS^J6J@dmC_JL~ZmW^i(`fIgz_kcTuBd#3mlm0GWn z|Bj-8-%A0Rt+ZN{200jo3Ud!I@f0;_3kX=a{y^fSKk0gfAAv-ouemwbK^BGbpNstV zPdu#MK+_HJv?EEPfhQ%KjKjlXUhd}+ro*M|@FEWZ+V9^9^8a0iVaQ*elEQoTP1;+2 z;Y0)kkD5ttz1XbV)hIIAF^JX5NkIf9%|?a|GQ^(@yqf4Bjr`YT_Pzo}eN?+o$L0b? z(C@YXbV^zi>(8YlB&x~KlF8b5IB45aM(pUHuktgChV{={$Ws6}1Fki1-I|;DeHweB z-6xaV^cxv2!R!%pJ}Y{w7IM^*jcVF-2fU_R@4ElB$_TJqvU|o@?cz9^5@yk`1vOvt z7;Nlx4}Ujr5{`$KzM(zm?>?VG^mnuhXkojn&qOL z*I``8T-|v?5Gkyvc-XfZ&Tw<=%Dy%LoPI=kJavzc|TLsFZn5#L0k66DeHIc3h%zMRyc^H^AQgafUfn1 zpazOu?^EB8x5rU%E&H*gScpK@VP z>Z&vHq%G~}d>_-5z8(WSlu_ZnCvTGq>9h1WYlQ{ku$o-m*ZCDM3~#zyB;@y__l!$`&eP&Iq{mxV8rBuI3w z9(MRh!1f$r&Ey;1^=5Elkl_oDS5mKYmE2`??>u*urEeh5Ya?c9!DMQu&K^!@Z+AKk zSu6*Wrfv#KfEM-BL6qcQu}1uUIO|)m@iVx>`}Jjn31FSGx|0_3mLqRDs)J$g4Zb!nFTm*#)#S1bNqj7H;?>@B|&q)U|%$1*Dhd9qYH-=5;(SBz`)EFd_=QFS*MPNQuDb1+LNlo9l5GwP$?n%7m$$yo-8G=&+QfV%aA-J3^7cH!YF8eV-(>8m(F zKb}jve$^R#2*|gumjkSG+{v!VtU&%Sb>?i_k#!!f&h=x$%GLaeV@_^u$g)cY?FJ(f zYcQ?JG}`ELPuVfpQLb!l4m+zf4<6QLEj-G~#sL1Jw-SP)_$uCIF=_`X0z&C{TODT2m1J|4Kab*JIP zcN?0DTm>rHeA_IB->wQy)i zy1J5Ix>52V-nj28SCz#|mJmr}iS@9!-Rsn)@DWzKs?g6VVToa^Kb-v@(YjZ#C(=@; z6}W3)92_3rSX6X%L@*%Ppea`kxbCb?`ooN_yY||z*LNzrYIW7yo55HL>+bk(M^)@S zNQ=h?QVlH16%pEFgpUwxXSr$|R9dk}593dSMm{n*z+5fStr8kiD^wfQI?>nA@rk#B z{ew6HJ41>IYq+iSto>&XkOQX5q;UAzu+rG=_Tjr}Cfn5bXW!1l2?L0jcd3GLKhtcw z$Jh2yv1pors&UTO100(%{2`YZeIt9^!L|7NizDeStN{-AhAb|w8`hkNP0VCzTWyz}lRDv0eQaWd)Z z=eAK=1r!TB*+GtLrc4{kcd%(jJ|;|jZ{tkVj}!)se4MmDeO!)NkO+m=EkoHMQ}t${ z{==V1Ys-`@k}weFRF9I)A~GYRMA0T+CU4RvC~)N!OnM{gRu=4N5L}cV-k1b*!R1GL z_TGtiGXEG`F)4)O+KL2$WP^qf+e7ERy4VdbYv(k5!BP;;&<7>=Mg>KwH*mcOWYjA| z#~;PEr(0A&eRkz3@zM<^Vsf`tSQ}Ol%M2kBheTxf1XWQ)^RNa|?mumpTonghQ_xn+ z{Oljuw!S62`=M7OnmzUGY*cz|$x9*2pFwRWP19{upCw(Ot#lB99fLOg!<>MG$p9+$ zz9?P7CanDH%pn+iHXLh-_>;c9?n#Ii>J5qTCB20p=*X38>D0S%u0qat%q&`vU7mcO z+szO)^i@D3oDvJ$ESU9^GyqFWS=$A0ay6SCytZ zYK#1p6=4Q^u(MD14>aa9d*}5So zcy`QrkGSI#NxrOS-JKKhR)qrD-OgP(75u#)3F0IGFM3WOFXVgiURsA)JOfh_Zj8{nqAWW^kw~72g99i`4>oq7(1tPd+d4b8Yo-ezId2EK}ZH{vMbv4Y}Sw_ffS@_L_UTbKwCJU zgz>oCBUj7C(hZ1Q{E(?&k<`>z4h*px>_x}~>&ePFcPx-vxgVW`WF(x-F@!}&9?0M* z22Y-Q^W`Hw1U~s7!lL?0zBa>rR1$OZ3lh*Z2dIBpP*5CJtLbjPu{DNzBZMNdujc(s zG74hjjAzCg#mH~+q0m{id~)o>6+p66IbURgrmZ-49}aN9GaG`%OBh@ z_<~r23}l~2x3s}IKW~N;4(L>IK1_Sc)Ayf!R{6&vG$|c7G?t^LFVGss6I?mYiX&$o}5#;~rg{PVeS1;_mC+XUyf8zj_hJ1CFSb(J-Ov;VOkh(1egEn*Ttfl@H!+L4P7ddg>nAa zQ#@f(lTH07^v)LpHs?Tr55sYL)Hf`FybR|Je3v3xC3zj|X)03q;8ve2P(%Jat7?IxEkVJC-?@ zSp$OgP0>}#B~|srfEqJ4pCis~R$H;U?Xbk0CpLDg4+glm(X@l7Twb78j68rpv^X{; z!x`mBn(K+qi5Y{d?0cZO#6pjox|GM=UP3DYHl$6^~EQIR??D9 zhzv`t%rv%fF3HSf}9wTO2Z$C~xyxL44aHo9|)b^XUWNbDM{ zr(1$O+T;)T?nSEPbS4b>l6{$zYQq^FVACGoOJF98hR2KNPj!d1uwvuQ%ymI>PH^g0 z#79HWo{`A$)93i)PsM_61g>>8lFb(zUOW)Y`)Ow{nxV(X{q(_K?K(4x;fQo!bxqim z=t3!HDO>0;fzEV;ErkvcC7O-!rkFPm7gvxDKzi^>X6k?_me6aRDVIn?>sjR&NH%-3 z<(X{-G$y*aDx#l3QS@Fs2hlF)Y(RzxZ19{*J&^pS`lZ~ZfBP!L^ zcoMAqu&49DR&11yW8F8P29+*xw_Dlw)QQmg6;?H5wGq{o27;RRJTzXdJnbH@)7m!W z6oV|Her5X{u%LDNYsXGzOO}@JU2(-QjixAO{TndggWSTmWfB+f&$%r-9ST&@@D~l~ zvi$}WFOjhaBFeF9i9!jBECumNZ;<*b)!v&cKwovACEF5e)3M#qLF$8vJ1G1o3w}M0 zr<0{+&BdiU9A|190y43IksqY?V{Q!vZOYy;5*u(&{P%)*ZszduJsLW8pR*_G49iLN zyMmY9CFkmS0wUj;`zj~k+%1(N$oI-9=+b6{deeM$^>ocz%=q`l6q8N0(4@q(t_}?# zTx8RfDQ?)q07X9-Vh8>3@7kr~n>MstTPvVyE&Qy1um`JD&e0PE#o&g1Ibe$ag|0|p z9ltq4$VShWsbJj;Rn#1KWdS3y&GQ}>=Ji*j^ViXYb?g&7XR}IEA}ur{KP##772N3d z%9!3{^H(rWU0yw8-#D!)dEYa3aChf1f&4C^zQ}aLonm}m{ECZ;J3;5BP%q$9+bEZ2 z!5ey`#XuBVmqQa(|E-W4+@K#c1_o0fkqYC{PNNtRP{^HR2k{u~NZ(qkyT9s^qCNWw zE!p&`D5>MHQ==_QVB>u5rSU!a;g#g9eWG5k;aR`#b)x2+f9*(ligv>}cyRwB89AfY zh*ECyu0f4KeVT+?tfleF?K~t$E=&Vow(cbJn?LQ3;o=?+XZtLL=h!|?D&g^jMTST= zBd22?<|lQNwu*%_QVShyBOE{DJ+yS6i-zlRD0t4PA!aZlKL z=adShn4|q^Hzy7-R&U<#^3XbU^T*pC%#EFIl{BX-*fa)`_8sLm?5gL{?>3B^L95|w zLPDKc4@i)yYnJ+|8EIOv@aQx}Nd@eKU>=U{)Cj@)2*08T%i0fUQg~B5^eswxvS{ir zeV929l!AWT+r#ZvvwA#)+yxl$o`mMb!N?)ORwVnHZ@$?GDU6d9PqD7pQe|kpJv1^s zC5Dh#e9sL`HgRwxHfeHcQ;D2Vw7l$eOyLLPJWnJ(>Q;6Dqj)$Xr8CoYIh>~A_L1{N!1!9i;fkgDe>-x^;<;*=iO;@ zAV8r5>tP*3IKizBVM`h;9S>&(qmNU3ov<-Kq+ut94Soj4QjrU{1{C(bQ+NJJ+L0S0 zt1r1$=q!cK9NDHU!pP==2imcHe*b-!=V8JTMd5Y-^1#cUDlXhe`^tUn|lB2lTh%s0j;-Fahi~7gGbuT;zM!U>pOSjS&%=! zTkaJ*Xzq=0OMvxR`}YsH3x;O^dZS}&_zCWPniyu^g;_WsH`cx} z@OJ=tFwC!4YsAo1ti0Xj{{i|r4(5OujC;v)WKxoFgJ2-!6q}gD*P-BpNOfEE$@65# zjpuINc3@#fHaO8Ki3#(%TcN}7y#l$IldnHIMdMe23#=PBkfbL$>lx)Xww252tbgK7 z;j=-wRvqc{4=J3Q$nM|M!IIji-Ovw3SeXsj8cRkBzPvZ4yKsxFp639D3v4|A)a=Lz}{fpcGF4EkNa+_NCC2izjp* z3f7@Z8b-4D8*MDe${47v##ur&k~PZ0IZwb|=1&3Rw03_^J$aYH$!Ed1uDA$Dbz*PFi7SBB7+*eIR{8)%-FVuouoTf-Gv7w>AatacNDSIR9HzjMlA`daug+Y4-R05F z(L;+zn&%yL_X>(jZ8F6-Iztgw)!}kYP4geZ`W>y8mYq?PbgPaEf>Ehtd(k1A^=6v| zj+xe_lem+H+qQ;i-m)mm-;8_S-sB_bRdLzbfS1;Z!0Pp>E_oq-wD|`tE(o`JBYjrS z+Fq~PY*wGde8$}(CfGJTjkFeoI7*MWXx=>AIio{j@Cs79F)JnL1dLFtbfxi+c36<7H?#JN>lhpa4o30RZR@*Zmzw>K*$BPE0r@Ir-i;xa zNh62Su@^0L5}JDQ`nsdb3BI%~7mv8-m8iGWH&2tni<{w%1TBo7zQ^TX?LMkar^Ns= zpKe2R-+0^^5AbN!oCOF>mgnlQvuvwio@)itT=g2HoMREsufTz_>6Nd7Kp?WL-&z20 zv>T4xg|LX~svLBN8Y}OeLrw;ffjav4tHO3QG_Q0~1D;Ipe{4R8KRLA6mBike zq=SI=eW{JQrasz*js`Uzm2-hrqaLwGBBdiHvPW5<{~{=+FI8Hnt$c+Zlzo2vp~N(u zqn39iJvTftF2|_o`wm$tazK*akX3Ds;)3Culjm;N#EgGu$&n!vwJ+6KT^b}(OQKUg zEbQReuMOANK?a zQbfJa81x|tduikGFai85wD_Q$JGjUaYd~dq?j`PD5f(;DmMz3nDKnY4o_1eiu0+Tc zT`rZtwN$yc-w*x$;L3s@0&nKlddt8>R8B?z-YTe)oC^t|xoK&A+_^xP@^7>Xui^d) z8ZR-5zHZ4Rx^#fUORu^`$62RELvVZ2{MQo(`#JZChzsH(BMczAU<7ZEpZFNR)`!-Z z%cs$2SB_;zOE)Z@C0`ZSlBd9s3)SUUYl@dT4q}$v5wyt`o4j-wILj)!wQ#j_iq9)a zM`}q0cf9HliFbK*Sd7r(4|K>=e8mBfn1yP?&B5(k(XN=kYea1U-_N`C8%4rB-k-ip z-u_dho3I7eiYAb2g~Uy_?vT~w0#$^O9Enx2@m(l*Y5%$+L#-r1iCe2|BeM~yG`4E~QwCG4>_k8#z(tJ50;p#`56+*@_b@Sle zu(V=neZg50kMIyHaACt6#i>8#3mxc>^s z)^UnTZBA&~BN?B?D44g1if?k(i#5n7{6IB=DWl$qg*lpKCh^`r1Ussit?715VD4I5 zk*IG~KdC^S2Z&Ko%C`M6uGgfe)6F~7!tf9#mP}T=_4;PoT9c=wX~MkFx;PZYm6|1j z4g4Su&WXH5!~9fho}h8RqA91tO-p3+PD7{dHx{sQ!A`=fxYq(VNI3yZ&i3I7x5xAV z>(wxOjI=wvR^G&*`qrT|=Qa7zkKwE3CM_jjqeK{z%viux%hk8P`Rp62G0Ct-OZXF?!pwDy}aas5|@~Mr; zAc*4>(OhAY)ra2kKvDjLA3R80r=bd5hE2T6DO{RwpB=ceCQZ>*y!k76W|m-E-H^l% z;%GyQkAtObS=y20pm;Bbf7^OS62XQ2a4^Gar`8zLMB6R7sH>;_ z7K69Dqkk_IQMh2~g~tN{)<}jiPibUUA0Eql_~;}e=txW`X8o z)J+5|w@uIasjkZxf(UJe;^TDPK}sJCjFOkMYxWu?9c*U{i-UbJD16kYyWCNAIN%>+ zMOy#i?XrX?dTqFATx*f~D7;n+obT-w7Wa#+&d7?QZC!0@DO=?q4QX+j4&$7^4IVy{ zG-kM{b8V+4XbXDCv6h9gq|0G&K8m0Z|T+~ZzhaB zTkWat@5clm32~6!Mg$%AikOhLln`{Y?lXg0CBOMLVVyAC6zMKrVCMMIYB@MK>>VQ; zV}h1cBM2?(Q%~g4o1~zSBA%I=_lk{yE8=uD$p(7Nr{nqEa&VhpWI8rKnNK6MSYaj3o=kO`j(4ml0~#eB!f7#X9e) z9z1SZFgst!qpJ|J^n21e&wjTk_I|Sav-unm-K)q8350uNW?#EVxdwZ5j2mKmcG`5({q*;|`{Y=SYQBNU~?CGNy{6&+n5^gS&e|qbNi> z#`?>}H6LO|t%Ep(>RjtZ)XZRYG`(;^HvpDLHHKNAod_a<9>%sX6@nffaB)ZsUh);( z8>BOsSwAD6kX*WxiHG_I>u`Vj8v_uIDd3QS&LCn-rHq;ymKLy-VRSSms*qS+aZ&D6f5H6n}?2T>Re`>RK00LcD%*+KPHlG<5 z*r#!ehlpl?NPuSKzU8kmh2)kwDjeD{`0a^8&|(Z-RM5i30LEEuFke^$;P54dbIXWv zo0xwJz8*+?A8pzvPqRNn+16d>GA51J8o{%`YsTp756jRIuG3mA!oyf-|2V{y-pir+ zmK1F{ya}4<4qt0tr**N?Y|d_wpn7P&BVn*6Ssbnh&At0<=kzx~46)HreUrsA*D z9P|~EYbmx)O08crW1rDLet8aNjjvx{7qXww47r);N$b2bY30xia1GY!t??M(>_WG; z2%2DX*|sr0-q8k4C$Zqgi!T^XbdT@Qv@efL;0zaxqd&COmkDD!{3xm0|5h)uU})!$ zOdH-tlp2398^7-~-0^z4NqFiMtve6e=(i9mJ^@9o9$8@B-vtLWyuHVta`BomAriBs zu_H!tzU97@oa);jnyQH!WvviGPOrK57C(IiaB%Be@p?opXf=IMRfkpp70>D5$bTz{ z#p{2>1BexN(1Nlhcoubi!ogjNJQr0tS0%eVhoi$$pHbFi~i};gj<`iG+weL@HZGD;8{bij=eKYzl>3g91UGM!{Mz`&pAHo5O1pJ#g z=bdn2&F-JK*01$@Akt;n?%MfUO{HC$x}-avTA7-`o@QmSd@a!mcYt%9vucVh>V51F zMLm8zcxRJ^9W=p%WHYCrde-i_9M#F-CjDZ{PiB#!6DIvtI8Nob-G7D$k2IMSzG`w%r&8A2OV{AKuxR zB6g!PhVk|p=KIF$xceKSgZmBr94hs8kXSSu!rGL_=HeOI?wLTtLxIGOxYxr3rtQrn zSeg#lG6@r>l?LlAUM}5`tIe)t^e_c#*@O~1*!|GRr7FAT*y$U+ZW1sk~U;>;e!HRX93_cQr(n1PY|xvs5@ z8Tq`Qa?c)~)Txm=`QKb|StYtS7y}0~c!T%DllTV==R8Pz=F$V>@+sCF5{l5wr(?dL zgGCi1f5t;D1*@MW($xZx6s+N2yDH5y!_7!N{}%`KX} zzA1&|iYaFBC*aqZ!Z&Y5m=Wf(`|+`#8vE^SGwB^$bkUP%je4UAF8ZPvR{;z!dbx#C z1oSr_QP<$wHRKzVja#7xx2CC9e7NJftI~f+IYqMZB;$S4?2+6+SOgG=RTgSZZF4~v z(2UT+`G>@@MgJS~)Mg%fz^`F#$|^uWEsJub!W*SAESmJ2j1Z^iBG}YE^upi@o&0<} zYJQ#zaYf4wdOw{dkXeA6SBETzepvU-faj$g*m{s8$lA-x%YckHj3J1aH~UQr3%@i- zk-AL?9Quni-@l)ExGWQAIQNtOJzKpC0TBF_9s8NQJzUVr(Nm?payu;jYlzls*JiO> zeD-t?_Vb(1WdOkg2CHT~)Cqej^1bI-!ON)b+?W}D>}}gdKQD$ZOqB9d6X0K z)qFQ@yUn9kzl6B}GxmT$2&}Jn>bz(2^KFC*twKaDykUjEA3rMKi??R^5a(T!=NgGmZ|N0G)=}ayKS`u*j~K8PpchjNRbuw*4Xw z^X0f2pGY>RUWGp%)sn?yeTChD1fr}sm=GdL+8P?SlvkK`WLCW_HT89K+Zl-;Pa0BC zNs6ad?s=KZMy`4}+AoVz`qJ98e20%k)u#oIj|Q!JK$j^CNo2u%*l$HcjR2kRnCr1Vrkb#LI(uiG2Tzo29ATe|sO#8rn28-6NDdcN*qd%qdh(gqan%~DXI@OY@F_ez@I@+`dKKy(XX1Q0G#MR5nhRG7; zDj8p5UWF&D*`F~_<_hDQ~TW3 zT8_4|k_q7P`u#A&;wPZpj- z&{a^h+Uswi14ggGFka7yhO&NJX3+ecX1+qwqggCH=IXeg%}>6Rte+Z;-STi|JHza? zA4gouM^9of=|VtbNuwe`=~9m9YW1(sv>9A-neE2Rv#l(D=(^p^si938nY#~x({VtJ zCD_y~nz-GD`OvL+`XKx)dq6tBfz-#kKLHvZ+v+p9eCTM79?3sZ-(^!eHtp*d9F#{) z_YSmLXk5^6Q(IAFfzEGw4N~RAGXcn|UHkf>mT2QN(|WAdb}d6nf3j$>b$&=3hrk+y z3p^4rVSPI)VTIy)6a70nAG<@_DaXuI9OxbVLC8^L0Hm&g&x+Q{5d-My-8y8ULqpVn zEN{az=l7VpkQiD=9vlEn^u>u0u}V&ZDxqbX>gfG*ELh|yb(_u;Gs!qRBLg7R9@vmp z2zoijfp(EwVdPqij=i6&Llwr>ay*)cc7!y%ES2}`GgG)dDmEAHh+PP{=BZ31+;%Y* z;GZmr7&)vNtXhasb~XlZgE-27@FHR%-^uN{v5yKDpnX&cQk(6|{A%uOvmt;Sq^FXp z{c40RH5`=o%5#v0*Z=JFYv4-U59LUmK1WN!?JQp!Y@aZ;915gJTrM-XkZ1rd4?OY3 z@T_pePk8Az1gHq@t*eqR|+G z=XJPCV&Vt?t^zPrMX;+t?qjVmKuE{@b%>j~>Qz^Ie0COBWk#+Z`BH7KW{bxfGkB5= zKfMwT%E;mo=lVOe!de~LGpY<_}( zY=Ryv=2)WRUNX8bd-_L54v!P=L!bOdRIe*6{)IW7G^ucE1fYIK@Q9}`YvB4&33+5> z8QGQpTT}f#E@jwml4J@@y8Gj70XbL zJ@%&%%sR78!<6a#;5##M?x?YxneJgAv+0p@>va`(rix|r82M9*WXbtHo~TH*{T_=- zwv@w? zdvjj?gu+dg0`+Oc%!jkS2|@0_FE*KC2eJqQQpB>853~1^{RGxU?#)pxd^y zQ{-o%mR|-kr$djs_@8s#-9_`A|?Bjk};*9+EyY_=*BVSCRGle7G>H!2= z7_^snEY@*f=$QPh^iLg^a1xFs6u37l4#hHTrgeXEw=pqW#3dPZlZaVDs|%J-u@y+> z$NZ{%HH(@g{*enP3l=f_#S6#stWa=Z@;FIaTYo4aM+lJauV!AtH#uzY=u5NMdGiAD z_hizUP%8%wq35O8OHwa@)X$5!*gwUrJ>Uiw+Xl~Ep5suQV$%IvZct}5XBX%+b7E>` z3eyM@%~}wp^MB%k@o)Xda6ClY{P$Y&B;`2fsrID74fMZrc0~1Mo2X*_h767DK9Q60 zdmwUC`Bf?PmT?wib}K;7aZzB3VoE42?y`ePL_1ViKq7SRRrdHfZY#~3p^-cy@-`4n z@IMtCK*d~Try@VbJck zfBp_E4myZrFzn7^%H`Z_OH|g{?eTOh^(W}C4RyjT8zDmM1z(rExuKx4mP z5S~vUVh9R8LEqp2iCt`BtknZEf=>Xh%0*>b9E=$mB2g^jHGTGLjN628QuA+?e@wz- zF8P~@l``tANH7|;gi84-+cn z{MMv@IK86HUvkwn{VDGorbl36(X=k# zYZ4W+8alQRRKmut`os|WUItPso?gMt5@z|fEV(5rlQhwLRFDW&GZ*pWOI$>ynRTs% zC`WdXUo7<`hbA4Hw_*709d}tMhhS$8)@3eBg1B0R;FQb4_pwmRKQ%X%#P8#u{YG+7 zL969G^)tiR%dw(8b#b$b0V|Vp!`i%dfB;M=6hc0=Bd)?v)?>;M`rP@CVc`&MOISHj zV-fuF=yMqPOMnDl9u@lalX^Aj;XV;;E-z}YX*Un391lDkwq`r+zOn9q#$FOF5e$ng z;4TG(gq%`3R@8qSR}ZDK-;ne#iKVivf!;m>btm^L4?yn4$Tb&g2G`-haSS1ZPs{&4 zr^e~jQ&`yuD3-G2pM^Uo{iVB1!ZTQT4E}p;gv3EihmZ>ZCnpN*!b@+nht7A#`p?5phF7hls{Ex3-myP3MUi{?9cK~yB< z0ni<8hleeNp@em>~gtpe3^usaUL64Drc|Tj5QlX!u|<)l=sv z$8J05OccWKW}R2g0ne|98vH3{=Krbd8b1Gh-_XfC*)Z2X)Od?`wQuEZ&4=-2sltAy zE`aYKUnL4!IkhG*{^BBDUo8N7mzuc)DSwsnJSRB-vUKIQ?wL|!nOvKJ{m=b1Y>i-N z0t*qz`g(p7Ewz6JgzP2F zWxMN%)kcDWEHWs6-0D4QWeR}tNb?$zKSCnI6t6%rd;x0YJYr0m$NC%VoR$AONPo=t z@RX~hM1gDbX-mk;H)=X;C4;#<{TgRI@qO<2z=3LKOmcL!+9A|T(3aDKj}9L;w7Snf&T~F Cl9AH@ diff --git a/dev-packages/browser-integration-tests/suites/tracing/metrics/web-vitals-lcp-streamed-spans/template.html b/dev-packages/browser-integration-tests/suites/tracing/metrics/web-vitals-lcp-streamed-spans/template.html deleted file mode 100644 index b613a556aca4..000000000000 --- a/dev-packages/browser-integration-tests/suites/tracing/metrics/web-vitals-lcp-streamed-spans/template.html +++ /dev/null @@ -1,10 +0,0 @@ - - - - - - -
- - - diff --git a/dev-packages/browser-integration-tests/suites/tracing/metrics/web-vitals-lcp-streamed-spans/test.ts b/dev-packages/browser-integration-tests/suites/tracing/metrics/web-vitals-lcp-streamed-spans/test.ts deleted file mode 100644 index 57a2d907eeb0..000000000000 --- a/dev-packages/browser-integration-tests/suites/tracing/metrics/web-vitals-lcp-streamed-spans/test.ts +++ /dev/null @@ -1,69 +0,0 @@ -import type { Route } from '@playwright/test'; -import { expect } from '@playwright/test'; -import { sentryTest } from '../../../../utils/fixtures'; -import { shouldSkipTracingTest } from '../../../../utils/helpers'; -import { getSpanOp, waitForStreamedSpan } from '../../../../utils/spanUtils'; - -sentryTest.beforeEach(async ({ browserName, page }) => { - if (shouldSkipTracingTest() || browserName !== 'chromium') { - sentryTest.skip(); - } - - await page.setViewportSize({ width: 800, height: 1200 }); -}); - -sentryTest('captures LCP as a streamed span with element attributes', async ({ getLocalTestUrl, page }) => { - page.route('**', route => route.continue()); - page.route('**/my/image.png', async (route: Route) => { - return route.fulfill({ - path: `${__dirname}/assets/sentry-logo-600x179.png`, - }); - }); - - const url = await getLocalTestUrl({ testDir: __dirname }); - - const lcpSpanPromise = waitForStreamedSpan(page, span => getSpanOp(span) === 'ui.webvital.lcp'); - const pageloadSpanPromise = waitForStreamedSpan(page, span => getSpanOp(span) === 'pageload'); - - await page.goto(url); - - // Wait for LCP to be captured - await page.waitForTimeout(1000); - - // LCP finalizes on the first trusted input or visibility change, and web-vitals checks - // `isTrusted`, so a synthetically dispatched `visibilitychange` does not finalize it. Click to - // finalize the way a real user would. - await page.click('body'); - - const lcpSpan = await lcpSpanPromise; - const pageloadSpan = await pageloadSpanPromise; - - expect(lcpSpan.attributes['sentry.op']).toEqual({ type: 'string', value: 'ui.webvital.lcp' }); - expect(lcpSpan.attributes['sentry.origin']).toEqual({ type: 'string', value: 'auto.http.browser.lcp' }); - expect(lcpSpan.attributes['sentry.exclusive_time']).toEqual({ type: 'integer', value: 0 }); - expect(lcpSpan.attributes['user_agent.original']?.value).toEqual(expect.stringContaining('Chrome')); - - // Check the LCP span carries the segment name it belongs to - expect(lcpSpan.attributes['sentry.segment.name']).toEqual({ type: 'string', value: 'Pageload' }); - - // Check browser.web_vital.lcp.* attributes - expect(lcpSpan.attributes['browser.web_vital.lcp.element']?.value).toEqual(expect.stringContaining('body > img')); - expect(lcpSpan.attributes['browser.web_vital.lcp.url']?.value).toBe('https://sentry-test-site.example/my/image.png'); - expect(lcpSpan.attributes['browser.web_vital.lcp.size']?.value).toEqual(expect.any(Number)); - - // Check web vital value attribute - expect(lcpSpan.attributes['browser.web_vital.lcp.value']?.type).toMatch(/^(double)|(integer)$/); - expect(lcpSpan.attributes['browser.web_vital.lcp.value']?.value).toBeGreaterThan(0); - - // Check pageload span id is present - expect(lcpSpan.attributes['sentry.pageload.span_id']?.value).toBe(pageloadSpan.span_id); - - // Span should have meaningful duration (navigation start -> LCP event) - expect(lcpSpan.end_timestamp).toBeGreaterThan(lcpSpan.start_timestamp); - - expect(lcpSpan.span_id).toMatch(/^[\da-f]{16}$/); - expect(lcpSpan.trace_id).toMatch(/^[\da-f]{32}$/); - - expect(lcpSpan.parent_span_id).toBe(pageloadSpan.span_id); - expect(lcpSpan.trace_id).toBe(pageloadSpan.trace_id); -}); diff --git a/dev-packages/browser-integration-tests/suites/tracing/metrics/web-vitals-lcp-streamed-spans/init.js b/dev-packages/browser-integration-tests/suites/tracing/metrics/web-vitals-lcp/init.js similarity index 65% rename from dev-packages/browser-integration-tests/suites/tracing/metrics/web-vitals-lcp-streamed-spans/init.js rename to dev-packages/browser-integration-tests/suites/tracing/metrics/web-vitals-lcp/init.js index bd3b6ed17872..1f0b64911a75 100644 --- a/dev-packages/browser-integration-tests/suites/tracing/metrics/web-vitals-lcp-streamed-spans/init.js +++ b/dev-packages/browser-integration-tests/suites/tracing/metrics/web-vitals-lcp/init.js @@ -5,7 +5,6 @@ window._testBaseTimestamp = performance.timeOrigin / 1000; Sentry.init({ dsn: 'https://public@dsn.ingest.sentry.io/1337', - integrations: [Sentry.browserTracingIntegration(), Sentry.spanStreamingIntegration()], - traceLifecycle: 'stream', + integrations: [Sentry.browserTracingIntegration()], tracesSampleRate: 1, }); diff --git a/dev-packages/browser-integration-tests/suites/tracing/metrics/web-vitals-lcp/test.ts b/dev-packages/browser-integration-tests/suites/tracing/metrics/web-vitals-lcp/test.ts index 4d61904c58ac..427bfa2111a1 100644 --- a/dev-packages/browser-integration-tests/suites/tracing/metrics/web-vitals-lcp/test.ts +++ b/dev-packages/browser-integration-tests/suites/tracing/metrics/web-vitals-lcp/test.ts @@ -1,14 +1,18 @@ import type { Route } from '@playwright/test'; import { expect } from '@playwright/test'; -import type { Event } from '@sentry/core'; import { sentryTest } from '../../../../utils/fixtures'; -import { getFirstSentryEnvelopeRequest, shouldSkipTracingTest } from '../../../../utils/helpers'; +import { shouldSkipTracingTest } from '../../../../utils/helpers'; +import { getSpanOp, waitForStreamedSpan } from '../../../../utils/spanUtils'; -sentryTest('captures LCP vitals with element details', async ({ browserName, getLocalTestUrl, page }) => { +sentryTest.beforeEach(async ({ browserName, page }) => { if (shouldSkipTracingTest() || browserName !== 'chromium') { sentryTest.skip(); } + await page.setViewportSize({ width: 800, height: 1200 }); +}); + +sentryTest('captures LCP as a streamed span with element attributes', async ({ getLocalTestUrl, page }) => { page.route('**', route => route.continue()); page.route('**/my/image.png', async (route: Route) => { return route.fulfill({ @@ -17,16 +21,54 @@ sentryTest('captures LCP vitals with element details', async ({ browserName, get }); const url = await getLocalTestUrl({ testDir: __dirname }); - const [eventData] = await Promise.all([getFirstSentryEnvelopeRequest(page), page.goto(url)]); - expect(eventData.measurements).toBeDefined(); - expect(eventData.measurements?.lcp?.value).toBeDefined(); + const lcpSpanPromise = waitForStreamedSpan(page, span => getSpanOp(span) === 'ui.webvital.lcp'); + const pageloadSpanPromise = waitForStreamedSpan(page, span => getSpanOp(span) === 'pageload'); + + await page.goto(url); + + // Wait for LCP to be captured + await page.waitForTimeout(1000); + + // LCP finalizes on the first trusted input or visibility change, and web-vitals checks + // `isTrusted`, so a synthetically dispatched `visibilitychange` does not finalize it. Click to + // finalize the way a real user would. + await page.click('body'); + + const lcpSpan = await lcpSpanPromise; + const pageloadSpan = await pageloadSpanPromise; + + expect(lcpSpan.attributes['sentry.op']).toEqual({ type: 'string', value: 'ui.webvital.lcp' }); + expect(lcpSpan.attributes['sentry.origin']).toEqual({ type: 'string', value: 'auto.http.browser.lcp' }); + expect(lcpSpan.attributes['sentry.exclusive_time']).toEqual({ type: 'integer', value: 0 }); + expect(lcpSpan.attributes['user_agent.original']?.value).toEqual(expect.stringContaining('Chrome')); + + // Check the LCP span carries the segment name it belongs to + expect(lcpSpan.attributes['sentry.segment.name']).toEqual({ type: 'string', value: 'Pageload' }); + + // Check browser.web_vital.lcp.* attributes + expect(lcpSpan.attributes['browser.web_vital.lcp.element']?.value).toEqual(expect.stringContaining('body > img')); + expect(lcpSpan.attributes['browser.web_vital.lcp.url']?.value).toBe('https://sentry-test-site.example/my/image.png'); + expect(lcpSpan.attributes['browser.web_vital.lcp.size']?.value).toEqual(expect.any(Number)); + + // Check web vital value attribute + expect(lcpSpan.attributes['browser.web_vital.lcp.value']?.type).toMatch(/^(double)|(integer)$/); + expect(lcpSpan.attributes['browser.web_vital.lcp.value']?.value).toBeGreaterThan(0); + + const renderTime = lcpSpan.attributes['browser.web_vital.lcp.render_time']?.value as number; + expect(renderTime).toBeGreaterThan(0); + expect(lcpSpan.attributes['browser.web_vital.lcp.load_time']?.value).toBeGreaterThan(0); + expect(lcpSpan.attributes['browser.web_vital.lcp.value']?.value).toBeCloseTo(renderTime); + + // Check pageload span id is present + expect(lcpSpan.attributes['sentry.pageload.span_id']?.value).toBe(pageloadSpan.span_id); + + // Span should have meaningful duration (navigation start -> LCP event) + expect(lcpSpan.end_timestamp).toBeGreaterThan(lcpSpan.start_timestamp); - expect(eventData.contexts?.trace?.data?.['lcp.element'].startsWith('body >')).toBe(true); - expect(eventData.contexts?.trace?.data?.['lcp.size']).toBeGreaterThan(0); - expect(eventData.contexts?.trace?.data?.['lcp.loadTime']).toBeGreaterThan(0); - expect(eventData.contexts?.trace?.data?.['lcp.renderTime']).toBeGreaterThan(0); + expect(lcpSpan.span_id).toMatch(/^[\da-f]{16}$/); + expect(lcpSpan.trace_id).toMatch(/^[\da-f]{32}$/); - // The LCP value should be the renderTime because the renderTime is set - expect(eventData.measurements?.lcp?.value).toBeCloseTo(eventData.contexts?.trace?.data?.['lcp.renderTime']); + expect(lcpSpan.parent_span_id).toBe(pageloadSpan.span_id); + expect(lcpSpan.trace_id).toBe(pageloadSpan.trace_id); }); diff --git a/dev-packages/browser-integration-tests/suites/tracing/metrics/web-vitals-ttfb-streamed/test.ts b/dev-packages/browser-integration-tests/suites/tracing/metrics/web-vitals-ttfb-streamed/test.ts deleted file mode 100644 index 4a51ad0f6948..000000000000 --- a/dev-packages/browser-integration-tests/suites/tracing/metrics/web-vitals-ttfb-streamed/test.ts +++ /dev/null @@ -1,36 +0,0 @@ -import { expect } from '@playwright/test'; -import { sentryTest } from '../../../../utils/fixtures'; -import { hidePage, shouldSkipTracingTest } from '../../../../utils/helpers'; -import { getSpanOp, waitForStreamedSpan } from '../../../../utils/spanUtils'; - -sentryTest.beforeEach(async ({ page }) => { - if (shouldSkipTracingTest()) { - sentryTest.skip(); - } - - await page.setViewportSize({ width: 800, height: 1200 }); -}); - -sentryTest( - 'captures TTFB and TTFB request time as attributes on the streamed pageload span', - async ({ getLocalTestUrl, page }) => { - const url = await getLocalTestUrl({ testDir: __dirname }); - - const pageloadSpanPromise = waitForStreamedSpan(page, span => getSpanOp(span) === 'pageload'); - - await page.goto(url); - await hidePage(page); - - const pageloadSpan = await pageloadSpanPromise; - - // If responseStart === 0, TTFB is not reported. - // This seems to happen somewhat randomly, so we handle it. - const responseStart = await page.evaluate("performance.getEntriesByType('navigation')[0].responseStart;"); - if (responseStart !== 0) { - expect(pageloadSpan.attributes['browser.web_vital.ttfb.value']?.type).toMatch(/^(double)|(integer)$/); - expect(pageloadSpan.attributes['browser.web_vital.ttfb.value']?.value).toBeGreaterThan(0); - } - - expect(pageloadSpan.attributes['browser.web_vital.ttfb.request_time']?.type).toMatch(/^(double)|(integer)$/); - }, -); diff --git a/dev-packages/browser-integration-tests/suites/tracing/metrics/web-vitals-ttfb-streamed/init.js b/dev-packages/browser-integration-tests/suites/tracing/metrics/web-vitals-ttfb/init.js similarity index 66% rename from dev-packages/browser-integration-tests/suites/tracing/metrics/web-vitals-ttfb-streamed/init.js rename to dev-packages/browser-integration-tests/suites/tracing/metrics/web-vitals-ttfb/init.js index d8da96d88a64..a9df1902a276 100644 --- a/dev-packages/browser-integration-tests/suites/tracing/metrics/web-vitals-ttfb-streamed/init.js +++ b/dev-packages/browser-integration-tests/suites/tracing/metrics/web-vitals-ttfb/init.js @@ -5,8 +5,7 @@ window._testBaseTimestamp = performance.timeOrigin / 1000; Sentry.init({ dsn: 'https://public@dsn.ingest.sentry.io/1337', - integrations: [Sentry.browserTracingIntegration(), Sentry.spanStreamingIntegration()], - traceLifecycle: 'stream', + integrations: [Sentry.browserTracingIntegration()], tracesSampleRate: 1, debug: true, }); diff --git a/dev-packages/browser-integration-tests/suites/tracing/metrics/web-vitals-ttfb/test.ts b/dev-packages/browser-integration-tests/suites/tracing/metrics/web-vitals-ttfb/test.ts index d2997f712c39..4a51ad0f6948 100644 --- a/dev-packages/browser-integration-tests/suites/tracing/metrics/web-vitals-ttfb/test.ts +++ b/dev-packages/browser-integration-tests/suites/tracing/metrics/web-vitals-ttfb/test.ts @@ -1,24 +1,36 @@ import { expect } from '@playwright/test'; -import type { Event } from '@sentry/core'; import { sentryTest } from '../../../../utils/fixtures'; -import { getFirstSentryEnvelopeRequest, shouldSkipTracingTest } from '../../../../utils/helpers'; +import { hidePage, shouldSkipTracingTest } from '../../../../utils/helpers'; +import { getSpanOp, waitForStreamedSpan } from '../../../../utils/spanUtils'; -sentryTest('should capture TTFB vital.', async ({ getLocalTestUrl, page }) => { +sentryTest.beforeEach(async ({ page }) => { if (shouldSkipTracingTest()) { sentryTest.skip(); } - const url = await getLocalTestUrl({ testDir: __dirname }); - const eventData = await getFirstSentryEnvelopeRequest(page, url); + await page.setViewportSize({ width: 800, height: 1200 }); +}); - expect(eventData.measurements).toBeDefined(); +sentryTest( + 'captures TTFB and TTFB request time as attributes on the streamed pageload span', + async ({ getLocalTestUrl, page }) => { + const url = await getLocalTestUrl({ testDir: __dirname }); - // If responseStart === 0, ttfb is not reported - // This seems to happen somewhat randomly, so we just ignore this in that case - const responseStart = await page.evaluate("performance.getEntriesByType('navigation')[0].responseStart;"); - if (responseStart !== 0) { - expect(eventData.measurements?.ttfb?.value).toBeDefined(); - } + const pageloadSpanPromise = waitForStreamedSpan(page, span => getSpanOp(span) === 'pageload'); - expect(eventData.measurements?.['ttfb.requestTime']?.value).toBeDefined(); -}); + await page.goto(url); + await hidePage(page); + + const pageloadSpan = await pageloadSpanPromise; + + // If responseStart === 0, TTFB is not reported. + // This seems to happen somewhat randomly, so we handle it. + const responseStart = await page.evaluate("performance.getEntriesByType('navigation')[0].responseStart;"); + if (responseStart !== 0) { + expect(pageloadSpan.attributes['browser.web_vital.ttfb.value']?.type).toMatch(/^(double)|(integer)$/); + expect(pageloadSpan.attributes['browser.web_vital.ttfb.value']?.value).toBeGreaterThan(0); + } + + expect(pageloadSpan.attributes['browser.web_vital.ttfb.request_time']?.type).toMatch(/^(double)|(integer)$/); + }, +); diff --git a/dev-packages/browser-integration-tests/suites/tracing/metrics/web-vitals/test.ts b/dev-packages/browser-integration-tests/suites/tracing/metrics/web-vitals/test.ts index 751a7111ce54..482dd815af14 100644 --- a/dev-packages/browser-integration-tests/suites/tracing/metrics/web-vitals/test.ts +++ b/dev-packages/browser-integration-tests/suites/tracing/metrics/web-vitals/test.ts @@ -1,8 +1,8 @@ import type { Route } from '@playwright/test'; import { expect } from '@playwright/test'; -import type { Event } from '@sentry/core'; import { sentryTest } from '../../../../utils/fixtures'; -import { getFirstSentryEnvelopeRequest, shouldSkipTracingTest } from '../../../../utils/helpers'; +import { shouldSkipTracingTest } from '../../../../utils/helpers'; +import { getSpanOp, waitForStreamedSpan } from '../../../../utils/spanUtils'; /** * Bit of an odd test but we previously ran into cases where we would report TTFB > (LCP, FCP, FP) @@ -23,15 +23,15 @@ sentryTest('paint web vitals values are greater than TTFB', async ({ browserName }); const url = await getLocalTestUrl({ testDir: __dirname }); - const [eventData] = await Promise.all([ - getFirstSentryEnvelopeRequest(page), - page.goto(url), - page.locator('button').click(), - ]); + const pageloadPromise = waitForStreamedSpan(page, span => span.is_segment && getSpanOp(span) === 'pageload'); + const lcpPromise = waitForStreamedSpan(page, span => getSpanOp(span) === 'ui.webvital.lcp'); + await page.goto(url); + await page.locator('img').evaluate((img: HTMLImageElement) => img.decode()); + await page.waitForTimeout(1000); + await page.locator('button').click(); + const [eventData, lcpSpan] = await Promise.all([pageloadPromise, lcpPromise]); - expect(eventData.measurements).toBeDefined(); - - const ttfbValue = eventData.measurements?.ttfb?.value; + const ttfbValue = eventData.attributes['browser.web_vital.ttfb.value']?.value as number | undefined; if (!ttfbValue) { // TTFB is unfortunately quite flaky. Sometimes, the web-vitals library doesn't report TTFB because @@ -44,9 +44,9 @@ sentryTest('paint web vitals values are greater than TTFB', async ({ browserName sentryTest.skip(); } - const lcpValue = eventData.measurements?.lcp?.value; - const fcpValue = eventData.measurements?.fcp?.value; - const fpValue = eventData.measurements?.fp?.value; + const lcpValue = lcpSpan.attributes['browser.web_vital.lcp.value']?.value; + const fcpValue = eventData.attributes['browser.web_vital.fcp.value']?.value; + const fpValue = eventData.attributes['browser.web_vital.fp.value']?.value; expect(lcpValue).toBeDefined(); expect(fcpValue).toBeDefined(); @@ -67,17 +67,20 @@ sentryTest( } const url = await getLocalTestUrl({ testDir: __dirname }); - const [eventData] = await Promise.all([getFirstSentryEnvelopeRequest(page), page.goto(url)]); + const [eventData] = await Promise.all([ + waitForStreamedSpan(page, span => span.is_segment && getSpanOp(span) === 'pageload'), + page.goto(url), + ]); - const timeOriginAttribute = eventData.contexts?.trace?.data?.['performance.timeOrigin']; - const activationStart = eventData.contexts?.trace?.data?.['performance.activationStart']; + const timeOriginAttribute = eventData.attributes['browser.performance.time_origin']?.value as number; + const activationStart = eventData.attributes['browser.performance.navigation.activation_start']?.value; - const transactionStartTimestamp = eventData.start_timestamp; + const spanStartTimestamp = eventData.start_timestamp; expect(timeOriginAttribute).toBeDefined(); - expect(transactionStartTimestamp).toBeDefined(); + expect(spanStartTimestamp).toBeDefined(); - const delta = Math.abs(transactionStartTimestamp! - timeOriginAttribute); + const delta = Math.abs(spanStartTimestamp! - timeOriginAttribute); // The delta should be less than 1ms if this flakes, we should increase the threshold expect(delta).toBeLessThanOrEqual(1); diff --git a/dev-packages/browser-integration-tests/suites/tracing/request/fetch-data-url/init.js b/dev-packages/browser-integration-tests/suites/tracing/request/fetch-data-url/init.js index d1c5e9767aa2..5ab240338c8c 100644 --- a/dev-packages/browser-integration-tests/suites/tracing/request/fetch-data-url/init.js +++ b/dev-packages/browser-integration-tests/suites/tracing/request/fetch-data-url/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()], tracesSampleRate: 1, diff --git a/dev-packages/browser-integration-tests/suites/tracing/request/fetch-data-url/test.ts b/dev-packages/browser-integration-tests/suites/tracing/request/fetch-data-url/test.ts index 2e79255e3993..e120b22f5c90 100644 --- a/dev-packages/browser-integration-tests/suites/tracing/request/fetch-data-url/test.ts +++ b/dev-packages/browser-integration-tests/suites/tracing/request/fetch-data-url/test.ts @@ -1,34 +1,27 @@ import { expect } from '@playwright/test'; import { sentryTest } from '../../../../utils/fixtures'; -import { - envelopeRequestParser, - shouldSkipTracingTest, - waitForTransactionRequestOnUrl, -} from '../../../../utils/helpers'; +import { shouldSkipTracingTest } from '../../../../utils/helpers'; +import { collectStreamedSpans, getSpanOp, waitForStreamedSpan } from '../../../../utils/spanUtils'; sentryTest('sanitizes data URLs in fetch span name and attributes', async ({ getLocalTestUrl, page }) => { - if (shouldSkipTracingTest()) { - sentryTest.skip(); - } - + sentryTest.skip(shouldSkipTracingTest()); + const spans = collectStreamedSpans(page); + const pageloadPromise = waitForStreamedSpan(page, span => span.is_segment && getSpanOp(span) === 'pageload'); const url = await getLocalTestUrl({ testDir: __dirname }); - - const req = await waitForTransactionRequestOnUrl(page, url); - const transactionEvent = envelopeRequestParser(req); - - const requestSpans = transactionEvent.spans?.filter(({ op }) => op === 'http.client'); - - expect(requestSpans).toHaveLength(1); - - const span = requestSpans?.[0]; - - const sanitizedUrl = 'data:text/plain,base64,SGVsbG8gV2... [truncated]'; - expect(span?.description).toBe(`GET ${sanitizedUrl}`); - - expect(span?.data).toMatchObject({ - 'http.request.method': 'GET', - type: 'fetch', + await page.goto(url); + const pageload = await pageloadPromise; + await page.evaluate(() => (window as any).Sentry.flush()); + + const requests = spans.filter(span => getSpanOp(span) === 'http.client'); + expect(requests).toHaveLength(1); + const span = requests[0]; + expect(span.name).toBe('GET'); + expect(span.parent_span_id).toBe(pageload.span_id); + expect(span.trace_id).toBe(pageload.trace_id); + expect(span.attributes['http.request.method']).toEqual({ type: 'string', value: 'GET' }); + expect(span.attributes.type).toEqual({ type: 'string', value: 'fetch' }); + expect(span.attributes['url.full']).toEqual({ + type: 'string', + value: 'data:text/plain,base64,SGVsbG8gV2... [truncated]', }); - - expect(span?.data?.['url.full']).toBe(sanitizedUrl); }); diff --git a/dev-packages/browser-integration-tests/suites/tracing/request/fetch-immediate/init.js b/dev-packages/browser-integration-tests/suites/tracing/request/fetch-immediate/init.js index 1ad638ebeadb..0a301839c169 100644 --- a/dev-packages/browser-integration-tests/suites/tracing/request/fetch-immediate/init.js +++ b/dev-packages/browser-integration-tests/suites/tracing/request/fetch-immediate/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()], tracePropagationTargets: ['http://sentry-test-site.example'], diff --git a/dev-packages/browser-integration-tests/suites/tracing/request/fetch-immediate/test.ts b/dev-packages/browser-integration-tests/suites/tracing/request/fetch-immediate/test.ts index f43ff5448679..f69631959e3d 100644 --- a/dev-packages/browser-integration-tests/suites/tracing/request/fetch-immediate/test.ts +++ b/dev-packages/browser-integration-tests/suites/tracing/request/fetch-immediate/test.ts @@ -1,10 +1,7 @@ +import { collectStreamedSpans, getSpanOp, waitForStreamedSpan } from '../../../../utils/spanUtils'; import { expect } from '@playwright/test'; import { sentryTest } from '../../../../utils/fixtures'; -import { - envelopeRequestParser, - shouldSkipTracingTest, - waitForTransactionRequestOnUrl, -} from '../../../../utils/helpers'; +import { shouldSkipTracingTest } from '../../../../utils/helpers'; sentryTest('should create spans for fetch requests called directly after init', async ({ getLocalTestUrl, page }) => { if (shouldSkipTracingTest()) { @@ -15,25 +12,28 @@ sentryTest('should create spans for fetch requests called directly after init', const url = await getLocalTestUrl({ testDir: __dirname }); - const req = await waitForTransactionRequestOnUrl(page, url); - const tracingEvent = envelopeRequestParser(req); + const spans = collectStreamedSpans(page); + const pageloadPromise = waitForStreamedSpan(page, span => span.is_segment && getSpanOp(span) === 'pageload'); + await page.goto(url); + const pageload = await pageloadPromise; + await page.evaluate(() => (window as any).Sentry.flush()); - const requestSpans = tracingEvent.spans?.filter(({ op }) => op === 'http.client'); + const requestSpans = spans.filter(span => getSpanOp(span) === 'http.client'); expect(requestSpans).toHaveLength(1); expect(requestSpans![0]).toMatchObject({ - description: 'GET http://sentry-test-site.example/0', - parent_span_id: tracingEvent.contexts?.trace?.span_id, + name: 'GET sentry-test-site.example', + parent_span_id: pageload.span_id, span_id: expect.stringMatching(/[a-f\d]{16}/), start_timestamp: expect.any(Number), - timestamp: expect.any(Number), - trace_id: tracingEvent.contexts?.trace?.trace_id, - data: { - 'http.request.method': 'GET', - 'url.full': 'http://sentry-test-site.example/0', - 'server.address': 'sentry-test-site.example', - type: 'fetch', - }, + end_timestamp: expect.any(Number), + trace_id: pageload.trace_id, + attributes: expect.objectContaining({ + 'http.request.method': { type: 'string', value: 'GET' }, + 'url.full': { type: 'string', value: 'http://sentry-test-site.example/0' }, + 'server.address': { type: 'string', value: 'sentry-test-site.example' }, + type: { type: 'string', value: 'fetch' }, + }), }); }); diff --git a/dev-packages/browser-integration-tests/suites/tracing/request/fetch-no-tracing/init.js b/dev-packages/browser-integration-tests/suites/tracing/request/fetch-no-tracing/init.js index 2db7a75be0e6..3cbcaa4fe152 100644 --- a/dev-packages/browser-integration-tests/suites/tracing/request/fetch-no-tracing/init.js +++ b/dev-packages/browser-integration-tests/suites/tracing/request/fetch-no-tracing/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', tracePropagationTargets: ['http://sentry-test-site.example'], }); diff --git a/dev-packages/browser-integration-tests/suites/tracing/request/fetch-propagateTraceparent/init.js b/dev-packages/browser-integration-tests/suites/tracing/request/fetch-propagateTraceparent/init.js index efcc9dde33fa..cb785bbf7fda 100644 --- a/dev-packages/browser-integration-tests/suites/tracing/request/fetch-propagateTraceparent/init.js +++ b/dev-packages/browser-integration-tests/suites/tracing/request/fetch-propagateTraceparent/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()], tracePropagationTargets: ['http://sentry-test-site.example'], diff --git a/dev-packages/browser-integration-tests/suites/tracing/request/fetch-relative-url-streamed/subject.js b/dev-packages/browser-integration-tests/suites/tracing/request/fetch-relative-url-streamed/subject.js deleted file mode 100644 index 143b078692db..000000000000 --- a/dev-packages/browser-integration-tests/suites/tracing/request/fetch-relative-url-streamed/subject.js +++ /dev/null @@ -1 +0,0 @@ -fetch('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/test-req/0').then(fetch('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/test-req/1').then(fetch('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/test-req/2'))); diff --git a/dev-packages/browser-integration-tests/suites/tracing/request/fetch-relative-url-streamed/test.ts b/dev-packages/browser-integration-tests/suites/tracing/request/fetch-relative-url-streamed/test.ts deleted file mode 100644 index 820103bac06e..000000000000 --- a/dev-packages/browser-integration-tests/suites/tracing/request/fetch-relative-url-streamed/test.ts +++ /dev/null @@ -1,39 +0,0 @@ -import { expect } from '@playwright/test'; -import { sentryTest, TEST_HOST } from '../../../../utils/fixtures'; -import { shouldSkipTracingTest } from '../../../../utils/helpers'; -import { getSpanOp, waitForStreamedSpans } from '../../../../utils/spanUtils'; - -sentryTest('names spans for relative fetch requests after the page domain', async ({ getLocalTestUrl, page }) => { - sentryTest.skip(shouldSkipTracingTest()); - - const url = await getLocalTestUrl({ testDir: __dirname }); - - const spansPromise = waitForStreamedSpans( - page, - spans => spans.filter(s => getSpanOp(s) === 'http.client').length >= 3, - ); - - await page.goto(url); - - const requestSpans = (await spansPromise) - .filter(s => getSpanOp(s) === 'http.client') - .sort((a, b) => - (a.attributes!['url.full']!.value as string).localeCompare(b.attributes!['url.full']!.value as string), - ); - - expect(requestSpans).toHaveLength(3); - - requestSpans.forEach((span, index) => - expect(span).toMatchObject({ - // A relative URL has no domain of its own, so it resolves against the page origin. - name: 'GET sentry-test.io', - attributes: expect.objectContaining({ - 'http.request.method': { type: 'string', value: 'GET' }, - 'url.full': { type: 'string', value: `${TEST_HOST}/test-req/${index}` }, - 'url.domain': { type: 'string', value: 'sentry-test.io' }, - 'server.address': { type: 'string', value: 'sentry-test.io' }, - type: { type: 'string', value: 'fetch' }, - }), - }), - ); -}); diff --git a/dev-packages/browser-integration-tests/suites/tracing/request/fetch-relative-url/init.js b/dev-packages/browser-integration-tests/suites/tracing/request/fetch-relative-url/init.js index bb6d4918d1bf..5ab240338c8c 100644 --- a/dev-packages/browser-integration-tests/suites/tracing/request/fetch-relative-url/init.js +++ b/dev-packages/browser-integration-tests/suites/tracing/request/fetch-relative-url/init.js @@ -3,8 +3,8 @@ import * as Sentry from '@sentry/browser'; window.Sentry = Sentry; Sentry.init({ - traceLifecycle: 'static', dsn: 'https://public@dsn.ingest.sentry.io/1337', integrations: [Sentry.browserTracingIntegration()], tracesSampleRate: 1, + autoSessionTracking: false, }); diff --git a/dev-packages/browser-integration-tests/suites/tracing/request/fetch-relative-url/test.ts b/dev-packages/browser-integration-tests/suites/tracing/request/fetch-relative-url/test.ts index 323060c7e4b2..5f42cfef8ef1 100644 --- a/dev-packages/browser-integration-tests/suites/tracing/request/fetch-relative-url/test.ts +++ b/dev-packages/browser-integration-tests/suites/tracing/request/fetch-relative-url/test.ts @@ -1,38 +1,41 @@ import { expect } from '@playwright/test'; import { sentryTest, TEST_HOST } from '../../../../utils/fixtures'; -import { - envelopeRequestParser, - shouldSkipTracingTest, - waitForTransactionRequestOnUrl, -} from '../../../../utils/helpers'; +import { shouldSkipTracingTest } from '../../../../utils/helpers'; +import { collectStreamedSpans, getSpanOp, waitForStreamedSpan } from '../../../../utils/spanUtils'; -sentryTest('should create spans for fetch requests', async ({ getLocalTestUrl, page }) => { - if (shouldSkipTracingTest()) { - sentryTest.skip(); - } +sentryTest('names spans for relative fetch requests after the page domain', async ({ getLocalTestUrl, page }) => { + sentryTest.skip(shouldSkipTracingTest()); const url = await getLocalTestUrl({ testDir: __dirname }); - const req = await waitForTransactionRequestOnUrl(page, url); - const tracingEvent = envelopeRequestParser(req); - const requestSpans = tracingEvent.spans?.filter(({ op }) => op === 'http.client'); + const spans = collectStreamedSpans(page); + const pageloadPromise = waitForStreamedSpan(page, span => span.is_segment && getSpanOp(span) === 'pageload'); + + await page.goto(url); + + const pageloadSpan = await pageloadPromise; + await page.evaluate(() => (window as any).Sentry.flush()); + const requestSpans = spans + .filter(s => getSpanOp(s) === 'http.client') + .sort((a, b) => + (a.attributes!['url.full']!.value as string).localeCompare(b.attributes!['url.full']!.value as string), + ); expect(requestSpans).toHaveLength(3); - requestSpans?.forEach((span, index) => + requestSpans.forEach((span, index) => expect(span).toMatchObject({ - description: `GET /test-req/${index}`, - parent_span_id: tracingEvent.contexts?.trace?.span_id, - span_id: expect.stringMatching(/[a-f\d]{16}/), - start_timestamp: expect.any(Number), - timestamp: expect.any(Number), - trace_id: tracingEvent.contexts?.trace?.trace_id, - data: { - 'http.request.method': 'GET', - 'url.full': `${TEST_HOST}/test-req/${index}`, - 'server.address': 'sentry-test.io', - type: 'fetch', - }, + // A relative URL has no domain of its own, so it resolves against the page origin. + name: 'GET sentry-test.io', + parent_span_id: pageloadSpan.span_id, + trace_id: pageloadSpan.trace_id, + attributes: expect.objectContaining({ + 'http.request.method': { type: 'string', value: 'GET' }, + 'url.full': { type: 'string', value: `${TEST_HOST}/test-req/${index}` }, + 'url.domain': { type: 'string', value: 'sentry-test.io' }, + 'server.address': { type: 'string', value: 'sentry-test.io' }, + type: { type: 'string', value: 'fetch' }, + }), }), ); }); diff --git a/dev-packages/browser-integration-tests/suites/tracing/request/fetch-streamed-track-stream-performance/init.js b/dev-packages/browser-integration-tests/suites/tracing/request/fetch-streamed-track-stream-performance/init.js deleted file mode 100644 index 260d01fe817b..000000000000 --- a/dev-packages/browser-integration-tests/suites/tracing/request/fetch-streamed-track-stream-performance/init.js +++ /dev/null @@ -1,15 +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.spanStreamingIntegration(), - Sentry.fetchStreamPerformanceIntegration(), - ], - tracePropagationTargets: ['http://sentry-test-site.example'], - tracesSampleRate: 1, - autoSessionTracking: false, -}); diff --git a/dev-packages/browser-integration-tests/suites/tracing/request/fetch-streamed/subject.js b/dev-packages/browser-integration-tests/suites/tracing/request/fetch-streamed/subject.js deleted file mode 100644 index 482a738009c2..000000000000 --- a/dev-packages/browser-integration-tests/suites/tracing/request/fetch-streamed/subject.js +++ /dev/null @@ -1,5 +0,0 @@ -fetch('http://sentry-test-site.example/0').then( - fetch('http://sentry-test-site.example/1', { headers: { 'X-Test-Header': 'existing-header' } }).then( - fetch('http://sentry-test-site.example/2'), - ), -); diff --git a/dev-packages/browser-integration-tests/suites/tracing/request/fetch-streamed/test.ts b/dev-packages/browser-integration-tests/suites/tracing/request/fetch-streamed/test.ts deleted file mode 100644 index 484a7151de1f..000000000000 --- a/dev-packages/browser-integration-tests/suites/tracing/request/fetch-streamed/test.ts +++ /dev/null @@ -1,48 +0,0 @@ -import { expect } from '@playwright/test'; -import { sentryTest } from '../../../../utils/fixtures'; -import { shouldSkipTracingTest } from '../../../../utils/helpers'; -import { getSpanOp, waitForStreamedSpans } from '../../../../utils/spanUtils'; - -sentryTest('creates spans for fetch requests', async ({ getLocalTestUrl, page }) => { - sentryTest.skip(shouldSkipTracingTest()); - - await page.route('http://sentry-test-site.example/*', route => route.fulfill({ body: 'ok' })); - - const url = await getLocalTestUrl({ testDir: __dirname }); - - const spansPromise = waitForStreamedSpans( - page, - spans => spans.filter(s => getSpanOp(s) === 'http.client').length >= 3, - ); - - await page.goto(url); - - const allSpans = await spansPromise; - const pageloadSpan = allSpans.find(s => getSpanOp(s) === 'pageload'); - const requestSpans = allSpans - .filter(s => getSpanOp(s) === 'http.client') - .sort((a, b) => - (a.attributes!['url.full']!.value as string).localeCompare(b.attributes!['url.full']!.value as string), - ); - - expect(requestSpans).toHaveLength(3); - - requestSpans.forEach((span, index) => - expect(span).toMatchObject({ - // Streamed span names drop the high-cardinality URL path. - name: 'GET sentry-test-site.example', - parent_span_id: pageloadSpan?.span_id, - span_id: expect.stringMatching(/[a-f\d]{16}/), - start_timestamp: expect.any(Number), - end_timestamp: expect.any(Number), - trace_id: pageloadSpan?.trace_id, - attributes: expect.objectContaining({ - 'http.request.method': { type: 'string', value: 'GET' }, - 'url.full': { type: 'string', value: `http://sentry-test-site.example/${index}` }, - 'url.domain': { type: 'string', value: 'sentry-test-site.example' }, - 'server.address': { type: 'string', value: 'sentry-test-site.example' }, - type: { type: 'string', value: 'fetch' }, - }), - }), - ); -}); diff --git a/dev-packages/browser-integration-tests/suites/tracing/request/fetch-strip-query-and-fragment/init.js b/dev-packages/browser-integration-tests/suites/tracing/request/fetch-strip-query-and-fragment/init.js index d334f6a7d2a6..de6b87574482 100644 --- a/dev-packages/browser-integration-tests/suites/tracing/request/fetch-strip-query-and-fragment/init.js +++ b/dev-packages/browser-integration-tests/suites/tracing/request/fetch-strip-query-and-fragment/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({ instrumentPageLoad: false, instrumentNavigation: false })], tracePropagationTargets: ['http://sentry-test-site.example'], diff --git a/dev-packages/browser-integration-tests/suites/tracing/request/fetch-strip-query-and-fragment/test.ts b/dev-packages/browser-integration-tests/suites/tracing/request/fetch-strip-query-and-fragment/test.ts index bbe15d81429f..08a70cc55468 100644 --- a/dev-packages/browser-integration-tests/suites/tracing/request/fetch-strip-query-and-fragment/test.ts +++ b/dev-packages/browser-integration-tests/suites/tracing/request/fetch-strip-query-and-fragment/test.ts @@ -1,6 +1,7 @@ import { expect } from '@playwright/test'; import { sentryTest } from '../../../../utils/fixtures'; -import { envelopeRequestParser, shouldSkipTracingTest, waitForTransactionRequest } from '../../../../utils/helpers'; +import { shouldSkipTracingTest } from '../../../../utils/helpers'; +import { getSpanOp, waitForStreamedSpan } from '../../../../utils/spanUtils'; sentryTest('strips query params in fetch request spans', async ({ getLocalTestUrl, page }) => { if (shouldSkipTracingTest()) { @@ -13,35 +14,34 @@ sentryTest('strips query params in fetch request spans', async ({ getLocalTestUr await page.goto(url); - const txnPromise = waitForTransactionRequest(page); + const rootPromise = waitForStreamedSpan(page, span => span.is_segment && span.name === 'rootSpan'); + const requestPromise = waitForStreamedSpan(page, span => getSpanOp(span) === 'http.client'); await page.locator('#btnQuery').click(); - const transactionEvent = envelopeRequestParser(await txnPromise); + const [rootSpan, requestSpan] = await Promise.all([rootPromise, requestPromise]); - expect(transactionEvent.transaction).toEqual('rootSpan'); - - const requestSpan = transactionEvent.spans?.find(({ op }) => op === 'http.client'); + expect(rootSpan.name).toBe('rootSpan'); expect(requestSpan).toMatchObject({ - description: 'GET http://sentry-test-site.example/0', - parent_span_id: transactionEvent.contexts?.trace?.span_id, + name: 'GET sentry-test-site.example', + parent_span_id: rootSpan.span_id, span_id: expect.stringMatching(/[a-f\d]{16}/), start_timestamp: expect.any(Number), - timestamp: expect.any(Number), - trace_id: transactionEvent.contexts?.trace?.trace_id, - data: expect.objectContaining({ - 'http.request.method': 'GET', - 'url.full': 'http://sentry-test-site.example/0?id=123;page=5', - 'url.query': 'id=123;page=5', - 'http.response.status_code': 200, - 'http.response.body.size': 2, - 'sentry.op': 'http.client', - 'sentry.origin': 'auto.http.browser', - type: 'fetch', - 'server.address': 'sentry-test-site.example', + end_timestamp: expect.any(Number), + trace_id: rootSpan.trace_id, + attributes: expect.objectContaining({ + 'http.request.method': { type: 'string', value: 'GET' }, + 'url.full': { type: 'string', value: 'http://sentry-test-site.example/0?id=123;page=5' }, + 'url.query': { type: 'string', value: 'id=123;page=5' }, + 'http.response.status_code': { type: 'integer', value: 200 }, + 'http.response.body.size': { type: 'integer', value: 2 }, + 'sentry.op': { type: 'string', value: 'http.client' }, + 'sentry.origin': { type: 'string', value: 'auto.http.browser' }, + type: { type: 'string', value: 'fetch' }, + 'server.address': { type: 'string', value: 'sentry-test-site.example' }, }), }); - expect(requestSpan?.data).not.toHaveProperty('url.fragment'); + expect(requestSpan?.attributes).not.toHaveProperty('url.fragment'); }); sentryTest('strips hash fragment in fetch request spans', async ({ getLocalTestUrl, page }) => { @@ -55,35 +55,34 @@ sentryTest('strips hash fragment in fetch request spans', async ({ getLocalTestU await page.goto(url); - const txnPromise = waitForTransactionRequest(page); + const rootPromise = waitForStreamedSpan(page, span => span.is_segment && span.name === 'rootSpan'); + const requestPromise = waitForStreamedSpan(page, span => getSpanOp(span) === 'http.client'); await page.locator('#btnFragment').click(); - const transactionEvent = envelopeRequestParser(await txnPromise); - - expect(transactionEvent.transaction).toEqual('rootSpan'); + const [rootSpan, requestSpan] = await Promise.all([rootPromise, requestPromise]); - const requestSpan = transactionEvent.spans?.find(({ op }) => op === 'http.client'); + expect(rootSpan.name).toBe('rootSpan'); expect(requestSpan).toMatchObject({ - description: 'GET http://sentry-test-site.example/1', - parent_span_id: transactionEvent.contexts?.trace?.span_id, + name: 'GET sentry-test-site.example', + parent_span_id: rootSpan.span_id, span_id: expect.stringMatching(/[a-f\d]{16}/), start_timestamp: expect.any(Number), - timestamp: expect.any(Number), - trace_id: transactionEvent.contexts?.trace?.trace_id, - data: expect.objectContaining({ - 'http.request.method': 'GET', - 'url.full': 'http://sentry-test-site.example/1#fragment', - 'url.fragment': 'fragment', - 'http.response.status_code': 200, - 'http.response.body.size': 2, - 'sentry.op': 'http.client', - 'sentry.origin': 'auto.http.browser', - type: 'fetch', - 'server.address': 'sentry-test-site.example', + end_timestamp: expect.any(Number), + trace_id: rootSpan.trace_id, + attributes: expect.objectContaining({ + 'http.request.method': { type: 'string', value: 'GET' }, + 'url.full': { type: 'string', value: 'http://sentry-test-site.example/1#fragment' }, + 'url.fragment': { type: 'string', value: 'fragment' }, + 'http.response.status_code': { type: 'integer', value: 200 }, + 'http.response.body.size': { type: 'integer', value: 2 }, + 'sentry.op': { type: 'string', value: 'http.client' }, + 'sentry.origin': { type: 'string', value: 'auto.http.browser' }, + type: { type: 'string', value: 'fetch' }, + 'server.address': { type: 'string', value: 'sentry-test-site.example' }, }), }); - expect(requestSpan?.data).not.toHaveProperty('url.query'); + expect(requestSpan?.attributes).not.toHaveProperty('url.query'); }); sentryTest('strips hash fragment and query params in fetch request spans', async ({ getLocalTestUrl, page }) => { @@ -97,32 +96,31 @@ sentryTest('strips hash fragment and query params in fetch request spans', async await page.goto(url); - const txnPromise = waitForTransactionRequest(page); + const rootPromise = waitForStreamedSpan(page, span => span.is_segment && span.name === 'rootSpan'); + const requestPromise = waitForStreamedSpan(page, span => getSpanOp(span) === 'http.client'); await page.locator('#btnQueryFragment').click(); - const transactionEvent = envelopeRequestParser(await txnPromise); + const [rootSpan, requestSpan] = await Promise.all([rootPromise, requestPromise]); - expect(transactionEvent.transaction).toEqual('rootSpan'); - - const requestSpan = transactionEvent.spans?.find(({ op }) => op === 'http.client'); + expect(rootSpan.name).toBe('rootSpan'); expect(requestSpan).toMatchObject({ - description: 'GET http://sentry-test-site.example/2', - parent_span_id: transactionEvent.contexts?.trace?.span_id, + name: 'GET sentry-test-site.example', + parent_span_id: rootSpan.span_id, span_id: expect.stringMatching(/[a-f\d]{16}/), start_timestamp: expect.any(Number), - timestamp: expect.any(Number), - trace_id: transactionEvent.contexts?.trace?.trace_id, - data: expect.objectContaining({ - 'http.request.method': 'GET', - 'url.full': 'http://sentry-test-site.example/2?id=1#fragment', - 'url.query': 'id=1', - 'url.fragment': 'fragment', - 'http.response.status_code': 200, - 'http.response.body.size': 2, - 'sentry.op': 'http.client', - 'sentry.origin': 'auto.http.browser', - type: 'fetch', - 'server.address': 'sentry-test-site.example', + end_timestamp: expect.any(Number), + trace_id: rootSpan.trace_id, + attributes: expect.objectContaining({ + 'http.request.method': { type: 'string', value: 'GET' }, + 'url.full': { type: 'string', value: 'http://sentry-test-site.example/2?id=1#fragment' }, + 'url.query': { type: 'string', value: 'id=1' }, + 'url.fragment': { type: 'string', value: 'fragment' }, + 'http.response.status_code': { type: 'integer', value: 200 }, + 'http.response.body.size': { type: 'integer', value: 2 }, + 'sentry.op': { type: 'string', value: 'http.client' }, + 'sentry.origin': { type: 'string', value: 'auto.http.browser' }, + type: { type: 'string', value: 'fetch' }, + 'server.address': { type: 'string', value: 'sentry-test-site.example' }, }), }); }); @@ -140,32 +138,31 @@ sentryTest( await page.goto(url); - const txnPromise = waitForTransactionRequest(page); + const rootPromise = waitForStreamedSpan(page, span => span.is_segment && span.name === 'rootSpan'); + const requestPromise = waitForStreamedSpan(page, span => getSpanOp(span) === 'http.client'); await page.locator('#btnQueryFragmentSameOrigin').click(); - const transactionEvent = envelopeRequestParser(await txnPromise); - - expect(transactionEvent.transaction).toEqual('rootSpan'); + const [rootSpan, requestSpan] = await Promise.all([rootPromise, requestPromise]); - const requestSpan = transactionEvent.spans?.find(({ op }) => op === 'http.client'); + expect(rootSpan.name).toBe('rootSpan'); expect(requestSpan).toMatchObject({ - description: 'GET /api/users', - parent_span_id: transactionEvent.contexts?.trace?.span_id, + name: 'GET sentry-test.io', + parent_span_id: rootSpan.span_id, span_id: expect.stringMatching(/[a-f\d]{16}/), start_timestamp: expect.any(Number), - timestamp: expect.any(Number), - trace_id: transactionEvent.contexts?.trace?.trace_id, - data: expect.objectContaining({ - 'http.request.method': 'GET', - 'url.full': 'http://sentry-test.io/api/users?id=1#fragment', - 'url.query': 'id=1', - 'url.fragment': 'fragment', - 'http.response.status_code': 200, - 'http.response.body.size': 2, - 'sentry.op': 'http.client', - 'sentry.origin': 'auto.http.browser', - type: 'fetch', - 'server.address': 'sentry-test.io', + end_timestamp: expect.any(Number), + trace_id: rootSpan.trace_id, + attributes: expect.objectContaining({ + 'http.request.method': { type: 'string', value: 'GET' }, + 'url.full': { type: 'string', value: 'http://sentry-test.io/api/users?id=1#fragment' }, + 'url.query': { type: 'string', value: 'id=1' }, + 'url.fragment': { type: 'string', value: 'fragment' }, + 'http.response.status_code': { type: 'integer', value: 200 }, + 'http.response.body.size': { type: 'integer', value: 2 }, + 'sentry.op': { type: 'string', value: 'http.client' }, + 'sentry.origin': { type: 'string', value: 'auto.http.browser' }, + type: { type: 'string', value: 'fetch' }, + 'server.address': { type: 'string', value: 'sentry-test.io' }, }), }); }, diff --git a/dev-packages/browser-integration-tests/suites/tracing/request/fetch-trace-header-merging/init.js b/dev-packages/browser-integration-tests/suites/tracing/request/fetch-trace-header-merging/init.js index 65cfbab96ee2..b2280b70e307 100644 --- a/dev-packages/browser-integration-tests/suites/tracing/request/fetch-trace-header-merging/init.js +++ b/dev-packages/browser-integration-tests/suites/tracing/request/fetch-trace-header-merging/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()], tracePropagationTargets: ['http://sentry-test-site.example'], diff --git a/dev-packages/browser-integration-tests/suites/tracing/request/fetch-tracing-unsampled-propagateTraceparent/init.js b/dev-packages/browser-integration-tests/suites/tracing/request/fetch-tracing-unsampled-propagateTraceparent/init.js index d24ea3bd6ea5..fa09e056425e 100644 --- a/dev-packages/browser-integration-tests/suites/tracing/request/fetch-tracing-unsampled-propagateTraceparent/init.js +++ b/dev-packages/browser-integration-tests/suites/tracing/request/fetch-tracing-unsampled-propagateTraceparent/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()], tracePropagationTargets: ['http://sentry-test-site.example'], diff --git a/dev-packages/browser-integration-tests/suites/tracing/request/fetch-tracing-unsampled/init.js b/dev-packages/browser-integration-tests/suites/tracing/request/fetch-tracing-unsampled/init.js index 06a40a8e55ec..5e9c86105ae7 100644 --- a/dev-packages/browser-integration-tests/suites/tracing/request/fetch-tracing-unsampled/init.js +++ b/dev-packages/browser-integration-tests/suites/tracing/request/fetch-tracing-unsampled/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()], tracePropagationTargets: ['http://sentry-test-site.example'], diff --git a/dev-packages/browser-integration-tests/suites/tracing/request/fetch-tracing-without-performance-propagateTraceparent/init.js b/dev-packages/browser-integration-tests/suites/tracing/request/fetch-tracing-without-performance-propagateTraceparent/init.js index bc687b1606dd..8d3c1948d89d 100644 --- a/dev-packages/browser-integration-tests/suites/tracing/request/fetch-tracing-without-performance-propagateTraceparent/init.js +++ b/dev-packages/browser-integration-tests/suites/tracing/request/fetch-tracing-without-performance-propagateTraceparent/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()], tracePropagationTargets: ['http://sentry-test-site.example'], diff --git a/dev-packages/browser-integration-tests/suites/tracing/request/fetch-tracing-without-performance/init.js b/dev-packages/browser-integration-tests/suites/tracing/request/fetch-tracing-without-performance/init.js index 9e18155fa4aa..69d4c5781782 100644 --- a/dev-packages/browser-integration-tests/suites/tracing/request/fetch-tracing-without-performance/init.js +++ b/dev-packages/browser-integration-tests/suites/tracing/request/fetch-tracing-without-performance/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()], tracePropagationTargets: ['http://sentry-test-site.example'], diff --git a/dev-packages/browser-integration-tests/suites/tracing/request/fetch-streamed/init.js b/dev-packages/browser-integration-tests/suites/tracing/request/fetch-track-stream-performance/init.js similarity index 72% rename from dev-packages/browser-integration-tests/suites/tracing/request/fetch-streamed/init.js rename to dev-packages/browser-integration-tests/suites/tracing/request/fetch-track-stream-performance/init.js index c4c8791cf32c..4a965f288264 100644 --- a/dev-packages/browser-integration-tests/suites/tracing/request/fetch-streamed/init.js +++ b/dev-packages/browser-integration-tests/suites/tracing/request/fetch-track-stream-performance/init.js @@ -4,7 +4,7 @@ window.Sentry = Sentry; Sentry.init({ dsn: 'https://public@dsn.ingest.sentry.io/1337', - integrations: [Sentry.browserTracingIntegration(), Sentry.spanStreamingIntegration()], + integrations: [Sentry.browserTracingIntegration(), Sentry.fetchStreamPerformanceIntegration()], tracePropagationTargets: ['http://sentry-test-site.example'], tracesSampleRate: 1, autoSessionTracking: false, diff --git a/dev-packages/browser-integration-tests/suites/tracing/request/fetch-streamed-track-stream-performance/subject.js b/dev-packages/browser-integration-tests/suites/tracing/request/fetch-track-stream-performance/subject.js similarity index 100% rename from dev-packages/browser-integration-tests/suites/tracing/request/fetch-streamed-track-stream-performance/subject.js rename to dev-packages/browser-integration-tests/suites/tracing/request/fetch-track-stream-performance/subject.js diff --git a/dev-packages/browser-integration-tests/suites/tracing/request/fetch-streamed-track-stream-performance/test.ts b/dev-packages/browser-integration-tests/suites/tracing/request/fetch-track-stream-performance/test.ts similarity index 91% rename from dev-packages/browser-integration-tests/suites/tracing/request/fetch-streamed-track-stream-performance/test.ts rename to dev-packages/browser-integration-tests/suites/tracing/request/fetch-track-stream-performance/test.ts index 04f0261beff4..e2149b8d96bd 100644 --- a/dev-packages/browser-integration-tests/suites/tracing/request/fetch-streamed-track-stream-performance/test.ts +++ b/dev-packages/browser-integration-tests/suites/tracing/request/fetch-track-stream-performance/test.ts @@ -60,7 +60,9 @@ sentryTest( }), }); - expect(streamSpan.end_timestamp).toBeGreaterThan(streamSpan.start_timestamp); + expect(streamSpan.parent_span_id).toBe(requestSpan.parent_span_id); + expect(streamSpan.trace_id).toBe(requestSpan.trace_id); + expect(streamSpan.end_timestamp).toBeGreaterThanOrEqual(streamSpan.start_timestamp); } finally { server.close(); } diff --git a/dev-packages/browser-integration-tests/suites/tracing/request/fetch-with-custom-sentry-headers-propagateTraceparent/init.js b/dev-packages/browser-integration-tests/suites/tracing/request/fetch-with-custom-sentry-headers-propagateTraceparent/init.js index efcc9dde33fa..cb785bbf7fda 100644 --- a/dev-packages/browser-integration-tests/suites/tracing/request/fetch-with-custom-sentry-headers-propagateTraceparent/init.js +++ b/dev-packages/browser-integration-tests/suites/tracing/request/fetch-with-custom-sentry-headers-propagateTraceparent/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()], tracePropagationTargets: ['http://sentry-test-site.example'], diff --git a/dev-packages/browser-integration-tests/suites/tracing/request/fetch-with-custom-sentry-headers/init.js b/dev-packages/browser-integration-tests/suites/tracing/request/fetch-with-custom-sentry-headers/init.js index 65cfbab96ee2..b2280b70e307 100644 --- a/dev-packages/browser-integration-tests/suites/tracing/request/fetch-with-custom-sentry-headers/init.js +++ b/dev-packages/browser-integration-tests/suites/tracing/request/fetch-with-custom-sentry-headers/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()], tracePropagationTargets: ['http://sentry-test-site.example'], diff --git a/dev-packages/browser-integration-tests/suites/tracing/request/fetch-with-request/init.js b/dev-packages/browser-integration-tests/suites/tracing/request/fetch-with-request/init.js index 65cfbab96ee2..b2280b70e307 100644 --- a/dev-packages/browser-integration-tests/suites/tracing/request/fetch-with-request/init.js +++ b/dev-packages/browser-integration-tests/suites/tracing/request/fetch-with-request/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()], tracePropagationTargets: ['http://sentry-test-site.example'], diff --git a/dev-packages/browser-integration-tests/suites/tracing/request/xhr-relative-url-streamed/init.js b/dev-packages/browser-integration-tests/suites/tracing/request/fetch/init.js similarity index 79% rename from dev-packages/browser-integration-tests/suites/tracing/request/xhr-relative-url-streamed/init.js rename to dev-packages/browser-integration-tests/suites/tracing/request/fetch/init.js index 5ab240338c8c..439f60ae6dc4 100644 --- a/dev-packages/browser-integration-tests/suites/tracing/request/xhr-relative-url-streamed/init.js +++ b/dev-packages/browser-integration-tests/suites/tracing/request/fetch/init.js @@ -5,6 +5,7 @@ window.Sentry = Sentry; Sentry.init({ dsn: 'https://public@dsn.ingest.sentry.io/1337', integrations: [Sentry.browserTracingIntegration()], + tracePropagationTargets: ['http://sentry-test-site.example'], tracesSampleRate: 1, autoSessionTracking: false, }); diff --git a/dev-packages/browser-integration-tests/suites/tracing/request/fetch/test.ts b/dev-packages/browser-integration-tests/suites/tracing/request/fetch/test.ts index a177445bb980..7c6fc57cf95d 100644 --- a/dev-packages/browser-integration-tests/suites/tracing/request/fetch/test.ts +++ b/dev-packages/browser-integration-tests/suites/tracing/request/fetch/test.ts @@ -1,41 +1,47 @@ import { expect } from '@playwright/test'; import { sentryTest } from '../../../../utils/fixtures'; -import { - envelopeRequestParser, - shouldSkipTracingTest, - waitForTransactionRequestOnUrl, -} from '../../../../utils/helpers'; +import { shouldSkipTracingTest } from '../../../../utils/helpers'; +import { collectStreamedSpans, getSpanOp, waitForStreamedSpan } from '../../../../utils/spanUtils'; -sentryTest('should create spans for fetch requests', async ({ getLocalTestUrl, page }) => { - if (shouldSkipTracingTest()) { - sentryTest.skip(); - } +sentryTest('creates spans for fetch requests', async ({ getLocalTestUrl, page }) => { + sentryTest.skip(shouldSkipTracingTest()); await page.route('http://sentry-test-site.example/*', route => route.fulfill({ body: 'ok' })); const url = await getLocalTestUrl({ testDir: __dirname }); - const req = await waitForTransactionRequestOnUrl(page, url); - const tracingEvent = envelopeRequestParser(req); + const spans = collectStreamedSpans(page); + const pageloadPromise = waitForStreamedSpan(page, span => span.is_segment && getSpanOp(span) === 'pageload'); + + await page.goto(url); - const requestSpans = tracingEvent.spans?.filter(({ op }) => op === 'http.client'); + const pageloadSpan = await pageloadPromise; + await page.evaluate(() => (window as any).Sentry.flush()); + const allSpans = spans; + const requestSpans = allSpans + .filter(s => getSpanOp(s) === 'http.client') + .sort((a, b) => + (a.attributes!['url.full']!.value as string).localeCompare(b.attributes!['url.full']!.value as string), + ); expect(requestSpans).toHaveLength(3); - requestSpans?.forEach((span, index) => + requestSpans.forEach((span, index) => expect(span).toMatchObject({ - description: `GET http://sentry-test-site.example/${index}`, - parent_span_id: tracingEvent.contexts?.trace?.span_id, + // Streamed span names drop the high-cardinality URL path. + name: 'GET sentry-test-site.example', + parent_span_id: pageloadSpan?.span_id, span_id: expect.stringMatching(/[a-f\d]{16}/), start_timestamp: expect.any(Number), - timestamp: expect.any(Number), - trace_id: tracingEvent.contexts?.trace?.trace_id, - data: { - 'http.request.method': 'GET', - 'url.full': `http://sentry-test-site.example/${index}`, - 'server.address': 'sentry-test-site.example', - type: 'fetch', - }, + end_timestamp: expect.any(Number), + trace_id: pageloadSpan?.trace_id, + attributes: expect.objectContaining({ + 'http.request.method': { type: 'string', value: 'GET' }, + 'url.full': { type: 'string', value: `http://sentry-test-site.example/${index}` }, + 'url.domain': { type: 'string', value: 'sentry-test-site.example' }, + 'server.address': { type: 'string', value: 'sentry-test-site.example' }, + type: { type: 'string', value: 'fetch' }, + }), }), ); }); diff --git a/dev-packages/browser-integration-tests/suites/tracing/request/init.js b/dev-packages/browser-integration-tests/suites/tracing/request/init.js index 847df632b642..439f60ae6dc4 100644 --- a/dev-packages/browser-integration-tests/suites/tracing/request/init.js +++ b/dev-packages/browser-integration-tests/suites/tracing/request/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()], tracePropagationTargets: ['http://sentry-test-site.example'], diff --git a/dev-packages/browser-integration-tests/suites/tracing/request/xhr-data-url/init.js b/dev-packages/browser-integration-tests/suites/tracing/request/xhr-data-url/init.js index d1c5e9767aa2..5ab240338c8c 100644 --- a/dev-packages/browser-integration-tests/suites/tracing/request/xhr-data-url/init.js +++ b/dev-packages/browser-integration-tests/suites/tracing/request/xhr-data-url/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()], tracesSampleRate: 1, diff --git a/dev-packages/browser-integration-tests/suites/tracing/request/xhr-data-url/test.ts b/dev-packages/browser-integration-tests/suites/tracing/request/xhr-data-url/test.ts index 549c468c2e0b..199ade590012 100644 --- a/dev-packages/browser-integration-tests/suites/tracing/request/xhr-data-url/test.ts +++ b/dev-packages/browser-integration-tests/suites/tracing/request/xhr-data-url/test.ts @@ -1,29 +1,27 @@ import { expect } from '@playwright/test'; -import type { Event } from '@sentry/core'; import { sentryTest } from '../../../../utils/fixtures'; -import { getFirstSentryEnvelopeRequest, shouldSkipTracingTest } from '../../../../utils/helpers'; - -sentryTest('sanitizes data URLs in XHR span name and attributes', async ({ getLocalTestUrl, page }) => { - if (shouldSkipTracingTest()) { - sentryTest.skip(); - } +import { shouldSkipTracingTest } from '../../../../utils/helpers'; +import { collectStreamedSpans, getSpanOp, waitForStreamedSpan } from '../../../../utils/spanUtils'; +sentryTest('sanitizes data URLs in xhr span name and attributes', async ({ getLocalTestUrl, page }) => { + sentryTest.skip(shouldSkipTracingTest()); + const spans = collectStreamedSpans(page); + const pageloadPromise = waitForStreamedSpan(page, span => span.is_segment && getSpanOp(span) === 'pageload'); const url = await getLocalTestUrl({ testDir: __dirname }); - - const eventData = await getFirstSentryEnvelopeRequest(page, url); - const requestSpans = eventData.spans?.filter(({ op }) => op === 'http.client'); - - expect(requestSpans).toHaveLength(1); - - const span = requestSpans?.[0]; - - const sanitizedUrl = 'data:text/plain,base64,SGVsbG8gV2... [truncated]'; - expect(span?.description).toBe(`GET ${sanitizedUrl}`); - - expect(span?.data).toMatchObject({ - 'http.request.method': 'GET', - type: 'xhr', + await page.goto(url); + const pageload = await pageloadPromise; + await page.evaluate(() => (window as any).Sentry.flush()); + + const requests = spans.filter(span => getSpanOp(span) === 'http.client'); + expect(requests).toHaveLength(1); + const span = requests[0]; + expect(span.name).toBe('GET'); + expect(span.parent_span_id).toBe(pageload.span_id); + expect(span.trace_id).toBe(pageload.trace_id); + expect(span.attributes['http.request.method']).toEqual({ type: 'string', value: 'GET' }); + expect(span.attributes.type).toEqual({ type: 'string', value: 'xhr' }); + expect(span.attributes['url.full']).toEqual({ + type: 'string', + value: 'data:text/plain,base64,SGVsbG8gV2... [truncated]', }); - - expect(span?.data?.['url.full']).toBe(sanitizedUrl); }); diff --git a/dev-packages/browser-integration-tests/suites/tracing/request/xhr-no-tracing/init.js b/dev-packages/browser-integration-tests/suites/tracing/request/xhr-no-tracing/init.js index 2db7a75be0e6..3cbcaa4fe152 100644 --- a/dev-packages/browser-integration-tests/suites/tracing/request/xhr-no-tracing/init.js +++ b/dev-packages/browser-integration-tests/suites/tracing/request/xhr-no-tracing/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', tracePropagationTargets: ['http://sentry-test-site.example'], }); diff --git a/dev-packages/browser-integration-tests/suites/tracing/request/xhr-propagateTraceparent/init.js b/dev-packages/browser-integration-tests/suites/tracing/request/xhr-propagateTraceparent/init.js index efcc9dde33fa..cb785bbf7fda 100644 --- a/dev-packages/browser-integration-tests/suites/tracing/request/xhr-propagateTraceparent/init.js +++ b/dev-packages/browser-integration-tests/suites/tracing/request/xhr-propagateTraceparent/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()], tracePropagationTargets: ['http://sentry-test-site.example'], diff --git a/dev-packages/browser-integration-tests/suites/tracing/request/xhr-relative-url-streamed/subject.js b/dev-packages/browser-integration-tests/suites/tracing/request/xhr-relative-url-streamed/subject.js deleted file mode 100644 index 75155522ab50..000000000000 --- a/dev-packages/browser-integration-tests/suites/tracing/request/xhr-relative-url-streamed/subject.js +++ /dev/null @@ -1,11 +0,0 @@ -const xhr_1 = new XMLHttpRequest(); -xhr_1.open('GET', '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/test-req/0'); -xhr_1.send(); - -const xhr_2 = new XMLHttpRequest(); -xhr_2.open('GET', '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/test-req/1'); -xhr_2.send(); - -const xhr_3 = new XMLHttpRequest(); -xhr_3.open('GET', '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/test-req/2'); -xhr_3.send(); diff --git a/dev-packages/browser-integration-tests/suites/tracing/request/xhr-relative-url-streamed/test.ts b/dev-packages/browser-integration-tests/suites/tracing/request/xhr-relative-url-streamed/test.ts deleted file mode 100644 index 31ab73597b88..000000000000 --- a/dev-packages/browser-integration-tests/suites/tracing/request/xhr-relative-url-streamed/test.ts +++ /dev/null @@ -1,39 +0,0 @@ -import { expect } from '@playwright/test'; -import { sentryTest, TEST_HOST } from '../../../../utils/fixtures'; -import { shouldSkipTracingTest } from '../../../../utils/helpers'; -import { getSpanOp, waitForStreamedSpans } from '../../../../utils/spanUtils'; - -sentryTest('names spans for relative XHR requests after the page domain', async ({ getLocalTestUrl, page }) => { - sentryTest.skip(shouldSkipTracingTest()); - - const url = await getLocalTestUrl({ testDir: __dirname }); - - const spansPromise = waitForStreamedSpans( - page, - spans => spans.filter(s => getSpanOp(s) === 'http.client').length >= 3, - ); - - await page.goto(url); - - const requestSpans = (await spansPromise) - .filter(s => getSpanOp(s) === 'http.client') - .sort((a, b) => - (a.attributes!['url.full']!.value as string).localeCompare(b.attributes!['url.full']!.value as string), - ); - - expect(requestSpans).toHaveLength(3); - - requestSpans.forEach((span, index) => - expect(span).toMatchObject({ - // A relative URL has no domain of its own, so it resolves against the page origin. - name: 'GET sentry-test.io', - attributes: expect.objectContaining({ - 'http.request.method': { type: 'string', value: 'GET' }, - 'url.full': { type: 'string', value: `${TEST_HOST}/test-req/${index}` }, - 'url.domain': { type: 'string', value: 'sentry-test.io' }, - 'server.address': { type: 'string', value: 'sentry-test.io' }, - type: { type: 'string', value: 'xhr' }, - }), - }), - ); -}); diff --git a/dev-packages/browser-integration-tests/suites/tracing/request/xhr-relative-url/init.js b/dev-packages/browser-integration-tests/suites/tracing/request/xhr-relative-url/init.js index bb6d4918d1bf..5ab240338c8c 100644 --- a/dev-packages/browser-integration-tests/suites/tracing/request/xhr-relative-url/init.js +++ b/dev-packages/browser-integration-tests/suites/tracing/request/xhr-relative-url/init.js @@ -3,8 +3,8 @@ import * as Sentry from '@sentry/browser'; window.Sentry = Sentry; Sentry.init({ - traceLifecycle: 'static', dsn: 'https://public@dsn.ingest.sentry.io/1337', integrations: [Sentry.browserTracingIntegration()], tracesSampleRate: 1, + autoSessionTracking: false, }); diff --git a/dev-packages/browser-integration-tests/suites/tracing/request/xhr-relative-url/test.ts b/dev-packages/browser-integration-tests/suites/tracing/request/xhr-relative-url/test.ts index 110df467837f..960030218253 100644 --- a/dev-packages/browser-integration-tests/suites/tracing/request/xhr-relative-url/test.ts +++ b/dev-packages/browser-integration-tests/suites/tracing/request/xhr-relative-url/test.ts @@ -1,38 +1,41 @@ import { expect } from '@playwright/test'; import { sentryTest, TEST_HOST } from '../../../../utils/fixtures'; -import { - envelopeRequestParser, - shouldSkipTracingTest, - waitForTransactionRequestOnUrl, -} from '../../../../utils/helpers'; +import { shouldSkipTracingTest } from '../../../../utils/helpers'; +import { collectStreamedSpans, getSpanOp, waitForStreamedSpan } from '../../../../utils/spanUtils'; -sentryTest('should create spans for xhr requests', async ({ getLocalTestUrl, page }) => { - if (shouldSkipTracingTest()) { - sentryTest.skip(); - } +sentryTest('names spans for relative XHR requests after the page domain', async ({ getLocalTestUrl, page }) => { + sentryTest.skip(shouldSkipTracingTest()); const url = await getLocalTestUrl({ testDir: __dirname }); - const req = await waitForTransactionRequestOnUrl(page, url); - const tracingEvent = envelopeRequestParser(req); - const requestSpans = tracingEvent.spans?.filter(({ op }) => op === 'http.client'); + const spans = collectStreamedSpans(page); + const pageloadPromise = waitForStreamedSpan(page, span => span.is_segment && getSpanOp(span) === 'pageload'); + + await page.goto(url); + + const pageloadSpan = await pageloadPromise; + await page.evaluate(() => (window as any).Sentry.flush()); + const requestSpans = spans + .filter(s => getSpanOp(s) === 'http.client') + .sort((a, b) => + (a.attributes!['url.full']!.value as string).localeCompare(b.attributes!['url.full']!.value as string), + ); expect(requestSpans).toHaveLength(3); - requestSpans?.forEach((span, index) => + requestSpans.forEach((span, index) => expect(span).toMatchObject({ - description: `GET /test-req/${index}`, - parent_span_id: tracingEvent.contexts?.trace?.span_id, - span_id: expect.stringMatching(/[a-f\d]{16}/), - start_timestamp: expect.any(Number), - timestamp: expect.any(Number), - trace_id: tracingEvent.contexts?.trace?.trace_id, - data: { - 'http.request.method': 'GET', - 'url.full': `${TEST_HOST}/test-req/${index}`, - 'server.address': 'sentry-test.io', - type: 'xhr', - }, + // A relative URL has no domain of its own, so it resolves against the page origin. + name: 'GET sentry-test.io', + parent_span_id: pageloadSpan.span_id, + trace_id: pageloadSpan.trace_id, + attributes: expect.objectContaining({ + 'http.request.method': { type: 'string', value: 'GET' }, + 'url.full': { type: 'string', value: `${TEST_HOST}/test-req/${index}` }, + 'url.domain': { type: 'string', value: 'sentry-test.io' }, + 'server.address': { type: 'string', value: 'sentry-test.io' }, + type: { type: 'string', value: 'xhr' }, + }), }), ); }); diff --git a/dev-packages/browser-integration-tests/suites/tracing/request/xhr-streamed/subject.js b/dev-packages/browser-integration-tests/suites/tracing/request/xhr-streamed/subject.js deleted file mode 100644 index 9c584bf743cb..000000000000 --- a/dev-packages/browser-integration-tests/suites/tracing/request/xhr-streamed/subject.js +++ /dev/null @@ -1,12 +0,0 @@ -const xhr_1 = new XMLHttpRequest(); -xhr_1.open('GET', 'http://sentry-test-site.example/0'); -xhr_1.send(); - -const xhr_2 = new XMLHttpRequest(); -xhr_2.open('GET', 'http://sentry-test-site.example/1'); -xhr_2.setRequestHeader('X-Test-Header', 'existing-header'); -xhr_2.send(); - -const xhr_3 = new XMLHttpRequest(); -xhr_3.open('GET', 'http://sentry-test-site.example/2'); -xhr_3.send(); diff --git a/dev-packages/browser-integration-tests/suites/tracing/request/xhr-streamed/test.ts b/dev-packages/browser-integration-tests/suites/tracing/request/xhr-streamed/test.ts deleted file mode 100644 index 2fd96eb8af58..000000000000 --- a/dev-packages/browser-integration-tests/suites/tracing/request/xhr-streamed/test.ts +++ /dev/null @@ -1,48 +0,0 @@ -import { expect } from '@playwright/test'; -import { sentryTest } from '../../../../utils/fixtures'; -import { shouldSkipTracingTest } from '../../../../utils/helpers'; -import { getSpanOp, waitForStreamedSpans } from '../../../../utils/spanUtils'; - -sentryTest('creates spans for XHR requests', async ({ getLocalTestUrl, page }) => { - sentryTest.skip(shouldSkipTracingTest()); - - await page.route('http://sentry-test-site.example/*', route => route.fulfill({ body: 'ok' })); - - const url = await getLocalTestUrl({ testDir: __dirname }); - - const spansPromise = waitForStreamedSpans( - page, - spans => spans.filter(s => getSpanOp(s) === 'http.client').length >= 3, - ); - - await page.goto(url); - - const allSpans = await spansPromise; - const pageloadSpan = allSpans.find(s => getSpanOp(s) === 'pageload'); - const requestSpans = allSpans - .filter(s => getSpanOp(s) === 'http.client') - .sort((a, b) => - (a.attributes!['url.full']!.value as string).localeCompare(b.attributes!['url.full']!.value as string), - ); - - expect(requestSpans).toHaveLength(3); - - requestSpans.forEach((span, index) => - expect(span).toMatchObject({ - // Streamed span names drop the high-cardinality URL path. - name: 'GET sentry-test-site.example', - parent_span_id: pageloadSpan?.span_id, - span_id: expect.stringMatching(/[a-f\d]{16}/), - start_timestamp: expect.any(Number), - end_timestamp: expect.any(Number), - trace_id: pageloadSpan?.trace_id, - attributes: expect.objectContaining({ - 'http.request.method': { type: 'string', value: 'GET' }, - 'url.full': { type: 'string', value: `http://sentry-test-site.example/${index}` }, - 'url.domain': { type: 'string', value: 'sentry-test-site.example' }, - 'server.address': { type: 'string', value: 'sentry-test-site.example' }, - type: { type: 'string', value: 'xhr' }, - }), - }), - ); -}); diff --git a/dev-packages/browser-integration-tests/suites/tracing/request/xhr-strip-query-and-fragment/init.js b/dev-packages/browser-integration-tests/suites/tracing/request/xhr-strip-query-and-fragment/init.js index d334f6a7d2a6..de6b87574482 100644 --- a/dev-packages/browser-integration-tests/suites/tracing/request/xhr-strip-query-and-fragment/init.js +++ b/dev-packages/browser-integration-tests/suites/tracing/request/xhr-strip-query-and-fragment/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({ instrumentPageLoad: false, instrumentNavigation: false })], tracePropagationTargets: ['http://sentry-test-site.example'], diff --git a/dev-packages/browser-integration-tests/suites/tracing/request/xhr-strip-query-and-fragment/test.ts b/dev-packages/browser-integration-tests/suites/tracing/request/xhr-strip-query-and-fragment/test.ts index f5445029b4c3..9404bbbd65a2 100644 --- a/dev-packages/browser-integration-tests/suites/tracing/request/xhr-strip-query-and-fragment/test.ts +++ b/dev-packages/browser-integration-tests/suites/tracing/request/xhr-strip-query-and-fragment/test.ts @@ -1,6 +1,7 @@ import { expect } from '@playwright/test'; import { sentryTest } from '../../../../utils/fixtures'; -import { envelopeRequestParser, shouldSkipTracingTest, waitForTransactionRequest } from '../../../../utils/helpers'; +import { shouldSkipTracingTest } from '../../../../utils/helpers'; +import { getSpanOp, waitForStreamedSpan } from '../../../../utils/spanUtils'; sentryTest('strips query params in XHR request spans', async ({ getLocalTestUrl, page }) => { if (shouldSkipTracingTest()) { @@ -13,34 +14,33 @@ sentryTest('strips query params in XHR request spans', async ({ getLocalTestUrl, await page.goto(url); - const txnPromise = waitForTransactionRequest(page); + const rootPromise = waitForStreamedSpan(page, span => span.is_segment && span.name === 'rootSpan'); + const requestPromise = waitForStreamedSpan(page, span => getSpanOp(span) === 'http.client'); await page.locator('#btnQuery').click(); - const transactionEvent = envelopeRequestParser(await txnPromise); + const [rootSpan, requestSpan] = await Promise.all([rootPromise, requestPromise]); - expect(transactionEvent.transaction).toEqual('rootSpan'); - - const requestSpan = transactionEvent.spans?.find(({ op }) => op === 'http.client'); + expect(rootSpan.name).toBe('rootSpan'); expect(requestSpan).toMatchObject({ - description: 'GET http://sentry-test-site.example/0', - parent_span_id: transactionEvent.contexts?.trace?.span_id, + name: 'GET sentry-test-site.example', + parent_span_id: rootSpan.span_id, span_id: expect.stringMatching(/[a-f\d]{16}/), start_timestamp: expect.any(Number), - timestamp: expect.any(Number), - trace_id: transactionEvent.contexts?.trace?.trace_id, - data: expect.objectContaining({ - 'http.request.method': 'GET', - 'url.full': 'http://sentry-test-site.example/0?id=123;page=5', - 'url.query': 'id=123;page=5', - 'http.response.status_code': 200, - 'sentry.op': 'http.client', - 'sentry.origin': 'auto.http.browser', - type: 'xhr', - 'server.address': 'sentry-test-site.example', + end_timestamp: expect.any(Number), + trace_id: rootSpan.trace_id, + attributes: expect.objectContaining({ + 'http.request.method': { type: 'string', value: 'GET' }, + 'url.full': { type: 'string', value: 'http://sentry-test-site.example/0?id=123;page=5' }, + 'url.query': { type: 'string', value: 'id=123;page=5' }, + 'http.response.status_code': { type: 'integer', value: 200 }, + 'sentry.op': { type: 'string', value: 'http.client' }, + 'sentry.origin': { type: 'string', value: 'auto.http.browser' }, + type: { type: 'string', value: 'xhr' }, + 'server.address': { type: 'string', value: 'sentry-test-site.example' }, }), }); - expect(requestSpan?.data).not.toHaveProperty('url.fragment'); + expect(requestSpan?.attributes).not.toHaveProperty('url.fragment'); }); sentryTest('strips hash fragment in XHR request spans', async ({ getLocalTestUrl, page }) => { @@ -54,34 +54,33 @@ sentryTest('strips hash fragment in XHR request spans', async ({ getLocalTestUrl await page.goto(url); - const txnPromise = waitForTransactionRequest(page); + const rootPromise = waitForStreamedSpan(page, span => span.is_segment && span.name === 'rootSpan'); + const requestPromise = waitForStreamedSpan(page, span => getSpanOp(span) === 'http.client'); await page.locator('#btnFragment').click(); - const transactionEvent = envelopeRequestParser(await txnPromise); - - expect(transactionEvent.transaction).toEqual('rootSpan'); + const [rootSpan, requestSpan] = await Promise.all([rootPromise, requestPromise]); - const requestSpan = transactionEvent.spans?.find(({ op }) => op === 'http.client'); + expect(rootSpan.name).toBe('rootSpan'); expect(requestSpan).toMatchObject({ - description: 'GET http://sentry-test-site.example/1', - parent_span_id: transactionEvent.contexts?.trace?.span_id, + name: 'GET sentry-test-site.example', + parent_span_id: rootSpan.span_id, span_id: expect.stringMatching(/[a-f\d]{16}/), start_timestamp: expect.any(Number), - timestamp: expect.any(Number), - trace_id: transactionEvent.contexts?.trace?.trace_id, - data: expect.objectContaining({ - 'http.request.method': 'GET', - 'url.full': 'http://sentry-test-site.example/1#fragment', - 'url.fragment': 'fragment', - 'http.response.status_code': 200, - 'sentry.op': 'http.client', - 'sentry.origin': 'auto.http.browser', - type: 'xhr', - 'server.address': 'sentry-test-site.example', + end_timestamp: expect.any(Number), + trace_id: rootSpan.trace_id, + attributes: expect.objectContaining({ + 'http.request.method': { type: 'string', value: 'GET' }, + 'url.full': { type: 'string', value: 'http://sentry-test-site.example/1#fragment' }, + 'url.fragment': { type: 'string', value: 'fragment' }, + 'http.response.status_code': { type: 'integer', value: 200 }, + 'sentry.op': { type: 'string', value: 'http.client' }, + 'sentry.origin': { type: 'string', value: 'auto.http.browser' }, + type: { type: 'string', value: 'xhr' }, + 'server.address': { type: 'string', value: 'sentry-test-site.example' }, }), }); - expect(requestSpan?.data).not.toHaveProperty('url.query'); + expect(requestSpan?.attributes).not.toHaveProperty('url.query'); }); sentryTest('strips hash fragment and query params in XHR request spans', async ({ getLocalTestUrl, page }) => { @@ -95,31 +94,30 @@ sentryTest('strips hash fragment and query params in XHR request spans', async ( await page.goto(url); - const txnPromise = waitForTransactionRequest(page); + const rootPromise = waitForStreamedSpan(page, span => span.is_segment && span.name === 'rootSpan'); + const requestPromise = waitForStreamedSpan(page, span => getSpanOp(span) === 'http.client'); await page.locator('#btnQueryFragment').click(); - const transactionEvent = envelopeRequestParser(await txnPromise); + const [rootSpan, requestSpan] = await Promise.all([rootPromise, requestPromise]); - expect(transactionEvent.transaction).toEqual('rootSpan'); - - const requestSpan = transactionEvent.spans?.find(({ op }) => op === 'http.client'); + expect(rootSpan.name).toBe('rootSpan'); expect(requestSpan).toMatchObject({ - description: 'GET http://sentry-test-site.example/2', - parent_span_id: transactionEvent.contexts?.trace?.span_id, + name: 'GET sentry-test-site.example', + parent_span_id: rootSpan.span_id, span_id: expect.stringMatching(/[a-f\d]{16}/), start_timestamp: expect.any(Number), - timestamp: expect.any(Number), - trace_id: transactionEvent.contexts?.trace?.trace_id, - data: expect.objectContaining({ - 'http.request.method': 'GET', - 'url.full': 'http://sentry-test-site.example/2?id=1#fragment', - 'url.query': 'id=1', - 'url.fragment': 'fragment', - 'http.response.status_code': 200, - 'sentry.op': 'http.client', - 'sentry.origin': 'auto.http.browser', - type: 'xhr', - 'server.address': 'sentry-test-site.example', + end_timestamp: expect.any(Number), + trace_id: rootSpan.trace_id, + attributes: expect.objectContaining({ + 'http.request.method': { type: 'string', value: 'GET' }, + 'url.full': { type: 'string', value: 'http://sentry-test-site.example/2?id=1#fragment' }, + 'url.query': { type: 'string', value: 'id=1' }, + 'url.fragment': { type: 'string', value: 'fragment' }, + 'http.response.status_code': { type: 'integer', value: 200 }, + 'sentry.op': { type: 'string', value: 'http.client' }, + 'sentry.origin': { type: 'string', value: 'auto.http.browser' }, + type: { type: 'string', value: 'xhr' }, + 'server.address': { type: 'string', value: 'sentry-test-site.example' }, }), }); }); @@ -137,31 +135,30 @@ sentryTest( await page.goto(url); - const txnPromise = waitForTransactionRequest(page); + const rootPromise = waitForStreamedSpan(page, span => span.is_segment && span.name === 'rootSpan'); + const requestPromise = waitForStreamedSpan(page, span => getSpanOp(span) === 'http.client'); await page.locator('#btnQueryFragmentSameOrigin').click(); - const transactionEvent = envelopeRequestParser(await txnPromise); - - expect(transactionEvent.transaction).toEqual('rootSpan'); + const [rootSpan, requestSpan] = await Promise.all([rootPromise, requestPromise]); - const requestSpan = transactionEvent.spans?.find(({ op }) => op === 'http.client'); + expect(rootSpan.name).toBe('rootSpan'); expect(requestSpan).toMatchObject({ - description: 'GET /api/users', - parent_span_id: transactionEvent.contexts?.trace?.span_id, + name: 'GET sentry-test.io', + parent_span_id: rootSpan.span_id, span_id: expect.stringMatching(/[a-f\d]{16}/), start_timestamp: expect.any(Number), - timestamp: expect.any(Number), - trace_id: transactionEvent.contexts?.trace?.trace_id, - data: expect.objectContaining({ - 'http.request.method': 'GET', - 'url.full': 'http://sentry-test.io/api/users?id=1#fragment', - 'url.query': 'id=1', - 'url.fragment': 'fragment', - 'http.response.status_code': 200, - 'sentry.op': 'http.client', - 'sentry.origin': 'auto.http.browser', - type: 'xhr', - 'server.address': 'sentry-test.io', + end_timestamp: expect.any(Number), + trace_id: rootSpan.trace_id, + attributes: expect.objectContaining({ + 'http.request.method': { type: 'string', value: 'GET' }, + 'url.full': { type: 'string', value: 'http://sentry-test.io/api/users?id=1#fragment' }, + 'url.query': { type: 'string', value: 'id=1' }, + 'url.fragment': { type: 'string', value: 'fragment' }, + 'http.response.status_code': { type: 'integer', value: 200 }, + 'sentry.op': { type: 'string', value: 'http.client' }, + 'sentry.origin': { type: 'string', value: 'auto.http.browser' }, + type: { type: 'string', value: 'xhr' }, + 'server.address': { type: 'string', value: 'sentry-test.io' }, }), }); }, diff --git a/dev-packages/browser-integration-tests/suites/tracing/request/xhr-tracing-unsampled-propagateTraceparent/init.js b/dev-packages/browser-integration-tests/suites/tracing/request/xhr-tracing-unsampled-propagateTraceparent/init.js index d24ea3bd6ea5..fa09e056425e 100644 --- a/dev-packages/browser-integration-tests/suites/tracing/request/xhr-tracing-unsampled-propagateTraceparent/init.js +++ b/dev-packages/browser-integration-tests/suites/tracing/request/xhr-tracing-unsampled-propagateTraceparent/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()], tracePropagationTargets: ['http://sentry-test-site.example'], diff --git a/dev-packages/browser-integration-tests/suites/tracing/request/xhr-tracing-unsampled/init.js b/dev-packages/browser-integration-tests/suites/tracing/request/xhr-tracing-unsampled/init.js index 06a40a8e55ec..5e9c86105ae7 100644 --- a/dev-packages/browser-integration-tests/suites/tracing/request/xhr-tracing-unsampled/init.js +++ b/dev-packages/browser-integration-tests/suites/tracing/request/xhr-tracing-unsampled/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()], tracePropagationTargets: ['http://sentry-test-site.example'], diff --git a/dev-packages/browser-integration-tests/suites/tracing/request/xhr-tracing-without-performance-propagateTraceparent/init.js b/dev-packages/browser-integration-tests/suites/tracing/request/xhr-tracing-without-performance-propagateTraceparent/init.js index c2beb16cffc6..db749bf038e0 100644 --- a/dev-packages/browser-integration-tests/suites/tracing/request/xhr-tracing-without-performance-propagateTraceparent/init.js +++ b/dev-packages/browser-integration-tests/suites/tracing/request/xhr-tracing-without-performance-propagateTraceparent/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()], tracePropagationTargets: ['http://sentry-test-site.example'], diff --git a/dev-packages/browser-integration-tests/suites/tracing/request/xhr-tracing-without-performance/init.js b/dev-packages/browser-integration-tests/suites/tracing/request/xhr-tracing-without-performance/init.js index b48b09d10f9b..11f91541bb18 100644 --- a/dev-packages/browser-integration-tests/suites/tracing/request/xhr-tracing-without-performance/init.js +++ b/dev-packages/browser-integration-tests/suites/tracing/request/xhr-tracing-without-performance/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()], tracePropagationTargets: ['http://sentry-test-site.example'], diff --git a/dev-packages/browser-integration-tests/suites/tracing/request/xhr-with-custom-sentry-headers-propagateTraceparent/init.js b/dev-packages/browser-integration-tests/suites/tracing/request/xhr-with-custom-sentry-headers-propagateTraceparent/init.js index efcc9dde33fa..cb785bbf7fda 100644 --- a/dev-packages/browser-integration-tests/suites/tracing/request/xhr-with-custom-sentry-headers-propagateTraceparent/init.js +++ b/dev-packages/browser-integration-tests/suites/tracing/request/xhr-with-custom-sentry-headers-propagateTraceparent/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()], tracePropagationTargets: ['http://sentry-test-site.example'], diff --git a/dev-packages/browser-integration-tests/suites/tracing/request/xhr-streamed/init.js b/dev-packages/browser-integration-tests/suites/tracing/request/xhr/init.js similarity index 74% rename from dev-packages/browser-integration-tests/suites/tracing/request/xhr-streamed/init.js rename to dev-packages/browser-integration-tests/suites/tracing/request/xhr/init.js index c4c8791cf32c..439f60ae6dc4 100644 --- a/dev-packages/browser-integration-tests/suites/tracing/request/xhr-streamed/init.js +++ b/dev-packages/browser-integration-tests/suites/tracing/request/xhr/init.js @@ -4,7 +4,7 @@ window.Sentry = Sentry; Sentry.init({ dsn: 'https://public@dsn.ingest.sentry.io/1337', - integrations: [Sentry.browserTracingIntegration(), Sentry.spanStreamingIntegration()], + integrations: [Sentry.browserTracingIntegration()], tracePropagationTargets: ['http://sentry-test-site.example'], tracesSampleRate: 1, autoSessionTracking: false, diff --git a/dev-packages/browser-integration-tests/suites/tracing/request/xhr/test.ts b/dev-packages/browser-integration-tests/suites/tracing/request/xhr/test.ts index 971cb0f5c030..980188468e43 100644 --- a/dev-packages/browser-integration-tests/suites/tracing/request/xhr/test.ts +++ b/dev-packages/browser-integration-tests/suites/tracing/request/xhr/test.ts @@ -1,36 +1,47 @@ import { expect } from '@playwright/test'; -import type { Event } from '@sentry/core'; import { sentryTest } from '../../../../utils/fixtures'; -import { getFirstSentryEnvelopeRequest, shouldSkipTracingTest } from '../../../../utils/helpers'; +import { shouldSkipTracingTest } from '../../../../utils/helpers'; +import { collectStreamedSpans, getSpanOp, waitForStreamedSpan } from '../../../../utils/spanUtils'; -sentryTest('should create spans for XHR requests', async ({ getLocalTestUrl, page }) => { - if (shouldSkipTracingTest()) { - sentryTest.skip(); - } +sentryTest('creates spans for XHR requests', async ({ getLocalTestUrl, page }) => { + sentryTest.skip(shouldSkipTracingTest()); await page.route('http://sentry-test-site.example/*', route => route.fulfill({ body: 'ok' })); const url = await getLocalTestUrl({ testDir: __dirname }); - const eventData = await getFirstSentryEnvelopeRequest(page, url); - const requestSpans = eventData.spans?.filter(({ op }) => op === 'http.client'); + const spans = collectStreamedSpans(page); + const pageloadPromise = waitForStreamedSpan(page, span => span.is_segment && getSpanOp(span) === 'pageload'); + + await page.goto(url); + + const pageloadSpan = await pageloadPromise; + await page.evaluate(() => (window as any).Sentry.flush()); + const allSpans = spans; + const requestSpans = allSpans + .filter(s => getSpanOp(s) === 'http.client') + .sort((a, b) => + (a.attributes!['url.full']!.value as string).localeCompare(b.attributes!['url.full']!.value as string), + ); expect(requestSpans).toHaveLength(3); - requestSpans?.forEach((span, index) => + requestSpans.forEach((span, index) => expect(span).toMatchObject({ - description: `GET http://sentry-test-site.example/${index}`, - parent_span_id: eventData.contexts?.trace?.span_id, + // Streamed span names drop the high-cardinality URL path. + name: 'GET sentry-test-site.example', + parent_span_id: pageloadSpan?.span_id, span_id: expect.stringMatching(/[a-f\d]{16}/), start_timestamp: expect.any(Number), - timestamp: expect.any(Number), - trace_id: eventData.contexts?.trace?.trace_id, - data: { - 'http.request.method': 'GET', - 'url.full': `http://sentry-test-site.example/${index}`, - 'server.address': 'sentry-test-site.example', - type: 'xhr', - }, + end_timestamp: expect.any(Number), + trace_id: pageloadSpan?.trace_id, + attributes: expect.objectContaining({ + 'http.request.method': { type: 'string', value: 'GET' }, + 'url.full': { type: 'string', value: `http://sentry-test-site.example/${index}` }, + 'url.domain': { type: 'string', value: 'sentry-test-site.example' }, + 'server.address': { type: 'string', value: 'sentry-test-site.example' }, + type: { type: 'string', value: 'xhr' }, + }), }), ); }); diff --git a/dev-packages/browser-integration-tests/suites/tracing/user-timing/disabled/test.ts b/dev-packages/browser-integration-tests/suites/tracing/user-timing/disabled/test.ts index 8d69125cbd1c..c17f621b7cde 100644 --- a/dev-packages/browser-integration-tests/suites/tracing/user-timing/disabled/test.ts +++ b/dev-packages/browser-integration-tests/suites/tracing/user-timing/disabled/test.ts @@ -1,17 +1,19 @@ import { expect } from '@playwright/test'; import { sentryTest } from '../../../../utils/fixtures'; import { shouldSkipTracingTest } from '../../../../utils/helpers'; -import { getSpanOp, waitForStreamedSpans } from '../../../../utils/spanUtils'; +import { collectStreamedSpans, getSpanOp, waitForStreamedSpan } from '../../../../utils/spanUtils'; sentryTest('does not capture mark and measure spans by default', async ({ getLocalTestUrl, page }) => { sentryTest.skip(shouldSkipTracingTest()); const url = await getLocalTestUrl({ testDir: __dirname }); - const spansPromise = waitForStreamedSpans(page, spans => spans.some(span => getSpanOp(span) === 'pageload')); + const spans = collectStreamedSpans(page); + const pageloadPromise = waitForStreamedSpan(page, span => span.is_segment && getSpanOp(span) === 'pageload'); await page.goto(url); - const spans = await spansPromise; + await pageloadPromise; + await page.evaluate(() => (window as any).Sentry.flush()); 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/pageload-measure-spans-domexception-details/init.js b/dev-packages/browser-integration-tests/suites/tracing/user-timing/pageload-measure-spans-domexception-details/init.js index d96c6f65b682..d82045937810 100644 --- a/dev-packages/browser-integration-tests/suites/tracing/user-timing/pageload-measure-spans-domexception-details/init.js +++ b/dev-packages/browser-integration-tests/suites/tracing/user-timing/pageload-measure-spans-domexception-details/init.js @@ -22,7 +22,6 @@ Object.defineProperty(measure, 'detail', { window.Sentry = Sentry; Sentry.init({ - traceLifecycle: 'static', dsn: 'https://public@dsn.ingest.sentry.io/1337', integrations: [Sentry.browserTracingIntegration(), userTimingIntegration()], tracesSampleRate: 1, diff --git a/dev-packages/browser-integration-tests/suites/tracing/user-timing/pageload-measure-spans-domexception-details/test.ts b/dev-packages/browser-integration-tests/suites/tracing/user-timing/pageload-measure-spans-domexception-details/test.ts index 7a39e97420e3..6d4c11e930c3 100644 --- a/dev-packages/browser-integration-tests/suites/tracing/user-timing/pageload-measure-spans-domexception-details/test.ts +++ b/dev-packages/browser-integration-tests/suites/tracing/user-timing/pageload-measure-spans-domexception-details/test.ts @@ -1,65 +1,39 @@ import { expect } from '@playwright/test'; -import type { Event } from '@sentry/core'; import { sentryTest } from '../../../../utils/fixtures'; -import { getFirstSentryEnvelopeRequest, shouldSkipTracingTest } from '../../../../utils/helpers'; - -// This is a regression test for https://github.com/getsentry/sentry-javascript/issues/16347 +import { shouldSkipTracingTest } from '../../../../utils/helpers'; +import { collectStreamedSpans, getSpanOp, waitForStreamedSpan } from '../../../../utils/spanUtils'; sentryTest( - 'should handle permission denial gracefully and still create measure spans', + 'captures measure details when another measure denies access', async ({ getLocalTestUrl, page, browserName }) => { - // Skip test on webkit because we can't validate the detail in the browser - if (shouldSkipTracingTest() || browserName === 'webkit') { - sentryTest.skip(); - } - + sentryTest.skip(shouldSkipTracingTest() || browserName === 'webkit'); + const spans = collectStreamedSpans(page); + const pageloadPromise = waitForStreamedSpan(page, span => span.is_segment && getSpanOp(span) === 'pageload'); const url = await getLocalTestUrl({ testDir: __dirname }); - const eventData = await getFirstSentryEnvelopeRequest(page, url); - - // Find all measure spans - const measureSpans = eventData.spans?.filter(({ op }) => op === 'measure'); - expect(measureSpans?.length).toBe(3); // All three measures should create spans - - // Test 1: Verify the restricted-test-measure span exists but has no detail - const restrictedMeasure = measureSpans?.find(span => span.description === 'restricted-test-measure'); - expect(restrictedMeasure).toBeDefined(); - expect(restrictedMeasure?.data).toMatchObject({ - 'sentry.op': 'measure', - 'sentry.origin': 'auto.browser.user_timing.measure', + await page.goto(url); + const pageload = await pageloadPromise; + await page.evaluate(() => (window as any).Sentry.flush()); + + const measures = spans.filter(span => getSpanOp(span) === 'measure'); + expect(measures).toHaveLength(3); + measures.forEach(span => { + expect(span.parent_span_id).toBe(pageload.span_id); + expect(span.attributes['sentry.origin']).toEqual({ type: 'string', value: 'auto.browser.user_timing.measure' }); }); + const restricted = measures.find(span => span.name === 'restricted-test-measure')!; + expect(Object.keys(restricted.attributes).filter(key => key.includes('detail'))).toHaveLength(0); - // Verify no detail attributes were added due to the permission error - const restrictedDataKeys = Object.keys(restrictedMeasure?.data || {}); - const restrictedDetailKeys = restrictedDataKeys.filter(key => key.includes('detail')); - expect(restrictedDetailKeys).toHaveLength(0); + const normal = measures.find(span => span.name === 'normal-measure')!; + expect(normal.attributes['sentry.browser.measure.detail']).toEqual({ type: 'string', value: 'this-should-work' }); - // Test 2: Verify the normal measure still captures detail correctly - const normalMeasure = measureSpans?.find(span => span.description === 'normal-measure'); - expect(normalMeasure).toBeDefined(); - expect(normalMeasure?.data).toMatchObject({ - 'sentry.browser.measure.detail': 'this-should-work', - 'sentry.op': 'measure', - 'sentry.origin': 'auto.browser.user_timing.measure', + const complex = measures.find(span => span.name === 'complex-detail-measure')!; + expect(complex.attributes['sentry.browser.measure.detail.nested']).toEqual({ + type: 'string', + value: JSON.stringify({ array: [1, 2, 3], object: { key: 'value' } }), }); - - // Test 3: Verify the complex detail object is captured correctly - const complexMeasure = measureSpans?.find(span => span.description === 'complex-detail-measure'); - expect(complexMeasure).toBeDefined(); - expect(complexMeasure?.data).toMatchObject({ - 'sentry.op': 'measure', - '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], - object: { - key: 'value', - }, - }), - 'sentry.browser.measure.detail.metadata': JSON.stringify({ - type: 'test', - version: '1.0', - tags: ['complex', 'nested', 'object'], - }), + expect(complex.attributes['sentry.browser.measure.detail.metadata']).toEqual({ + type: 'string', + value: JSON.stringify({ type: 'test', version: '1.0', tags: ['complex', 'nested', 'object'] }), }); }, ); diff --git a/dev-packages/browser-integration-tests/suites/tracing/user-timing/pageload-measure-spans/init.js b/dev-packages/browser-integration-tests/suites/tracing/user-timing/pageload-measure-spans/init.js index 3c2db904fabe..84eb34cca961 100644 --- a/dev-packages/browser-integration-tests/suites/tracing/user-timing/pageload-measure-spans/init.js +++ b/dev-packages/browser-integration-tests/suites/tracing/user-timing/pageload-measure-spans/init.js @@ -11,7 +11,6 @@ performance.measure('Next.js-before-hydration', { window.Sentry = Sentry; Sentry.init({ - traceLifecycle: 'static', dsn: 'https://public@dsn.ingest.sentry.io/1337', integrations: [Sentry.browserTracingIntegration(), userTimingIntegration()], tracesSampleRate: 1, diff --git a/dev-packages/browser-integration-tests/suites/tracing/user-timing/pageload-measure-spans/test.ts b/dev-packages/browser-integration-tests/suites/tracing/user-timing/pageload-measure-spans/test.ts index 365bc082372f..39bcf8b1fed8 100644 --- a/dev-packages/browser-integration-tests/suites/tracing/user-timing/pageload-measure-spans/test.ts +++ b/dev-packages/browser-integration-tests/suites/tracing/user-timing/pageload-measure-spans/test.ts @@ -1,35 +1,34 @@ import { expect } from '@playwright/test'; -import type { Event } from '@sentry/core'; import { sentryTest } from '../../../../utils/fixtures'; -import { getFirstSentryEnvelopeRequest, shouldSkipTracingTest } from '../../../../utils/helpers'; - -// Validation test for https://github.com/getsentry/sentry-javascript/issues/12281 -sentryTest('should add browser-related spans to pageload transaction', async ({ getLocalTestUrl, page }) => { - if (shouldSkipTracingTest()) { - sentryTest.skip(); - } +import { shouldSkipTracingTest } from '../../../../utils/helpers'; +import { collectStreamedSpans, getSpanOp, waitForStreamedSpan } from '../../../../utils/spanUtils'; +sentryTest('clamps pre-SDK measures to the document request start', async ({ getLocalTestUrl, page }) => { + sentryTest.skip(shouldSkipTracingTest()); + const spans = collectStreamedSpans(page); + const pageloadPromise = waitForStreamedSpan(page, span => span.is_segment && getSpanOp(span) === 'pageload'); const url = await getLocalTestUrl({ testDir: __dirname }); + await page.goto(url); + const pageload = await pageloadPromise; + await page.evaluate(() => (window as any).Sentry.flush()); - const eventData = await getFirstSentryEnvelopeRequest(page, url); - const browserSpans = eventData.spans?.filter(({ op }) => op?.startsWith('browser')); - - // Spans `dom_content_loaded_event`, `connect`, `cache` and `dns` are not - // always inside `pageload` transaction. - expect(browserSpans?.length).toBeGreaterThanOrEqual(4); - - const requestSpan = browserSpans!.find(({ op }) => op === 'browser.request'); - expect(requestSpan).toBeDefined(); - expect(requestSpan?.description).toBe(page.url()); - - const measureSpan = eventData.spans?.find(({ op }) => op === 'measure'); - expect(measureSpan).toBeDefined(); - - expect(requestSpan!.start_timestamp).toBeLessThanOrEqual(measureSpan!.start_timestamp); - expect(measureSpan?.data).toEqual({ - 'sentry.browser.measure_happened_before_request': true, - 'sentry.browser.measure_start_time': expect.any(Number), - 'sentry.op': 'measure', - 'sentry.origin': 'auto.browser.user_timing.measure', + const browserSpans = spans.filter(span => getSpanOp(span)?.startsWith('browser')); + expect(browserSpans.length).toBeGreaterThanOrEqual(4); + const request = browserSpans.find(span => getSpanOp(span) === 'browser.request')!; + expect(request.name).toBe('Request'); + expect(request.attributes['url.full']).toEqual({ type: 'string', value: page.url() }); + const measures = spans.filter(span => getSpanOp(span) === 'measure'); + expect(measures).toHaveLength(1); + const measure = measures[0]; + expect(measure.parent_span_id).toBe(pageload.span_id); + expect(request.start_timestamp).toBeLessThanOrEqual(measure.start_timestamp); + expect(measure.attributes).toMatchObject({ + 'sentry.browser.measure_happened_before_request': { type: 'boolean', value: true }, + 'sentry.browser.measure_start_time': { + type: expect.stringMatching(/^(double|integer)$/), + value: expect.any(Number), + }, + 'sentry.op': { type: 'string', value: 'measure' }, + 'sentry.origin': { type: 'string', value: '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 4e01eb11d375..f7b06272f603 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,17 +1,19 @@ import { expect } from '@playwright/test'; import { sentryTest } from '../../../../utils/fixtures'; import { shouldSkipTracingTest } from '../../../../utils/helpers'; -import { getSpanOp, waitForStreamedSpans } from '../../../../utils/spanUtils'; +import { collectStreamedSpans, getSpanOp, waitForStreamedSpan } from '../../../../utils/spanUtils'; sentryTest('captures non-ignored mark and measure spans', async ({ getLocalTestUrl, page }) => { sentryTest.skip(shouldSkipTracingTest()); const url = await getLocalTestUrl({ testDir: __dirname }); - const spansPromise = waitForStreamedSpans(page, spans => spans.some(span => getSpanOp(span) === 'pageload')); + const spans = collectStreamedSpans(page); + const pageloadPromise = waitForStreamedSpan(page, span => span.is_segment && getSpanOp(span) === 'pageload'); await page.goto(url); - const spans = await spansPromise; + await pageloadPromise; + await page.evaluate(() => (window as any).Sentry.flush()); const userTimingSpans = spans .filter(span => ['mark', 'measure'].includes(getSpanOp(span) ?? '')) .map(span => ({ From e3e7fd08214ebbbf3fcc20974965c8805c1da000 Mon Sep 17 00:00:00 2001 From: Martin Sonnberger Date: Wed, 7 Oct 2026 13:24:47 +0200 Subject: [PATCH 2/6] test(browser): retain static request and metric counterparts Co-Authored-By: GPT-6 --- .../assets/image.svg | 1 + .../assets/script.js | 1 + .../assets/style.css | 4 + .../pageload-resource-spans-static/init.js | 10 + .../template.html | 12 + .../pageload-resource-spans-static/test.ts | 223 ++++++++++++++++++ .../metrics/web-vitals-ttfb-static/init.js | 10 + .../web-vitals-ttfb-static/template.html | 9 + .../metrics/web-vitals-ttfb-static/test.ts | 24 ++ .../request/fetch-relative-url-static/init.js | 10 + .../fetch-relative-url-static/subject.js | 3 + .../request/fetch-relative-url-static/test.ts | 77 ++++++ .../tracing/request/fetch-static/init.js | 12 + .../tracing/request/fetch-static/subject.js | 5 + .../tracing/request/fetch-static/test.ts | 86 +++++++ .../request/xhr-relative-url-static/init.js | 10 + .../xhr-relative-url-static/subject.js | 12 + .../request/xhr-relative-url-static/test.ts | 77 ++++++ .../suites/tracing/request/xhr-static/init.js | 12 + .../tracing/request/xhr-static/subject.js | 12 + .../suites/tracing/request/xhr-static/test.ts | 75 ++++++ 21 files changed, 685 insertions(+) create mode 100644 dev-packages/browser-integration-tests/suites/tracing/metrics/pageload-resource-spans-static/assets/image.svg create mode 100644 dev-packages/browser-integration-tests/suites/tracing/metrics/pageload-resource-spans-static/assets/script.js create mode 100644 dev-packages/browser-integration-tests/suites/tracing/metrics/pageload-resource-spans-static/assets/style.css create mode 100644 dev-packages/browser-integration-tests/suites/tracing/metrics/pageload-resource-spans-static/init.js create mode 100644 dev-packages/browser-integration-tests/suites/tracing/metrics/pageload-resource-spans-static/template.html create mode 100644 dev-packages/browser-integration-tests/suites/tracing/metrics/pageload-resource-spans-static/test.ts create mode 100644 dev-packages/browser-integration-tests/suites/tracing/metrics/web-vitals-ttfb-static/init.js create mode 100644 dev-packages/browser-integration-tests/suites/tracing/metrics/web-vitals-ttfb-static/template.html create mode 100644 dev-packages/browser-integration-tests/suites/tracing/metrics/web-vitals-ttfb-static/test.ts create mode 100644 dev-packages/browser-integration-tests/suites/tracing/request/fetch-relative-url-static/init.js create mode 100644 dev-packages/browser-integration-tests/suites/tracing/request/fetch-relative-url-static/subject.js create mode 100644 dev-packages/browser-integration-tests/suites/tracing/request/fetch-relative-url-static/test.ts create mode 100644 dev-packages/browser-integration-tests/suites/tracing/request/fetch-static/init.js create mode 100644 dev-packages/browser-integration-tests/suites/tracing/request/fetch-static/subject.js create mode 100644 dev-packages/browser-integration-tests/suites/tracing/request/fetch-static/test.ts create mode 100644 dev-packages/browser-integration-tests/suites/tracing/request/xhr-relative-url-static/init.js create mode 100644 dev-packages/browser-integration-tests/suites/tracing/request/xhr-relative-url-static/subject.js create mode 100644 dev-packages/browser-integration-tests/suites/tracing/request/xhr-relative-url-static/test.ts create mode 100644 dev-packages/browser-integration-tests/suites/tracing/request/xhr-static/init.js create mode 100644 dev-packages/browser-integration-tests/suites/tracing/request/xhr-static/subject.js create mode 100644 dev-packages/browser-integration-tests/suites/tracing/request/xhr-static/test.ts diff --git a/dev-packages/browser-integration-tests/suites/tracing/metrics/pageload-resource-spans-static/assets/image.svg b/dev-packages/browser-integration-tests/suites/tracing/metrics/pageload-resource-spans-static/assets/image.svg new file mode 100644 index 000000000000..3e8fcacea683 --- /dev/null +++ b/dev-packages/browser-integration-tests/suites/tracing/metrics/pageload-resource-spans-static/assets/image.svg @@ -0,0 +1 @@ + diff --git a/dev-packages/browser-integration-tests/suites/tracing/metrics/pageload-resource-spans-static/assets/script.js b/dev-packages/browser-integration-tests/suites/tracing/metrics/pageload-resource-spans-static/assets/script.js new file mode 100644 index 000000000000..1ebec8919f7f --- /dev/null +++ b/dev-packages/browser-integration-tests/suites/tracing/metrics/pageload-resource-spans-static/assets/script.js @@ -0,0 +1 @@ +(() => {})(); diff --git a/dev-packages/browser-integration-tests/suites/tracing/metrics/pageload-resource-spans-static/assets/style.css b/dev-packages/browser-integration-tests/suites/tracing/metrics/pageload-resource-spans-static/assets/style.css new file mode 100644 index 000000000000..3e9fdb9de409 --- /dev/null +++ b/dev-packages/browser-integration-tests/suites/tracing/metrics/pageload-resource-spans-static/assets/style.css @@ -0,0 +1,4 @@ +p { + color: red; + text-align: center; +} diff --git a/dev-packages/browser-integration-tests/suites/tracing/metrics/pageload-resource-spans-static/init.js b/dev-packages/browser-integration-tests/suites/tracing/metrics/pageload-resource-spans-static/init.js new file mode 100644 index 000000000000..bb6d4918d1bf --- /dev/null +++ b/dev-packages/browser-integration-tests/suites/tracing/metrics/pageload-resource-spans-static/init.js @@ -0,0 +1,10 @@ +import * as Sentry from '@sentry/browser'; + +window.Sentry = Sentry; + +Sentry.init({ + traceLifecycle: 'static', + dsn: 'https://public@dsn.ingest.sentry.io/1337', + integrations: [Sentry.browserTracingIntegration()], + tracesSampleRate: 1, +}); diff --git a/dev-packages/browser-integration-tests/suites/tracing/metrics/pageload-resource-spans-static/template.html b/dev-packages/browser-integration-tests/suites/tracing/metrics/pageload-resource-spans-static/template.html new file mode 100644 index 000000000000..8491137339f2 --- /dev/null +++ b/dev-packages/browser-integration-tests/suites/tracing/metrics/pageload-resource-spans-static/template.html @@ -0,0 +1,12 @@ + + + + + + + + + + Rendered + + diff --git a/dev-packages/browser-integration-tests/suites/tracing/metrics/pageload-resource-spans-static/test.ts b/dev-packages/browser-integration-tests/suites/tracing/metrics/pageload-resource-spans-static/test.ts new file mode 100644 index 000000000000..276234385fae --- /dev/null +++ b/dev-packages/browser-integration-tests/suites/tracing/metrics/pageload-resource-spans-static/test.ts @@ -0,0 +1,223 @@ +import { SENTRY_OP, SENTRY_ORIGIN } from '@sentry/conventions/attributes'; +import type { Route } from '@playwright/test'; +import { expect } from '@playwright/test'; +import { type Event } from '@sentry/core'; +import { sentryTest } from '../../../../utils/fixtures'; +import { getFirstSentryEnvelopeRequest, shouldSkipTracingTest } from '../../../../utils/helpers'; + +sentryTest('adds resource spans to pageload transaction', async ({ getLocalTestUrl, page, browserName }) => { + if (shouldSkipTracingTest()) { + sentryTest.skip(); + } + + const isWebkitRun = browserName === 'webkit'; + + // Intercepting asset requests to avoid network-related flakiness and random retries (on Firefox). + await page.route('https://sentry-test-site.example/path/to/image.svg', (route: Route) => + route.fulfill({ + path: `${__dirname}/assets/image.svg`, + headers: { + 'Timing-Allow-Origin': '*', + 'Content-Type': 'image/svg+xml', + }, + }), + ); + await page.route('https://sentry-test-site.example/path/to/script.js', (route: Route) => + route.fulfill({ + path: `${__dirname}/assets/script.js`, + headers: { + 'Timing-Allow-Origin': '*', + 'Content-Type': 'application/javascript', + }, + }), + ); + await page.route('https://sentry-test-site.example/path/to/style.css', (route: Route) => + route.fulfill({ + path: `${__dirname}/assets/style.css`, + headers: { + 'Timing-Allow-Origin': '*', + 'Content-Type': 'text/css', + }, + }), + ); + + const url = await getLocalTestUrl({ testDir: __dirname }); + + const eventData = await getFirstSentryEnvelopeRequest(page, url); + const resourceSpans = eventData.spans?.filter(({ op }) => op?.startsWith('resource')); + + const scriptSpans = resourceSpans?.filter(({ op }) => op === 'resource.script'); + const linkSpan = resourceSpans?.filter(({ op }) => op === 'resource.link')[0]; + const imgSpan = resourceSpans?.filter(({ op }) => op === 'resource.img')[0]; + + const spanId = eventData.contexts?.trace?.span_id; + const traceId = eventData.contexts?.trace?.trace_id; + + expect(spanId).toBeDefined(); + expect(traceId).toBeDefined(); + + const hasCdnBundle = (process.env.PW_BUNDLE || '').startsWith('bundle'); + + const expectedScripts = ['/init.bundle.js', 'https://sentry-test-site.example/path/to/script.js']; + if (hasCdnBundle) { + expectedScripts.unshift('/cdn.bundle.js'); + } + + expect(scriptSpans?.map(({ description }) => description).sort()).toEqual(expectedScripts); + expect(scriptSpans?.map(({ parent_span_id }) => parent_span_id)).toEqual(expectedScripts.map(() => spanId)); + + // The init bundle script is served from the test origin: its description is origin-relative, + // but `url.full` retains the full absolute URL (needed for span description inference). + const sameOriginScriptSpan = scriptSpans?.find(({ description }) => description === '/init.bundle.js'); + expect(sameOriginScriptSpan?.data?.['http.request.same_origin']).toBe(true); + expect(sameOriginScriptSpan?.data?.['url.full']).toMatch(/^https?:\/\/.+\/init\.bundle\.js$/); + + const customScriptSpan = scriptSpans?.find( + ({ description }) => description === 'https://sentry-test-site.example/path/to/script.js', + ); + + expect(imgSpan).toEqual({ + data: { + 'http.response.body.decoded_size': expect.any(Number), + 'http.response.body.size': expect.any(Number), + 'http.response.size': expect.any(Number), + 'http.request.connect_start': expect.any(Number), + 'http.request.connection_end': expect.any(Number), + 'http.request.domain_lookup_end': expect.any(Number), + 'http.request.domain_lookup_start': expect.any(Number), + 'http.request.fetch_start': expect.any(Number), + 'http.request.redirect_end': expect.any(Number), + 'http.request.redirect_start': expect.any(Number), + 'http.request.request_start': expect.any(Number), + 'http.request.secure_connection_start': expect.any(Number), + 'http.request.worker_start': expect.any(Number), + 'http.request.response_end': expect.any(Number), + 'http.request.response_start': expect.any(Number), + 'http.request.time_to_first_byte': expect.any(Number), + 'network.protocol.name': '', + 'network.protocol.version': 'unknown', + [SENTRY_OP]: 'resource.img', + [SENTRY_ORIGIN]: 'auto.resource.browser.metrics', + 'server.address': 'sentry-test-site.example', + 'url.domain': 'sentry-test-site.example', + 'http.request.same_origin': false, + 'url.scheme': 'https', + 'url.full': 'https://sentry-test-site.example/path/to/image.svg', + // WebKit reports `deliveryType` as of Playwright 1.63's build, but still no response status + // or render blocking status. + 'http.response_delivery_type': '', + ...(!isWebkitRun && { + 'http.response.status_code': expect.any(Number), + 'resource.render_blocking_status': 'non-blocking', + }), + }, + description: 'https://sentry-test-site.example/path/to/image.svg', + op: 'resource.img', + origin: 'auto.resource.browser.metrics', + status: 'ok', + parent_span_id: spanId, + span_id: expect.stringMatching(/^[a-f\d]{16}$/), + start_timestamp: expect.any(Number), + timestamp: expect.any(Number), + trace_id: traceId, + }); + + // range check: TTFB is reasonably <10 seconds, which is really a check that we report it in + // seconds rather than milliseconds. WebKit resolves these intercepted routes without measurable + // delay, so only the other engines are held to a non-zero value. + const imgSpanTtfb = imgSpan?.data['http.request.time_to_first_byte']; + expect(imgSpanTtfb).toBeGreaterThan(isWebkitRun ? -1 : 0); + expect(imgSpanTtfb).toBeLessThan(10); + + expect(linkSpan).toEqual({ + data: { + 'http.response.body.decoded_size': expect.any(Number), + 'http.response.body.size': expect.any(Number), + 'http.response.size': expect.any(Number), + 'http.request.connect_start': expect.any(Number), + 'http.request.connection_end': expect.any(Number), + 'http.request.domain_lookup_end': expect.any(Number), + 'http.request.domain_lookup_start': expect.any(Number), + 'http.request.fetch_start': expect.any(Number), + 'http.request.redirect_end': expect.any(Number), + 'http.request.redirect_start': expect.any(Number), + 'http.request.request_start': expect.any(Number), + 'http.request.secure_connection_start': expect.any(Number), + 'http.request.worker_start': expect.any(Number), + 'http.request.response_end': expect.any(Number), + 'http.request.response_start': expect.any(Number), + 'http.request.time_to_first_byte': expect.any(Number), + 'network.protocol.name': '', + 'network.protocol.version': 'unknown', + [SENTRY_OP]: 'resource.link', + [SENTRY_ORIGIN]: 'auto.resource.browser.metrics', + 'server.address': 'sentry-test-site.example', + 'url.domain': 'sentry-test-site.example', + 'http.request.same_origin': false, + 'url.scheme': 'https', + 'url.full': 'https://sentry-test-site.example/path/to/style.css', + // WebKit reports `deliveryType` as of Playwright 1.63's build, but still no response status + // or render blocking status. + 'http.response_delivery_type': '', + ...(!isWebkitRun && { + 'http.response.status_code': expect.any(Number), + 'resource.render_blocking_status': 'non-blocking', + }), + }, + description: 'https://sentry-test-site.example/path/to/style.css', + op: 'resource.link', + origin: 'auto.resource.browser.metrics', + status: 'ok', + parent_span_id: spanId, + span_id: expect.stringMatching(/^[a-f\d]{16}$/), + start_timestamp: expect.any(Number), + timestamp: expect.any(Number), + trace_id: traceId, + }); + + expect(customScriptSpan).toEqual({ + data: { + 'http.response.body.decoded_size': expect.any(Number), + 'http.response.body.size': expect.any(Number), + 'http.response.size': expect.any(Number), + 'http.request.connection_end': expect.any(Number), + 'http.request.connect_start': expect.any(Number), + 'http.request.domain_lookup_end': expect.any(Number), + 'http.request.domain_lookup_start': expect.any(Number), + 'http.request.fetch_start': expect.any(Number), + 'http.request.redirect_end': expect.any(Number), + 'http.request.redirect_start': expect.any(Number), + 'http.request.request_start': expect.any(Number), + 'http.request.secure_connection_start': expect.any(Number), + 'http.request.worker_start': expect.any(Number), + 'http.request.response_end': expect.any(Number), + 'http.request.response_start': expect.any(Number), + 'http.request.time_to_first_byte': expect.any(Number), + 'network.protocol.name': '', + 'network.protocol.version': 'unknown', + 'sentry.op': 'resource.script', + 'sentry.origin': 'auto.resource.browser.metrics', + 'server.address': 'sentry-test-site.example', + 'url.domain': 'sentry-test-site.example', + 'http.request.same_origin': false, + 'url.scheme': 'https', + 'url.full': 'https://sentry-test-site.example/path/to/script.js', + // WebKit reports `deliveryType` as of Playwright 1.63's build, but still no response status + // or render blocking status. + 'http.response_delivery_type': '', + ...(!isWebkitRun && { + 'http.response.status_code': expect.any(Number), + 'resource.render_blocking_status': 'non-blocking', + }), + }, + description: 'https://sentry-test-site.example/path/to/script.js', + op: 'resource.script', + origin: 'auto.resource.browser.metrics', + status: 'ok', + parent_span_id: spanId, + span_id: expect.stringMatching(/^[a-f\d]{16}$/), + start_timestamp: expect.any(Number), + timestamp: expect.any(Number), + trace_id: traceId, + }); +}); diff --git a/dev-packages/browser-integration-tests/suites/tracing/metrics/web-vitals-ttfb-static/init.js b/dev-packages/browser-integration-tests/suites/tracing/metrics/web-vitals-ttfb-static/init.js new file mode 100644 index 000000000000..bb6d4918d1bf --- /dev/null +++ b/dev-packages/browser-integration-tests/suites/tracing/metrics/web-vitals-ttfb-static/init.js @@ -0,0 +1,10 @@ +import * as Sentry from '@sentry/browser'; + +window.Sentry = Sentry; + +Sentry.init({ + traceLifecycle: 'static', + dsn: 'https://public@dsn.ingest.sentry.io/1337', + integrations: [Sentry.browserTracingIntegration()], + tracesSampleRate: 1, +}); diff --git a/dev-packages/browser-integration-tests/suites/tracing/metrics/web-vitals-ttfb-static/template.html b/dev-packages/browser-integration-tests/suites/tracing/metrics/web-vitals-ttfb-static/template.html new file mode 100644 index 000000000000..b81f11e967e3 --- /dev/null +++ b/dev-packages/browser-integration-tests/suites/tracing/metrics/web-vitals-ttfb-static/template.html @@ -0,0 +1,9 @@ + + + + + + +
Rendered
+ + diff --git a/dev-packages/browser-integration-tests/suites/tracing/metrics/web-vitals-ttfb-static/test.ts b/dev-packages/browser-integration-tests/suites/tracing/metrics/web-vitals-ttfb-static/test.ts new file mode 100644 index 000000000000..d2997f712c39 --- /dev/null +++ b/dev-packages/browser-integration-tests/suites/tracing/metrics/web-vitals-ttfb-static/test.ts @@ -0,0 +1,24 @@ +import { expect } from '@playwright/test'; +import type { Event } from '@sentry/core'; +import { sentryTest } from '../../../../utils/fixtures'; +import { getFirstSentryEnvelopeRequest, shouldSkipTracingTest } from '../../../../utils/helpers'; + +sentryTest('should capture TTFB vital.', async ({ getLocalTestUrl, page }) => { + if (shouldSkipTracingTest()) { + sentryTest.skip(); + } + + const url = await getLocalTestUrl({ testDir: __dirname }); + const eventData = await getFirstSentryEnvelopeRequest(page, url); + + expect(eventData.measurements).toBeDefined(); + + // If responseStart === 0, ttfb is not reported + // This seems to happen somewhat randomly, so we just ignore this in that case + const responseStart = await page.evaluate("performance.getEntriesByType('navigation')[0].responseStart;"); + if (responseStart !== 0) { + expect(eventData.measurements?.ttfb?.value).toBeDefined(); + } + + expect(eventData.measurements?.['ttfb.requestTime']?.value).toBeDefined(); +}); diff --git a/dev-packages/browser-integration-tests/suites/tracing/request/fetch-relative-url-static/init.js b/dev-packages/browser-integration-tests/suites/tracing/request/fetch-relative-url-static/init.js new file mode 100644 index 000000000000..bb6d4918d1bf --- /dev/null +++ b/dev-packages/browser-integration-tests/suites/tracing/request/fetch-relative-url-static/init.js @@ -0,0 +1,10 @@ +import * as Sentry from '@sentry/browser'; + +window.Sentry = Sentry; + +Sentry.init({ + traceLifecycle: 'static', + dsn: 'https://public@dsn.ingest.sentry.io/1337', + integrations: [Sentry.browserTracingIntegration()], + tracesSampleRate: 1, +}); diff --git a/dev-packages/browser-integration-tests/suites/tracing/request/fetch-relative-url-static/subject.js b/dev-packages/browser-integration-tests/suites/tracing/request/fetch-relative-url-static/subject.js new file mode 100644 index 000000000000..b0d4abc78c65 --- /dev/null +++ b/dev-packages/browser-integration-tests/suites/tracing/request/fetch-relative-url-static/subject.js @@ -0,0 +1,3 @@ +fetch('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/test-req/0').then( + fetch('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/test-req/1', { headers: { 'X-Test-Header': 'existing-header' } }).then(fetch('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/test-req/2')), +); diff --git a/dev-packages/browser-integration-tests/suites/tracing/request/fetch-relative-url-static/test.ts b/dev-packages/browser-integration-tests/suites/tracing/request/fetch-relative-url-static/test.ts new file mode 100644 index 000000000000..323060c7e4b2 --- /dev/null +++ b/dev-packages/browser-integration-tests/suites/tracing/request/fetch-relative-url-static/test.ts @@ -0,0 +1,77 @@ +import { expect } from '@playwright/test'; +import { sentryTest, TEST_HOST } from '../../../../utils/fixtures'; +import { + envelopeRequestParser, + shouldSkipTracingTest, + waitForTransactionRequestOnUrl, +} from '../../../../utils/helpers'; + +sentryTest('should create spans for fetch requests', async ({ getLocalTestUrl, page }) => { + if (shouldSkipTracingTest()) { + sentryTest.skip(); + } + + const url = await getLocalTestUrl({ testDir: __dirname }); + const req = await waitForTransactionRequestOnUrl(page, url); + const tracingEvent = envelopeRequestParser(req); + + const requestSpans = tracingEvent.spans?.filter(({ op }) => op === 'http.client'); + + expect(requestSpans).toHaveLength(3); + + requestSpans?.forEach((span, index) => + expect(span).toMatchObject({ + description: `GET /test-req/${index}`, + parent_span_id: tracingEvent.contexts?.trace?.span_id, + span_id: expect.stringMatching(/[a-f\d]{16}/), + start_timestamp: expect.any(Number), + timestamp: expect.any(Number), + trace_id: tracingEvent.contexts?.trace?.trace_id, + data: { + 'http.request.method': 'GET', + 'url.full': `${TEST_HOST}/test-req/${index}`, + 'server.address': 'sentry-test.io', + type: 'fetch', + }, + }), + ); +}); + +sentryTest('should attach `sentry-trace` header to fetch requests', async ({ getLocalTestUrl, page }) => { + if (shouldSkipTracingTest()) { + sentryTest.skip(); + } + + const url = await getLocalTestUrl({ testDir: __dirname }); + + const requests = ( + await Promise.all([ + page.goto(url), + Promise.all([0, 1, 2].map(idx => page.waitForRequest(`${TEST_HOST}/test-req/${idx}`))), + ]) + )[1]; + + expect(requests).toHaveLength(3); + + const request1 = requests[0]; + const requestHeaders1 = request1.headers(); + expect(requestHeaders1).toMatchObject({ + 'sentry-trace': expect.stringMatching(/^([a-f\d]{32})-([a-f\d]{16})-1$/), + baggage: expect.any(String), + }); + + const request2 = requests[1]; + const requestHeaders2 = request2.headers(); + expect(requestHeaders2).toMatchObject({ + 'sentry-trace': expect.stringMatching(/^([a-f\d]{32})-([a-f\d]{16})-1$/), + baggage: expect.any(String), + 'x-test-header': 'existing-header', + }); + + const request3 = requests[2]; + const requestHeaders3 = request3.headers(); + expect(requestHeaders3).toMatchObject({ + 'sentry-trace': expect.stringMatching(/^([a-f\d]{32})-([a-f\d]{16})-1$/), + baggage: expect.any(String), + }); +}); diff --git a/dev-packages/browser-integration-tests/suites/tracing/request/fetch-static/init.js b/dev-packages/browser-integration-tests/suites/tracing/request/fetch-static/init.js new file mode 100644 index 000000000000..847df632b642 --- /dev/null +++ b/dev-packages/browser-integration-tests/suites/tracing/request/fetch-static/init.js @@ -0,0 +1,12 @@ +import * as Sentry from '@sentry/browser'; + +window.Sentry = Sentry; + +Sentry.init({ + traceLifecycle: 'static', + dsn: 'https://public@dsn.ingest.sentry.io/1337', + integrations: [Sentry.browserTracingIntegration()], + tracePropagationTargets: ['http://sentry-test-site.example'], + tracesSampleRate: 1, + autoSessionTracking: false, +}); diff --git a/dev-packages/browser-integration-tests/suites/tracing/request/fetch-static/subject.js b/dev-packages/browser-integration-tests/suites/tracing/request/fetch-static/subject.js new file mode 100644 index 000000000000..482a738009c2 --- /dev/null +++ b/dev-packages/browser-integration-tests/suites/tracing/request/fetch-static/subject.js @@ -0,0 +1,5 @@ +fetch('http://sentry-test-site.example/0').then( + fetch('http://sentry-test-site.example/1', { headers: { 'X-Test-Header': 'existing-header' } }).then( + fetch('http://sentry-test-site.example/2'), + ), +); diff --git a/dev-packages/browser-integration-tests/suites/tracing/request/fetch-static/test.ts b/dev-packages/browser-integration-tests/suites/tracing/request/fetch-static/test.ts new file mode 100644 index 000000000000..a177445bb980 --- /dev/null +++ b/dev-packages/browser-integration-tests/suites/tracing/request/fetch-static/test.ts @@ -0,0 +1,86 @@ +import { expect } from '@playwright/test'; +import { sentryTest } from '../../../../utils/fixtures'; +import { + envelopeRequestParser, + shouldSkipTracingTest, + waitForTransactionRequestOnUrl, +} from '../../../../utils/helpers'; + +sentryTest('should create spans for fetch requests', async ({ getLocalTestUrl, page }) => { + if (shouldSkipTracingTest()) { + sentryTest.skip(); + } + + await page.route('http://sentry-test-site.example/*', route => route.fulfill({ body: 'ok' })); + + const url = await getLocalTestUrl({ testDir: __dirname }); + + const req = await waitForTransactionRequestOnUrl(page, url); + const tracingEvent = envelopeRequestParser(req); + + const requestSpans = tracingEvent.spans?.filter(({ op }) => op === 'http.client'); + + expect(requestSpans).toHaveLength(3); + + requestSpans?.forEach((span, index) => + expect(span).toMatchObject({ + description: `GET http://sentry-test-site.example/${index}`, + parent_span_id: tracingEvent.contexts?.trace?.span_id, + span_id: expect.stringMatching(/[a-f\d]{16}/), + start_timestamp: expect.any(Number), + timestamp: expect.any(Number), + trace_id: tracingEvent.contexts?.trace?.trace_id, + data: { + 'http.request.method': 'GET', + 'url.full': `http://sentry-test-site.example/${index}`, + 'server.address': 'sentry-test-site.example', + type: 'fetch', + }, + }), + ); +}); + +sentryTest('attaches `sentry-trace` and `baggage` headers to fetch requests', async ({ getLocalTestUrl, page }) => { + if (shouldSkipTracingTest()) { + sentryTest.skip(); + } + + await page.route('http://sentry-test-site.example/*', route => route.fulfill({ body: 'ok' })); + + const url = await getLocalTestUrl({ testDir: __dirname }); + + const requests = ( + await Promise.all([ + page.goto(url), + Promise.all([0, 1, 2].map(idx => page.waitForRequest(`http://sentry-test-site.example/${idx}`))), + ]) + )[1]; + + expect(requests).toHaveLength(3); + + const request1 = requests[0]; + const requestHeaders1 = request1.headers(); + expect(requestHeaders1).toMatchObject({ + 'sentry-trace': expect.stringMatching(/^([a-f\d]{32})-([a-f\d]{16})-1$/), + baggage: expect.any(String), + }); + // traceparent must only be attached if propagateTraceparent is `true` + expect(requestHeaders1).not.toHaveProperty('traceparent'); + + const request2 = requests[1]; + const requestHeaders2 = request2.headers(); + expect(requestHeaders2).toMatchObject({ + 'sentry-trace': expect.stringMatching(/^([a-f\d]{32})-([a-f\d]{16})-1$/), + baggage: expect.any(String), + 'x-test-header': 'existing-header', + }); + expect(requestHeaders2).not.toHaveProperty('traceparent'); + + const request3 = requests[2]; + const requestHeaders3 = request3.headers(); + expect(requestHeaders3).toMatchObject({ + 'sentry-trace': expect.stringMatching(/^([a-f\d]{32})-([a-f\d]{16})-1$/), + baggage: expect.any(String), + }); + expect(requestHeaders3).not.toHaveProperty('traceparent'); +}); diff --git a/dev-packages/browser-integration-tests/suites/tracing/request/xhr-relative-url-static/init.js b/dev-packages/browser-integration-tests/suites/tracing/request/xhr-relative-url-static/init.js new file mode 100644 index 000000000000..bb6d4918d1bf --- /dev/null +++ b/dev-packages/browser-integration-tests/suites/tracing/request/xhr-relative-url-static/init.js @@ -0,0 +1,10 @@ +import * as Sentry from '@sentry/browser'; + +window.Sentry = Sentry; + +Sentry.init({ + traceLifecycle: 'static', + dsn: 'https://public@dsn.ingest.sentry.io/1337', + integrations: [Sentry.browserTracingIntegration()], + tracesSampleRate: 1, +}); diff --git a/dev-packages/browser-integration-tests/suites/tracing/request/xhr-relative-url-static/subject.js b/dev-packages/browser-integration-tests/suites/tracing/request/xhr-relative-url-static/subject.js new file mode 100644 index 000000000000..5fc9f91ab568 --- /dev/null +++ b/dev-packages/browser-integration-tests/suites/tracing/request/xhr-relative-url-static/subject.js @@ -0,0 +1,12 @@ +const xhr_1 = new XMLHttpRequest(); +xhr_1.open('GET', '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/test-req/0'); +xhr_1.send(); + +const xhr_2 = new XMLHttpRequest(); +xhr_2.open('GET', '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/test-req/1'); +xhr_2.setRequestHeader('X-Test-Header', 'existing-header'); +xhr_2.send(); + +const xhr_3 = new XMLHttpRequest(); +xhr_3.open('GET', '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/test-req/2'); +xhr_3.send(); diff --git a/dev-packages/browser-integration-tests/suites/tracing/request/xhr-relative-url-static/test.ts b/dev-packages/browser-integration-tests/suites/tracing/request/xhr-relative-url-static/test.ts new file mode 100644 index 000000000000..110df467837f --- /dev/null +++ b/dev-packages/browser-integration-tests/suites/tracing/request/xhr-relative-url-static/test.ts @@ -0,0 +1,77 @@ +import { expect } from '@playwright/test'; +import { sentryTest, TEST_HOST } from '../../../../utils/fixtures'; +import { + envelopeRequestParser, + shouldSkipTracingTest, + waitForTransactionRequestOnUrl, +} from '../../../../utils/helpers'; + +sentryTest('should create spans for xhr requests', async ({ getLocalTestUrl, page }) => { + if (shouldSkipTracingTest()) { + sentryTest.skip(); + } + + const url = await getLocalTestUrl({ testDir: __dirname }); + const req = await waitForTransactionRequestOnUrl(page, url); + const tracingEvent = envelopeRequestParser(req); + + const requestSpans = tracingEvent.spans?.filter(({ op }) => op === 'http.client'); + + expect(requestSpans).toHaveLength(3); + + requestSpans?.forEach((span, index) => + expect(span).toMatchObject({ + description: `GET /test-req/${index}`, + parent_span_id: tracingEvent.contexts?.trace?.span_id, + span_id: expect.stringMatching(/[a-f\d]{16}/), + start_timestamp: expect.any(Number), + timestamp: expect.any(Number), + trace_id: tracingEvent.contexts?.trace?.trace_id, + data: { + 'http.request.method': 'GET', + 'url.full': `${TEST_HOST}/test-req/${index}`, + 'server.address': 'sentry-test.io', + type: 'xhr', + }, + }), + ); +}); + +sentryTest('should attach `sentry-trace` header to xhr requests', async ({ getLocalTestUrl, page }) => { + if (shouldSkipTracingTest()) { + sentryTest.skip(); + } + + const url = await getLocalTestUrl({ testDir: __dirname }); + + const requests = ( + await Promise.all([ + page.goto(url), + Promise.all([0, 1, 2].map(idx => page.waitForRequest(`${TEST_HOST}/test-req/${idx}`))), + ]) + )[1]; + + expect(requests).toHaveLength(3); + + const request1 = requests[0]; + const requestHeaders1 = request1.headers(); + expect(requestHeaders1).toMatchObject({ + 'sentry-trace': expect.stringMatching(/^([a-f\d]{32})-([a-f\d]{16})-1$/), + baggage: expect.any(String), + }); + + const request2 = requests[1]; + const requestHeaders2 = request2.headers(); + expect(requestHeaders2).toMatchObject({ + 'sentry-trace': expect.stringMatching(/^([a-f\d]{32})-([a-f\d]{16})-1$/), + baggage: expect.any(String), + 'x-test-header': 'existing-header', + }); + + const request3 = requests[2]; + const requestHeaders3 = request3.headers(); + expect(requestHeaders3).toMatchObject({ + 'sentry-trace': expect.stringMatching(/^([a-f\d]{32})-([a-f\d]{16})-1$/), + baggage: expect.any(String), + }); +}); diff --git a/dev-packages/browser-integration-tests/suites/tracing/request/xhr-static/init.js b/dev-packages/browser-integration-tests/suites/tracing/request/xhr-static/init.js new file mode 100644 index 000000000000..847df632b642 --- /dev/null +++ b/dev-packages/browser-integration-tests/suites/tracing/request/xhr-static/init.js @@ -0,0 +1,12 @@ +import * as Sentry from '@sentry/browser'; + +window.Sentry = Sentry; + +Sentry.init({ + traceLifecycle: 'static', + dsn: 'https://public@dsn.ingest.sentry.io/1337', + integrations: [Sentry.browserTracingIntegration()], + tracePropagationTargets: ['http://sentry-test-site.example'], + tracesSampleRate: 1, + autoSessionTracking: false, +}); diff --git a/dev-packages/browser-integration-tests/suites/tracing/request/xhr-static/subject.js b/dev-packages/browser-integration-tests/suites/tracing/request/xhr-static/subject.js new file mode 100644 index 000000000000..9c584bf743cb --- /dev/null +++ b/dev-packages/browser-integration-tests/suites/tracing/request/xhr-static/subject.js @@ -0,0 +1,12 @@ +const xhr_1 = new XMLHttpRequest(); +xhr_1.open('GET', 'http://sentry-test-site.example/0'); +xhr_1.send(); + +const xhr_2 = new XMLHttpRequest(); +xhr_2.open('GET', 'http://sentry-test-site.example/1'); +xhr_2.setRequestHeader('X-Test-Header', 'existing-header'); +xhr_2.send(); + +const xhr_3 = new XMLHttpRequest(); +xhr_3.open('GET', 'http://sentry-test-site.example/2'); +xhr_3.send(); diff --git a/dev-packages/browser-integration-tests/suites/tracing/request/xhr-static/test.ts b/dev-packages/browser-integration-tests/suites/tracing/request/xhr-static/test.ts new file mode 100644 index 000000000000..971cb0f5c030 --- /dev/null +++ b/dev-packages/browser-integration-tests/suites/tracing/request/xhr-static/test.ts @@ -0,0 +1,75 @@ +import { expect } from '@playwright/test'; +import type { Event } from '@sentry/core'; +import { sentryTest } from '../../../../utils/fixtures'; +import { getFirstSentryEnvelopeRequest, shouldSkipTracingTest } from '../../../../utils/helpers'; + +sentryTest('should create spans for XHR requests', async ({ getLocalTestUrl, page }) => { + if (shouldSkipTracingTest()) { + sentryTest.skip(); + } + + await page.route('http://sentry-test-site.example/*', route => route.fulfill({ body: 'ok' })); + + const url = await getLocalTestUrl({ testDir: __dirname }); + + const eventData = await getFirstSentryEnvelopeRequest(page, url); + const requestSpans = eventData.spans?.filter(({ op }) => op === 'http.client'); + + expect(requestSpans).toHaveLength(3); + + requestSpans?.forEach((span, index) => + expect(span).toMatchObject({ + description: `GET http://sentry-test-site.example/${index}`, + parent_span_id: eventData.contexts?.trace?.span_id, + span_id: expect.stringMatching(/[a-f\d]{16}/), + start_timestamp: expect.any(Number), + timestamp: expect.any(Number), + trace_id: eventData.contexts?.trace?.trace_id, + data: { + 'http.request.method': 'GET', + 'url.full': `http://sentry-test-site.example/${index}`, + 'server.address': 'sentry-test-site.example', + type: 'xhr', + }, + }), + ); +}); + +sentryTest('should attach `sentry-trace` header to XHR requests', async ({ getLocalTestUrl, page }) => { + if (shouldSkipTracingTest()) { + sentryTest.skip(); + } + + const url = await getLocalTestUrl({ testDir: __dirname }); + + const requests = ( + await Promise.all([ + page.goto(url), + Promise.all([0, 1, 2].map(idx => page.waitForRequest(`http://sentry-test-site.example/${idx}`))), + ]) + )[1]; + + expect(requests).toHaveLength(3); + + const request1 = requests[0]; + const requestHeaders1 = request1.headers(); + expect(requestHeaders1).toMatchObject({ + 'sentry-trace': expect.stringMatching(/^([a-f\d]{32})-([a-f\d]{16})-1$/), + baggage: expect.any(String), + }); + + const request2 = requests[1]; + const requestHeaders2 = request2.headers(); + expect(requestHeaders2).toMatchObject({ + 'sentry-trace': expect.stringMatching(/^([a-f\d]{32})-([a-f\d]{16})-1$/), + baggage: expect.any(String), + 'x-test-header': 'existing-header', + }); + + const request3 = requests[2]; + const requestHeaders3 = request3.headers(); + expect(requestHeaders3).toMatchObject({ + 'sentry-trace': expect.stringMatching(/^([a-f\d]{32})-([a-f\d]{16})-1$/), + baggage: expect.any(String), + }); +}); From 37f9396ca47c1915de746132b47b8325a343b463 Mon Sep 17 00:00:00 2001 From: Martin Sonnberger Date: Wed, 7 Oct 2026 15:02:19 +0200 Subject: [PATCH 3/6] test(browser): wait for LCP candidates and use attribute constants Co-Authored-By: GPT-6 --- .../tracing/interactions/disabled/test.ts | 3 +- .../suites/tracing/interactions/spans/test.ts | 20 +- .../metrics/connection-rtt-navigation/test.ts | 10 +- .../tracing/metrics/handlers-lcp/test.ts | 11 +- .../metrics/pageload-browser-spans/test.ts | 3 +- .../pageload-resource-spans-static/test.ts | 190 +++++++++-------- .../metrics/pageload-resource-spans/test.ts | 196 ++++++++++-------- .../tracing/metrics/web-vitals-cls/test.ts | 25 ++- .../web-vitals-inp-before-send-span/test.ts | 3 +- .../metrics/web-vitals-inp-late/test.ts | 58 ++++-- .../metrics/web-vitals-inp-navigate/test.ts | 94 +++++---- .../web-vitals-inp-parametrized-late/test.ts | 58 ++++-- .../web-vitals-inp-parametrized/test.ts | 58 ++++-- .../tracing/metrics/web-vitals-inp/test.ts | 37 ++-- .../tracing/metrics/web-vitals-lcp/test.ts | 47 +++-- .../metrics/web-vitals-ttfb-static/test.ts | 3 +- .../tracing/metrics/web-vitals-ttfb/test.ts | 7 +- .../suites/tracing/metrics/web-vitals/test.ts | 25 ++- .../tracing/request/fetch-data-url/test.ts | 5 +- .../tracing/request/fetch-immediate/test.ts | 7 +- .../request/fetch-relative-url-static/test.ts | 7 +- .../request/fetch-relative-url/test.ts | 13 +- .../tracing/request/fetch-static/test.ts | 7 +- .../fetch-strip-query-and-fragment/test.ts | 79 ++++--- .../fetch-track-stream-performance/test.ts | 7 +- .../suites/tracing/request/fetch/test.ts | 13 +- .../tracing/request/xhr-data-url/test.ts | 5 +- .../request/xhr-relative-url-static/test.ts | 7 +- .../tracing/request/xhr-relative-url/test.ts | 13 +- .../suites/tracing/request/xhr-static/test.ts | 7 +- .../xhr-strip-query-and-fragment/test.ts | 70 ++++--- .../suites/tracing/request/xhr/test.ts | 13 +- .../test.ts | 3 +- .../pageload-measure-spans/test.ts | 7 +- .../suites/tracing/user-timing/spans/test.ts | 3 +- .../utils/helpers.ts | 16 ++ 36 files changed, 685 insertions(+), 445 deletions(-) diff --git a/dev-packages/browser-integration-tests/suites/tracing/interactions/disabled/test.ts b/dev-packages/browser-integration-tests/suites/tracing/interactions/disabled/test.ts index 6589fc67ed3a..400a19d98011 100644 --- a/dev-packages/browser-integration-tests/suites/tracing/interactions/disabled/test.ts +++ b/dev-packages/browser-integration-tests/suites/tracing/interactions/disabled/test.ts @@ -1,3 +1,4 @@ +import { SENTRY_ORIGIN } from '@sentry/conventions/attributes'; import { expect } from '@playwright/test'; import { sentryTest } from '../../../../utils/fixtures'; import { hidePage, shouldSkipTracingTest } from '../../../../utils/helpers'; @@ -14,5 +15,5 @@ sentryTest('does not capture interaction spans without the integration', async ( await hidePage(page); await page.evaluate(() => (window as any).Sentry.flush()); - expect(spans.filter(span => span.attributes['sentry.origin']?.value === 'auto.browser.interactions')).toHaveLength(0); + expect(spans.filter(span => span.attributes[SENTRY_ORIGIN]?.value === 'auto.browser.interactions')).toHaveLength(0); }); diff --git a/dev-packages/browser-integration-tests/suites/tracing/interactions/spans/test.ts b/dev-packages/browser-integration-tests/suites/tracing/interactions/spans/test.ts index 08c475e10551..2a83365805f8 100644 --- a/dev-packages/browser-integration-tests/suites/tracing/interactions/spans/test.ts +++ b/dev-packages/browser-integration-tests/suites/tracing/interactions/spans/test.ts @@ -1,6 +1,12 @@ import { expect } from '@playwright/test'; import { SDK_VERSION } from '@sentry/core'; import { + SENTRY_IS_LOCALHOST, + CULTURE_CALENDAR, + CULTURE_LOCALE, + CULTURE_TIMEZONE, + URL_FULL, + BROWSER_WEB_VITAL_INP_TARGET, SENTRY_IDLE_SPAN_FINISH_REASON, SENTRY_SEGMENT_ID, SENTRY_SEGMENT_NAME, @@ -51,20 +57,20 @@ sentryTest('captures streamed interaction span tree. @firefox', async ({ browser expect(interactionSegmentSpan).toEqual({ attributes: { - 'sentry.is_localhost': { value: false, type: 'boolean' }, + [SENTRY_IS_LOCALHOST]: { value: false, type: 'boolean' }, [SENTRY_TRACE_LIFECYCLE]: { type: 'string', value: 'stream', }, - 'culture.calendar': { + [CULTURE_CALENDAR]: { type: 'string', value: expect.any(String), }, - 'culture.locale': { + [CULTURE_LOCALE]: { type: 'string', value: expect.any(String), }, - 'culture.timezone': { + [CULTURE_TIMEZONE]: { type: 'string', value: expect.any(String), }, @@ -72,7 +78,7 @@ sentryTest('captures streamed interaction span tree. @firefox', async ({ browser type: 'string', value: expect.any(String), }, - 'url.full': { + [URL_FULL]: { type: 'string', value: expect.any(String), }, @@ -136,7 +142,7 @@ sentryTest('captures streamed interaction span tree. @firefox', async ({ browser const interactionSpan = interactionSpanTree.find(span => getSpanOp(span) === 'ui.interaction.click'); expect(interactionSpan).toEqual({ attributes: { - 'sentry.is_localhost': { value: false, type: 'boolean' }, + [SENTRY_IS_LOCALHOST]: { value: false, type: 'boolean' }, [SENTRY_TRACE_LIFECYCLE]: { type: 'string', value: 'stream', @@ -173,7 +179,7 @@ sentryTest('captures streamed interaction span tree. @firefox', async ({ browser type: 'string', value: 'production', }, - 'browser.web_vital.inp.target': { + [BROWSER_WEB_VITAL_INP_TARGET]: { type: 'string', value: 'body > button.clicked', }, diff --git a/dev-packages/browser-integration-tests/suites/tracing/metrics/connection-rtt-navigation/test.ts b/dev-packages/browser-integration-tests/suites/tracing/metrics/connection-rtt-navigation/test.ts index 0bb6a442459a..ef019fa0dc56 100644 --- a/dev-packages/browser-integration-tests/suites/tracing/metrics/connection-rtt-navigation/test.ts +++ b/dev-packages/browser-integration-tests/suites/tracing/metrics/connection-rtt-navigation/test.ts @@ -2,7 +2,11 @@ import { expect } from '@playwright/test'; import { sentryTest } from '../../../../utils/fixtures'; import { shouldSkipTracingTest } from '../../../../utils/helpers'; import { waitForStreamedSpanAndTraceHeaderOnUrl } from '../../../../utils/spanUtils'; -import { NETWORK_CONNECTION_RTT } from '@sentry/conventions/attributes'; +import { + BROWSER_WEB_VITAL_FCP_VALUE, + BROWSER_WEB_VITAL_TTFB_VALUE, + NETWORK_CONNECTION_RTT, +} from '@sentry/conventions/attributes'; sentryTest( 'records connection RTT on pageload and navigation spans', @@ -14,7 +18,7 @@ sentryTest( expect(pageload.attributes[NETWORK_CONNECTION_RTT]).toEqual({ type: 'integer', value: 0 }); expect(navigation.attributes[NETWORK_CONNECTION_RTT]).toEqual(pageload.attributes[NETWORK_CONNECTION_RTT]); - expect(navigation.attributes['browser.web_vital.fcp.value']).toBeUndefined(); - expect(navigation.attributes['browser.web_vital.ttfb.value']).toBeUndefined(); + expect(navigation.attributes[BROWSER_WEB_VITAL_FCP_VALUE]).toBeUndefined(); + expect(navigation.attributes[BROWSER_WEB_VITAL_TTFB_VALUE]).toBeUndefined(); }, ); diff --git a/dev-packages/browser-integration-tests/suites/tracing/metrics/handlers-lcp/test.ts b/dev-packages/browser-integration-tests/suites/tracing/metrics/handlers-lcp/test.ts index f54522520037..c3e4bbc30be8 100644 --- a/dev-packages/browser-integration-tests/suites/tracing/metrics/handlers-lcp/test.ts +++ b/dev-packages/browser-integration-tests/suites/tracing/metrics/handlers-lcp/test.ts @@ -1,3 +1,8 @@ +import { + BROWSER_WEB_VITAL_LCP_VALUE, + BROWSER_WEB_VITAL_LCP_ELEMENT, + BROWSER_WEB_VITAL_LCP_SIZE, +} from '@sentry/conventions/attributes'; import type { Route } from '@playwright/test'; import { expect } from '@playwright/test'; import { sentryTest } from '../../../../utils/fixtures'; @@ -26,9 +31,9 @@ sentryTest( await page.locator('button').click(); const lcpSpan = await lcpPromise; - expect(lcpSpan.attributes['browser.web_vital.lcp.value']?.value).toBeGreaterThan(0); - expect(lcpSpan.attributes['browser.web_vital.lcp.element']).toEqual({ type: 'string', value: 'body > img' }); - expect(lcpSpan.attributes['browser.web_vital.lcp.size']).toEqual({ type: 'integer', value: 107400 }); + expect(lcpSpan.attributes[BROWSER_WEB_VITAL_LCP_VALUE]?.value).toBeGreaterThan(0); + expect(lcpSpan.attributes[BROWSER_WEB_VITAL_LCP_ELEMENT]).toEqual({ type: 'string', value: 'body > img' }); + expect(lcpSpan.attributes[BROWSER_WEB_VITAL_LCP_SIZE]).toEqual({ type: 'integer', value: 107400 }); expect(await page.evaluate('window._LCP')).toBe(107400); expect(await page.evaluate('window._LCP2')).toBe(107400); expect(await page.evaluate('window._LCP3')).toBeUndefined(); diff --git a/dev-packages/browser-integration-tests/suites/tracing/metrics/pageload-browser-spans/test.ts b/dev-packages/browser-integration-tests/suites/tracing/metrics/pageload-browser-spans/test.ts index 80aab1164446..977dbd399033 100644 --- a/dev-packages/browser-integration-tests/suites/tracing/metrics/pageload-browser-spans/test.ts +++ b/dev-packages/browser-integration-tests/suites/tracing/metrics/pageload-browser-spans/test.ts @@ -1,3 +1,4 @@ +import { URL_FULL } from '@sentry/conventions/attributes'; import { expect } from '@playwright/test'; import { sentryTest } from '../../../../utils/fixtures'; import { shouldSkipTracingTest } from '../../../../utils/helpers'; @@ -22,7 +23,7 @@ sentryTest('adds browser performance spans to the pageload segment', async ({ ge const matches = browserSpans.filter(span => getSpanOp(span) === op); expect(matches).toHaveLength(1); expect(matches[0].name).toBe(name); - expect(matches[0].attributes['url.full']).toEqual({ type: 'string', value: page.url() }); + expect(matches[0].attributes[URL_FULL]).toEqual({ type: 'string', value: page.url() }); expect(matches[0].parent_span_id).toBe(pageload.span_id); expect(matches[0].trace_id).toBe(pageload.trace_id); }); diff --git a/dev-packages/browser-integration-tests/suites/tracing/metrics/pageload-resource-spans-static/test.ts b/dev-packages/browser-integration-tests/suites/tracing/metrics/pageload-resource-spans-static/test.ts index 276234385fae..c0b943433605 100644 --- a/dev-packages/browser-integration-tests/suites/tracing/metrics/pageload-resource-spans-static/test.ts +++ b/dev-packages/browser-integration-tests/suites/tracing/metrics/pageload-resource-spans-static/test.ts @@ -1,4 +1,32 @@ -import { SENTRY_OP, SENTRY_ORIGIN } from '@sentry/conventions/attributes'; +import { + HTTP_REQUEST_SAME_ORIGIN, + URL_FULL, + HTTP_RESPONSE_BODY_DECODED_SIZE, + HTTP_RESPONSE_BODY_SIZE, + HTTP_RESPONSE_SIZE, + HTTP_REQUEST_CONNECT_START, + HTTP_REQUEST_CONNECTION_END, + HTTP_REQUEST_DOMAIN_LOOKUP_END, + HTTP_REQUEST_DOMAIN_LOOKUP_START, + HTTP_REQUEST_FETCH_START, + HTTP_REQUEST_REDIRECT_END, + HTTP_REQUEST_REDIRECT_START, + HTTP_REQUEST_REQUEST_START, + HTTP_REQUEST_SECURE_CONNECTION_START, + HTTP_REQUEST_WORKER_START, + HTTP_REQUEST_RESPONSE_END, + HTTP_REQUEST_RESPONSE_START, + HTTP_REQUEST_TIME_TO_FIRST_BYTE, + NETWORK_PROTOCOL_NAME, + NETWORK_PROTOCOL_VERSION, + SERVER_ADDRESS, + URL_DOMAIN, + URL_SCHEME, + HTTP_RESPONSE_STATUS_CODE, + RESOURCE_RENDER_BLOCKING_STATUS, + SENTRY_OP, + SENTRY_ORIGIN, +} from '@sentry/conventions/attributes'; import type { Route } from '@playwright/test'; import { expect } from '@playwright/test'; import { type Event } from '@sentry/core'; @@ -69,8 +97,8 @@ sentryTest('adds resource spans to pageload transaction', async ({ getLocalTestU // The init bundle script is served from the test origin: its description is origin-relative, // but `url.full` retains the full absolute URL (needed for span description inference). const sameOriginScriptSpan = scriptSpans?.find(({ description }) => description === '/init.bundle.js'); - expect(sameOriginScriptSpan?.data?.['http.request.same_origin']).toBe(true); - expect(sameOriginScriptSpan?.data?.['url.full']).toMatch(/^https?:\/\/.+\/init\.bundle\.js$/); + expect(sameOriginScriptSpan?.data?.[HTTP_REQUEST_SAME_ORIGIN]).toBe(true); + expect(sameOriginScriptSpan?.data?.[URL_FULL]).toMatch(/^https?:\/\/.+\/init\.bundle\.js$/); const customScriptSpan = scriptSpans?.find( ({ description }) => description === 'https://sentry-test-site.example/path/to/script.js', @@ -78,37 +106,37 @@ sentryTest('adds resource spans to pageload transaction', async ({ getLocalTestU expect(imgSpan).toEqual({ data: { - 'http.response.body.decoded_size': expect.any(Number), - 'http.response.body.size': expect.any(Number), - 'http.response.size': expect.any(Number), - 'http.request.connect_start': expect.any(Number), - 'http.request.connection_end': expect.any(Number), - 'http.request.domain_lookup_end': expect.any(Number), - 'http.request.domain_lookup_start': expect.any(Number), - 'http.request.fetch_start': expect.any(Number), - 'http.request.redirect_end': expect.any(Number), - 'http.request.redirect_start': expect.any(Number), - 'http.request.request_start': expect.any(Number), - 'http.request.secure_connection_start': expect.any(Number), - 'http.request.worker_start': expect.any(Number), - 'http.request.response_end': expect.any(Number), - 'http.request.response_start': expect.any(Number), - 'http.request.time_to_first_byte': expect.any(Number), - 'network.protocol.name': '', - 'network.protocol.version': 'unknown', + [HTTP_RESPONSE_BODY_DECODED_SIZE]: expect.any(Number), + [HTTP_RESPONSE_BODY_SIZE]: expect.any(Number), + [HTTP_RESPONSE_SIZE]: expect.any(Number), + [HTTP_REQUEST_CONNECT_START]: expect.any(Number), + [HTTP_REQUEST_CONNECTION_END]: expect.any(Number), + [HTTP_REQUEST_DOMAIN_LOOKUP_END]: expect.any(Number), + [HTTP_REQUEST_DOMAIN_LOOKUP_START]: expect.any(Number), + [HTTP_REQUEST_FETCH_START]: expect.any(Number), + [HTTP_REQUEST_REDIRECT_END]: expect.any(Number), + [HTTP_REQUEST_REDIRECT_START]: expect.any(Number), + [HTTP_REQUEST_REQUEST_START]: expect.any(Number), + [HTTP_REQUEST_SECURE_CONNECTION_START]: expect.any(Number), + [HTTP_REQUEST_WORKER_START]: expect.any(Number), + [HTTP_REQUEST_RESPONSE_END]: expect.any(Number), + [HTTP_REQUEST_RESPONSE_START]: expect.any(Number), + [HTTP_REQUEST_TIME_TO_FIRST_BYTE]: expect.any(Number), + [NETWORK_PROTOCOL_NAME]: '', + [NETWORK_PROTOCOL_VERSION]: 'unknown', [SENTRY_OP]: 'resource.img', [SENTRY_ORIGIN]: 'auto.resource.browser.metrics', - 'server.address': 'sentry-test-site.example', - 'url.domain': 'sentry-test-site.example', - 'http.request.same_origin': false, - 'url.scheme': 'https', - 'url.full': 'https://sentry-test-site.example/path/to/image.svg', + [SERVER_ADDRESS]: 'sentry-test-site.example', + [URL_DOMAIN]: 'sentry-test-site.example', + [HTTP_REQUEST_SAME_ORIGIN]: false, + [URL_SCHEME]: 'https', + [URL_FULL]: 'https://sentry-test-site.example/path/to/image.svg', // WebKit reports `deliveryType` as of Playwright 1.63's build, but still no response status // or render blocking status. 'http.response_delivery_type': '', ...(!isWebkitRun && { - 'http.response.status_code': expect.any(Number), - 'resource.render_blocking_status': 'non-blocking', + [HTTP_RESPONSE_STATUS_CODE]: expect.any(Number), + [RESOURCE_RENDER_BLOCKING_STATUS]: 'non-blocking', }), }, description: 'https://sentry-test-site.example/path/to/image.svg', @@ -125,43 +153,43 @@ sentryTest('adds resource spans to pageload transaction', async ({ getLocalTestU // range check: TTFB is reasonably <10 seconds, which is really a check that we report it in // seconds rather than milliseconds. WebKit resolves these intercepted routes without measurable // delay, so only the other engines are held to a non-zero value. - const imgSpanTtfb = imgSpan?.data['http.request.time_to_first_byte']; + const imgSpanTtfb = imgSpan?.data[HTTP_REQUEST_TIME_TO_FIRST_BYTE]; expect(imgSpanTtfb).toBeGreaterThan(isWebkitRun ? -1 : 0); expect(imgSpanTtfb).toBeLessThan(10); expect(linkSpan).toEqual({ data: { - 'http.response.body.decoded_size': expect.any(Number), - 'http.response.body.size': expect.any(Number), - 'http.response.size': expect.any(Number), - 'http.request.connect_start': expect.any(Number), - 'http.request.connection_end': expect.any(Number), - 'http.request.domain_lookup_end': expect.any(Number), - 'http.request.domain_lookup_start': expect.any(Number), - 'http.request.fetch_start': expect.any(Number), - 'http.request.redirect_end': expect.any(Number), - 'http.request.redirect_start': expect.any(Number), - 'http.request.request_start': expect.any(Number), - 'http.request.secure_connection_start': expect.any(Number), - 'http.request.worker_start': expect.any(Number), - 'http.request.response_end': expect.any(Number), - 'http.request.response_start': expect.any(Number), - 'http.request.time_to_first_byte': expect.any(Number), - 'network.protocol.name': '', - 'network.protocol.version': 'unknown', + [HTTP_RESPONSE_BODY_DECODED_SIZE]: expect.any(Number), + [HTTP_RESPONSE_BODY_SIZE]: expect.any(Number), + [HTTP_RESPONSE_SIZE]: expect.any(Number), + [HTTP_REQUEST_CONNECT_START]: expect.any(Number), + [HTTP_REQUEST_CONNECTION_END]: expect.any(Number), + [HTTP_REQUEST_DOMAIN_LOOKUP_END]: expect.any(Number), + [HTTP_REQUEST_DOMAIN_LOOKUP_START]: expect.any(Number), + [HTTP_REQUEST_FETCH_START]: expect.any(Number), + [HTTP_REQUEST_REDIRECT_END]: expect.any(Number), + [HTTP_REQUEST_REDIRECT_START]: expect.any(Number), + [HTTP_REQUEST_REQUEST_START]: expect.any(Number), + [HTTP_REQUEST_SECURE_CONNECTION_START]: expect.any(Number), + [HTTP_REQUEST_WORKER_START]: expect.any(Number), + [HTTP_REQUEST_RESPONSE_END]: expect.any(Number), + [HTTP_REQUEST_RESPONSE_START]: expect.any(Number), + [HTTP_REQUEST_TIME_TO_FIRST_BYTE]: expect.any(Number), + [NETWORK_PROTOCOL_NAME]: '', + [NETWORK_PROTOCOL_VERSION]: 'unknown', [SENTRY_OP]: 'resource.link', [SENTRY_ORIGIN]: 'auto.resource.browser.metrics', - 'server.address': 'sentry-test-site.example', - 'url.domain': 'sentry-test-site.example', - 'http.request.same_origin': false, - 'url.scheme': 'https', - 'url.full': 'https://sentry-test-site.example/path/to/style.css', + [SERVER_ADDRESS]: 'sentry-test-site.example', + [URL_DOMAIN]: 'sentry-test-site.example', + [HTTP_REQUEST_SAME_ORIGIN]: false, + [URL_SCHEME]: 'https', + [URL_FULL]: 'https://sentry-test-site.example/path/to/style.css', // WebKit reports `deliveryType` as of Playwright 1.63's build, but still no response status // or render blocking status. 'http.response_delivery_type': '', ...(!isWebkitRun && { - 'http.response.status_code': expect.any(Number), - 'resource.render_blocking_status': 'non-blocking', + [HTTP_RESPONSE_STATUS_CODE]: expect.any(Number), + [RESOURCE_RENDER_BLOCKING_STATUS]: 'non-blocking', }), }, description: 'https://sentry-test-site.example/path/to/style.css', @@ -177,37 +205,37 @@ sentryTest('adds resource spans to pageload transaction', async ({ getLocalTestU expect(customScriptSpan).toEqual({ data: { - 'http.response.body.decoded_size': expect.any(Number), - 'http.response.body.size': expect.any(Number), - 'http.response.size': expect.any(Number), - 'http.request.connection_end': expect.any(Number), - 'http.request.connect_start': expect.any(Number), - 'http.request.domain_lookup_end': expect.any(Number), - 'http.request.domain_lookup_start': expect.any(Number), - 'http.request.fetch_start': expect.any(Number), - 'http.request.redirect_end': expect.any(Number), - 'http.request.redirect_start': expect.any(Number), - 'http.request.request_start': expect.any(Number), - 'http.request.secure_connection_start': expect.any(Number), - 'http.request.worker_start': expect.any(Number), - 'http.request.response_end': expect.any(Number), - 'http.request.response_start': expect.any(Number), - 'http.request.time_to_first_byte': expect.any(Number), - 'network.protocol.name': '', - 'network.protocol.version': 'unknown', - 'sentry.op': 'resource.script', - 'sentry.origin': 'auto.resource.browser.metrics', - 'server.address': 'sentry-test-site.example', - 'url.domain': 'sentry-test-site.example', - 'http.request.same_origin': false, - 'url.scheme': 'https', - 'url.full': 'https://sentry-test-site.example/path/to/script.js', + [HTTP_RESPONSE_BODY_DECODED_SIZE]: expect.any(Number), + [HTTP_RESPONSE_BODY_SIZE]: expect.any(Number), + [HTTP_RESPONSE_SIZE]: expect.any(Number), + [HTTP_REQUEST_CONNECTION_END]: expect.any(Number), + [HTTP_REQUEST_CONNECT_START]: expect.any(Number), + [HTTP_REQUEST_DOMAIN_LOOKUP_END]: expect.any(Number), + [HTTP_REQUEST_DOMAIN_LOOKUP_START]: expect.any(Number), + [HTTP_REQUEST_FETCH_START]: expect.any(Number), + [HTTP_REQUEST_REDIRECT_END]: expect.any(Number), + [HTTP_REQUEST_REDIRECT_START]: expect.any(Number), + [HTTP_REQUEST_REQUEST_START]: expect.any(Number), + [HTTP_REQUEST_SECURE_CONNECTION_START]: expect.any(Number), + [HTTP_REQUEST_WORKER_START]: expect.any(Number), + [HTTP_REQUEST_RESPONSE_END]: expect.any(Number), + [HTTP_REQUEST_RESPONSE_START]: expect.any(Number), + [HTTP_REQUEST_TIME_TO_FIRST_BYTE]: expect.any(Number), + [NETWORK_PROTOCOL_NAME]: '', + [NETWORK_PROTOCOL_VERSION]: 'unknown', + [SENTRY_OP]: 'resource.script', + [SENTRY_ORIGIN]: 'auto.resource.browser.metrics', + [SERVER_ADDRESS]: 'sentry-test-site.example', + [URL_DOMAIN]: 'sentry-test-site.example', + [HTTP_REQUEST_SAME_ORIGIN]: false, + [URL_SCHEME]: 'https', + [URL_FULL]: 'https://sentry-test-site.example/path/to/script.js', // WebKit reports `deliveryType` as of Playwright 1.63's build, but still no response status // or render blocking status. 'http.response_delivery_type': '', ...(!isWebkitRun && { - 'http.response.status_code': expect.any(Number), - 'resource.render_blocking_status': 'non-blocking', + [HTTP_RESPONSE_STATUS_CODE]: expect.any(Number), + [RESOURCE_RENDER_BLOCKING_STATUS]: 'non-blocking', }), }, description: 'https://sentry-test-site.example/path/to/script.js', diff --git a/dev-packages/browser-integration-tests/suites/tracing/metrics/pageload-resource-spans/test.ts b/dev-packages/browser-integration-tests/suites/tracing/metrics/pageload-resource-spans/test.ts index 02749e913747..f9b35fa67c6a 100644 --- a/dev-packages/browser-integration-tests/suites/tracing/metrics/pageload-resource-spans/test.ts +++ b/dev-packages/browser-integration-tests/suites/tracing/metrics/pageload-resource-spans/test.ts @@ -1,4 +1,32 @@ -import { SENTRY_OP, SENTRY_ORIGIN } from '@sentry/conventions/attributes'; +import { + URL_DOMAIN, + URL_FULL, + HTTP_REQUEST_SAME_ORIGIN, + HTTP_RESPONSE_BODY_DECODED_SIZE, + HTTP_RESPONSE_BODY_SIZE, + HTTP_RESPONSE_SIZE, + HTTP_REQUEST_CONNECT_START, + HTTP_REQUEST_CONNECTION_END, + HTTP_REQUEST_DOMAIN_LOOKUP_END, + HTTP_REQUEST_DOMAIN_LOOKUP_START, + HTTP_REQUEST_FETCH_START, + HTTP_REQUEST_REDIRECT_END, + HTTP_REQUEST_REDIRECT_START, + HTTP_REQUEST_REQUEST_START, + HTTP_REQUEST_SECURE_CONNECTION_START, + HTTP_REQUEST_WORKER_START, + HTTP_REQUEST_RESPONSE_END, + HTTP_REQUEST_RESPONSE_START, + HTTP_REQUEST_TIME_TO_FIRST_BYTE, + NETWORK_PROTOCOL_NAME, + NETWORK_PROTOCOL_VERSION, + SERVER_ADDRESS, + URL_SCHEME, + HTTP_RESPONSE_STATUS_CODE, + RESOURCE_RENDER_BLOCKING_STATUS, + SENTRY_OP, + SENTRY_ORIGIN, +} from '@sentry/conventions/attributes'; import type { Route } from '@playwright/test'; import { expect } from '@playwright/test'; import { sentryTest } from '../../../../utils/fixtures'; @@ -47,7 +75,7 @@ sentryTest( await page.goto(url); const pageload = await pageloadPromise; await page.evaluate(() => (window as any).Sentry.flush()); - const externalResources = spans.filter(span => span.attributes['url.domain']?.value === 'sentry-test-site.example'); + const externalResources = spans.filter(span => span.attributes[URL_DOMAIN]?.value === 'sentry-test-site.example'); expect(externalResources).toHaveLength(3); externalResources.forEach(span => { expect(span.parent_span_id).toBe(pageload.span_id); @@ -60,10 +88,10 @@ sentryTest( const imgSpan = resourceSpans.find(span => getSpanOp(span) === 'resource.img'); const linkSpan = resourceSpans.find(span => getSpanOp(span) === 'resource.link'); const customScriptSpan = scriptSpans.find( - span => span.attributes['url.domain']?.value === 'sentry-test-site.example', + span => span.attributes[URL_DOMAIN]?.value === 'sentry-test-site.example', ); const sameOriginScriptSpan = scriptSpans.find( - span => span.attributes['url.full']?.value === new URL('init.bundle.js', url).href, + span => span.attributes[URL_FULL]?.value === new URL('init.bundle.js', url).href, ); const spanId = pageload.span_id; const traceId = pageload.trace_id; @@ -71,59 +99,59 @@ sentryTest( if ((process.env.PW_BUNDLE || '').startsWith('bundle')) { expectedScripts.push(new URL('cdn.bundle.js', url).href); } - expect(scriptSpans.map(span => span.attributes['url.full']?.value).sort()).toEqual(expectedScripts.sort()); + expect(scriptSpans.map(span => span.attributes[URL_FULL]?.value).sort()).toEqual(expectedScripts.sort()); expect(scriptSpans.map(span => span.parent_span_id)).toEqual(expectedScripts.map(() => spanId)); expect(sameOriginScriptSpan?.name).toBe('sentry-test.io'); - expect(sameOriginScriptSpan?.attributes['http.request.same_origin']).toEqual({ type: 'boolean', value: true }); + expect(sameOriginScriptSpan?.attributes[HTTP_REQUEST_SAME_ORIGIN]).toEqual({ type: 'boolean', value: true }); expect(imgSpan).toMatchObject({ attributes: { - 'http.response.body.decoded_size': { + [HTTP_RESPONSE_BODY_DECODED_SIZE]: { type: expect.stringMatching(/^(integer|double)$/), value: expect.any(Number), }, - 'http.response.body.size': { type: expect.stringMatching(/^(integer|double)$/), value: expect.any(Number) }, - 'http.response.size': { type: expect.stringMatching(/^(integer|double)$/), value: expect.any(Number) }, - 'http.request.connect_start': { type: expect.stringMatching(/^(integer|double)$/), value: expect.any(Number) }, - 'http.request.connection_end': { type: expect.stringMatching(/^(integer|double)$/), value: expect.any(Number) }, - 'http.request.domain_lookup_end': { + [HTTP_RESPONSE_BODY_SIZE]: { type: expect.stringMatching(/^(integer|double)$/), value: expect.any(Number) }, + [HTTP_RESPONSE_SIZE]: { type: expect.stringMatching(/^(integer|double)$/), value: expect.any(Number) }, + [HTTP_REQUEST_CONNECT_START]: { type: expect.stringMatching(/^(integer|double)$/), value: expect.any(Number) }, + [HTTP_REQUEST_CONNECTION_END]: { type: expect.stringMatching(/^(integer|double)$/), value: expect.any(Number) }, + [HTTP_REQUEST_DOMAIN_LOOKUP_END]: { type: expect.stringMatching(/^(integer|double)$/), value: expect.any(Number), }, - 'http.request.domain_lookup_start': { + [HTTP_REQUEST_DOMAIN_LOOKUP_START]: { type: expect.stringMatching(/^(integer|double)$/), value: expect.any(Number), }, - 'http.request.fetch_start': { type: expect.stringMatching(/^(integer|double)$/), value: expect.any(Number) }, - 'http.request.redirect_end': { type: expect.stringMatching(/^(integer|double)$/), value: expect.any(Number) }, - 'http.request.redirect_start': { type: expect.stringMatching(/^(integer|double)$/), value: expect.any(Number) }, - 'http.request.request_start': { type: expect.stringMatching(/^(integer|double)$/), value: expect.any(Number) }, - 'http.request.secure_connection_start': { + [HTTP_REQUEST_FETCH_START]: { type: expect.stringMatching(/^(integer|double)$/), value: expect.any(Number) }, + [HTTP_REQUEST_REDIRECT_END]: { type: expect.stringMatching(/^(integer|double)$/), value: expect.any(Number) }, + [HTTP_REQUEST_REDIRECT_START]: { type: expect.stringMatching(/^(integer|double)$/), value: expect.any(Number) }, + [HTTP_REQUEST_REQUEST_START]: { type: expect.stringMatching(/^(integer|double)$/), value: expect.any(Number) }, + [HTTP_REQUEST_SECURE_CONNECTION_START]: { type: expect.stringMatching(/^(integer|double)$/), value: expect.any(Number), }, - 'http.request.worker_start': { type: expect.stringMatching(/^(integer|double)$/), value: expect.any(Number) }, - 'http.request.response_end': { type: expect.stringMatching(/^(integer|double)$/), value: expect.any(Number) }, - 'http.request.response_start': { type: expect.stringMatching(/^(integer|double)$/), value: expect.any(Number) }, - 'http.request.time_to_first_byte': { + [HTTP_REQUEST_WORKER_START]: { type: expect.stringMatching(/^(integer|double)$/), value: expect.any(Number) }, + [HTTP_REQUEST_RESPONSE_END]: { type: expect.stringMatching(/^(integer|double)$/), value: expect.any(Number) }, + [HTTP_REQUEST_RESPONSE_START]: { type: expect.stringMatching(/^(integer|double)$/), value: expect.any(Number) }, + [HTTP_REQUEST_TIME_TO_FIRST_BYTE]: { type: expect.stringMatching(/^(integer|double)$/), value: expect.any(Number), }, - 'network.protocol.name': { type: 'string', value: '' }, - 'network.protocol.version': { type: 'string', value: 'unknown' }, + [NETWORK_PROTOCOL_NAME]: { type: 'string', value: '' }, + [NETWORK_PROTOCOL_VERSION]: { type: 'string', value: 'unknown' }, [SENTRY_OP]: { type: 'string', value: 'resource.img' }, [SENTRY_ORIGIN]: { type: 'string', value: 'auto.resource.browser.metrics' }, - 'server.address': { type: 'string', value: 'sentry-test-site.example' }, - 'url.domain': { type: 'string', value: 'sentry-test-site.example' }, - 'http.request.same_origin': { type: 'boolean', value: false }, - 'url.scheme': { type: 'string', value: 'https' }, - 'url.full': { type: 'string', value: 'https://sentry-test-site.example/path/to/image.svg' }, + [SERVER_ADDRESS]: { type: 'string', value: 'sentry-test-site.example' }, + [URL_DOMAIN]: { type: 'string', value: 'sentry-test-site.example' }, + [HTTP_REQUEST_SAME_ORIGIN]: { type: 'boolean', value: false }, + [URL_SCHEME]: { type: 'string', value: 'https' }, + [URL_FULL]: { type: 'string', value: 'https://sentry-test-site.example/path/to/image.svg' }, // WebKit reports `deliveryType` as of Playwright 1.63's build, but still no response status // or render blocking status. 'http.response_delivery_type': { type: 'string', value: '' }, ...(!isWebkitRun && { - 'http.response.status_code': { type: expect.stringMatching(/^(integer|double)$/), value: expect.any(Number) }, - 'resource.render_blocking_status': { type: 'string', value: 'non-blocking' }, + [HTTP_RESPONSE_STATUS_CODE]: { type: expect.stringMatching(/^(integer|double)$/), value: expect.any(Number) }, + [RESOURCE_RENDER_BLOCKING_STATUS]: { type: 'string', value: 'non-blocking' }, }), }, name: 'sentry-test-site.example', @@ -138,58 +166,58 @@ sentryTest( // range check: TTFB is reasonably <10 seconds, which is really a check that we report it in // seconds rather than milliseconds. WebKit resolves these intercepted routes without measurable // delay, so only the other engines are held to a non-zero value. - const imgSpanTtfb = imgSpan?.attributes['http.request.time_to_first_byte']?.value; + const imgSpanTtfb = imgSpan?.attributes[HTTP_REQUEST_TIME_TO_FIRST_BYTE]?.value; expect(imgSpanTtfb).toBeGreaterThan(isWebkitRun ? -1 : 0); expect(imgSpanTtfb).toBeLessThan(10); expect(linkSpan).toMatchObject({ attributes: { - 'http.response.body.decoded_size': { + [HTTP_RESPONSE_BODY_DECODED_SIZE]: { type: expect.stringMatching(/^(integer|double)$/), value: expect.any(Number), }, - 'http.response.body.size': { type: expect.stringMatching(/^(integer|double)$/), value: expect.any(Number) }, - 'http.response.size': { type: expect.stringMatching(/^(integer|double)$/), value: expect.any(Number) }, - 'http.request.connect_start': { type: expect.stringMatching(/^(integer|double)$/), value: expect.any(Number) }, - 'http.request.connection_end': { type: expect.stringMatching(/^(integer|double)$/), value: expect.any(Number) }, - 'http.request.domain_lookup_end': { + [HTTP_RESPONSE_BODY_SIZE]: { type: expect.stringMatching(/^(integer|double)$/), value: expect.any(Number) }, + [HTTP_RESPONSE_SIZE]: { type: expect.stringMatching(/^(integer|double)$/), value: expect.any(Number) }, + [HTTP_REQUEST_CONNECT_START]: { type: expect.stringMatching(/^(integer|double)$/), value: expect.any(Number) }, + [HTTP_REQUEST_CONNECTION_END]: { type: expect.stringMatching(/^(integer|double)$/), value: expect.any(Number) }, + [HTTP_REQUEST_DOMAIN_LOOKUP_END]: { type: expect.stringMatching(/^(integer|double)$/), value: expect.any(Number), }, - 'http.request.domain_lookup_start': { + [HTTP_REQUEST_DOMAIN_LOOKUP_START]: { type: expect.stringMatching(/^(integer|double)$/), value: expect.any(Number), }, - 'http.request.fetch_start': { type: expect.stringMatching(/^(integer|double)$/), value: expect.any(Number) }, - 'http.request.redirect_end': { type: expect.stringMatching(/^(integer|double)$/), value: expect.any(Number) }, - 'http.request.redirect_start': { type: expect.stringMatching(/^(integer|double)$/), value: expect.any(Number) }, - 'http.request.request_start': { type: expect.stringMatching(/^(integer|double)$/), value: expect.any(Number) }, - 'http.request.secure_connection_start': { + [HTTP_REQUEST_FETCH_START]: { type: expect.stringMatching(/^(integer|double)$/), value: expect.any(Number) }, + [HTTP_REQUEST_REDIRECT_END]: { type: expect.stringMatching(/^(integer|double)$/), value: expect.any(Number) }, + [HTTP_REQUEST_REDIRECT_START]: { type: expect.stringMatching(/^(integer|double)$/), value: expect.any(Number) }, + [HTTP_REQUEST_REQUEST_START]: { type: expect.stringMatching(/^(integer|double)$/), value: expect.any(Number) }, + [HTTP_REQUEST_SECURE_CONNECTION_START]: { type: expect.stringMatching(/^(integer|double)$/), value: expect.any(Number), }, - 'http.request.worker_start': { type: expect.stringMatching(/^(integer|double)$/), value: expect.any(Number) }, - 'http.request.response_end': { type: expect.stringMatching(/^(integer|double)$/), value: expect.any(Number) }, - 'http.request.response_start': { type: expect.stringMatching(/^(integer|double)$/), value: expect.any(Number) }, - 'http.request.time_to_first_byte': { + [HTTP_REQUEST_WORKER_START]: { type: expect.stringMatching(/^(integer|double)$/), value: expect.any(Number) }, + [HTTP_REQUEST_RESPONSE_END]: { type: expect.stringMatching(/^(integer|double)$/), value: expect.any(Number) }, + [HTTP_REQUEST_RESPONSE_START]: { type: expect.stringMatching(/^(integer|double)$/), value: expect.any(Number) }, + [HTTP_REQUEST_TIME_TO_FIRST_BYTE]: { type: expect.stringMatching(/^(integer|double)$/), value: expect.any(Number), }, - 'network.protocol.name': { type: 'string', value: '' }, - 'network.protocol.version': { type: 'string', value: 'unknown' }, + [NETWORK_PROTOCOL_NAME]: { type: 'string', value: '' }, + [NETWORK_PROTOCOL_VERSION]: { type: 'string', value: 'unknown' }, [SENTRY_OP]: { type: 'string', value: 'resource.link' }, [SENTRY_ORIGIN]: { type: 'string', value: 'auto.resource.browser.metrics' }, - 'server.address': { type: 'string', value: 'sentry-test-site.example' }, - 'url.domain': { type: 'string', value: 'sentry-test-site.example' }, - 'http.request.same_origin': { type: 'boolean', value: false }, - 'url.scheme': { type: 'string', value: 'https' }, - 'url.full': { type: 'string', value: 'https://sentry-test-site.example/path/to/style.css' }, + [SERVER_ADDRESS]: { type: 'string', value: 'sentry-test-site.example' }, + [URL_DOMAIN]: { type: 'string', value: 'sentry-test-site.example' }, + [HTTP_REQUEST_SAME_ORIGIN]: { type: 'boolean', value: false }, + [URL_SCHEME]: { type: 'string', value: 'https' }, + [URL_FULL]: { type: 'string', value: 'https://sentry-test-site.example/path/to/style.css' }, // WebKit reports `deliveryType` as of Playwright 1.63's build, but still no response status // or render blocking status. 'http.response_delivery_type': { type: 'string', value: '' }, ...(!isWebkitRun && { - 'http.response.status_code': { type: expect.stringMatching(/^(integer|double)$/), value: expect.any(Number) }, - 'resource.render_blocking_status': { type: 'string', value: 'non-blocking' }, + [HTTP_RESPONSE_STATUS_CODE]: { type: expect.stringMatching(/^(integer|double)$/), value: expect.any(Number) }, + [RESOURCE_RENDER_BLOCKING_STATUS]: { type: 'string', value: 'non-blocking' }, }), }, name: 'sentry-test-site.example', @@ -203,52 +231,52 @@ sentryTest( expect(customScriptSpan).toMatchObject({ attributes: { - 'http.response.body.decoded_size': { + [HTTP_RESPONSE_BODY_DECODED_SIZE]: { type: expect.stringMatching(/^(integer|double)$/), value: expect.any(Number), }, - 'http.response.body.size': { type: expect.stringMatching(/^(integer|double)$/), value: expect.any(Number) }, - 'http.response.size': { type: expect.stringMatching(/^(integer|double)$/), value: expect.any(Number) }, - 'http.request.connection_end': { type: expect.stringMatching(/^(integer|double)$/), value: expect.any(Number) }, - 'http.request.connect_start': { type: expect.stringMatching(/^(integer|double)$/), value: expect.any(Number) }, - 'http.request.domain_lookup_end': { + [HTTP_RESPONSE_BODY_SIZE]: { type: expect.stringMatching(/^(integer|double)$/), value: expect.any(Number) }, + [HTTP_RESPONSE_SIZE]: { type: expect.stringMatching(/^(integer|double)$/), value: expect.any(Number) }, + [HTTP_REQUEST_CONNECTION_END]: { type: expect.stringMatching(/^(integer|double)$/), value: expect.any(Number) }, + [HTTP_REQUEST_CONNECT_START]: { type: expect.stringMatching(/^(integer|double)$/), value: expect.any(Number) }, + [HTTP_REQUEST_DOMAIN_LOOKUP_END]: { type: expect.stringMatching(/^(integer|double)$/), value: expect.any(Number), }, - 'http.request.domain_lookup_start': { + [HTTP_REQUEST_DOMAIN_LOOKUP_START]: { type: expect.stringMatching(/^(integer|double)$/), value: expect.any(Number), }, - 'http.request.fetch_start': { type: expect.stringMatching(/^(integer|double)$/), value: expect.any(Number) }, - 'http.request.redirect_end': { type: expect.stringMatching(/^(integer|double)$/), value: expect.any(Number) }, - 'http.request.redirect_start': { type: expect.stringMatching(/^(integer|double)$/), value: expect.any(Number) }, - 'http.request.request_start': { type: expect.stringMatching(/^(integer|double)$/), value: expect.any(Number) }, - 'http.request.secure_connection_start': { + [HTTP_REQUEST_FETCH_START]: { type: expect.stringMatching(/^(integer|double)$/), value: expect.any(Number) }, + [HTTP_REQUEST_REDIRECT_END]: { type: expect.stringMatching(/^(integer|double)$/), value: expect.any(Number) }, + [HTTP_REQUEST_REDIRECT_START]: { type: expect.stringMatching(/^(integer|double)$/), value: expect.any(Number) }, + [HTTP_REQUEST_REQUEST_START]: { type: expect.stringMatching(/^(integer|double)$/), value: expect.any(Number) }, + [HTTP_REQUEST_SECURE_CONNECTION_START]: { type: expect.stringMatching(/^(integer|double)$/), value: expect.any(Number), }, - 'http.request.worker_start': { type: expect.stringMatching(/^(integer|double)$/), value: expect.any(Number) }, - 'http.request.response_end': { type: expect.stringMatching(/^(integer|double)$/), value: expect.any(Number) }, - 'http.request.response_start': { type: expect.stringMatching(/^(integer|double)$/), value: expect.any(Number) }, - 'http.request.time_to_first_byte': { + [HTTP_REQUEST_WORKER_START]: { type: expect.stringMatching(/^(integer|double)$/), value: expect.any(Number) }, + [HTTP_REQUEST_RESPONSE_END]: { type: expect.stringMatching(/^(integer|double)$/), value: expect.any(Number) }, + [HTTP_REQUEST_RESPONSE_START]: { type: expect.stringMatching(/^(integer|double)$/), value: expect.any(Number) }, + [HTTP_REQUEST_TIME_TO_FIRST_BYTE]: { type: expect.stringMatching(/^(integer|double)$/), value: expect.any(Number), }, - 'network.protocol.name': { type: 'string', value: '' }, - 'network.protocol.version': { type: 'string', value: 'unknown' }, - 'sentry.op': { type: 'string', value: 'resource.script' }, - 'sentry.origin': { type: 'string', value: 'auto.resource.browser.metrics' }, - 'server.address': { type: 'string', value: 'sentry-test-site.example' }, - 'url.domain': { type: 'string', value: 'sentry-test-site.example' }, - 'http.request.same_origin': { type: 'boolean', value: false }, - 'url.scheme': { type: 'string', value: 'https' }, - 'url.full': { type: 'string', value: 'https://sentry-test-site.example/path/to/script.js' }, + [NETWORK_PROTOCOL_NAME]: { type: 'string', value: '' }, + [NETWORK_PROTOCOL_VERSION]: { type: 'string', value: 'unknown' }, + [SENTRY_OP]: { type: 'string', value: 'resource.script' }, + [SENTRY_ORIGIN]: { type: 'string', value: 'auto.resource.browser.metrics' }, + [SERVER_ADDRESS]: { type: 'string', value: 'sentry-test-site.example' }, + [URL_DOMAIN]: { type: 'string', value: 'sentry-test-site.example' }, + [HTTP_REQUEST_SAME_ORIGIN]: { type: 'boolean', value: false }, + [URL_SCHEME]: { type: 'string', value: 'https' }, + [URL_FULL]: { type: 'string', value: 'https://sentry-test-site.example/path/to/script.js' }, // WebKit reports `deliveryType` as of Playwright 1.63's build, but still no response status // or render blocking status. 'http.response_delivery_type': { type: 'string', value: '' }, ...(!isWebkitRun && { - 'http.response.status_code': { type: expect.stringMatching(/^(integer|double)$/), value: expect.any(Number) }, - 'resource.render_blocking_status': { type: 'string', value: 'non-blocking' }, + [HTTP_RESPONSE_STATUS_CODE]: { type: expect.stringMatching(/^(integer|double)$/), value: expect.any(Number) }, + [RESOURCE_RENDER_BLOCKING_STATUS]: { type: 'string', value: 'non-blocking' }, }), }, name: 'sentry-test-site.example', diff --git a/dev-packages/browser-integration-tests/suites/tracing/metrics/web-vitals-cls/test.ts b/dev-packages/browser-integration-tests/suites/tracing/metrics/web-vitals-cls/test.ts index 853bc078697f..22e494e73447 100644 --- a/dev-packages/browser-integration-tests/suites/tracing/metrics/web-vitals-cls/test.ts +++ b/dev-packages/browser-integration-tests/suites/tracing/metrics/web-vitals-cls/test.ts @@ -1,3 +1,12 @@ +import { + SENTRY_OP, + SENTRY_ORIGIN, + SENTRY_EXCLUSIVE_TIME, + USER_AGENT_ORIGINAL, + SENTRY_SEGMENT_NAME, + SENTRY_PAGELOAD_SPAN_ID, + BROWSER_WEB_VITAL_CLS_VALUE, +} from '@sentry/conventions/attributes'; import type { Page } from '@playwright/test'; import { expect } from '@playwright/test'; import { sentryTest } from '../../../../utils/fixtures'; @@ -33,13 +42,13 @@ sentryTest('captures CLS as a streamed span with source attributes', async ({ ge const clsSpan = await clsSpanPromise; const pageloadSpan = await pageloadSpanPromise; - expect(clsSpan.attributes['sentry.op']).toEqual({ type: 'string', value: 'ui.webvital.cls' }); - expect(clsSpan.attributes['sentry.origin']).toEqual({ type: 'string', value: 'auto.http.browser.cls' }); - expect(clsSpan.attributes['sentry.exclusive_time']).toEqual({ type: 'integer', value: 0 }); - expect(clsSpan.attributes['user_agent.original']?.value).toEqual(expect.stringContaining('Chrome')); + expect(clsSpan.attributes[SENTRY_OP]).toEqual({ type: 'string', value: 'ui.webvital.cls' }); + expect(clsSpan.attributes[SENTRY_ORIGIN]).toEqual({ type: 'string', value: 'auto.http.browser.cls' }); + expect(clsSpan.attributes[SENTRY_EXCLUSIVE_TIME]).toEqual({ type: 'integer', value: 0 }); + expect(clsSpan.attributes[USER_AGENT_ORIGINAL]?.value).toEqual(expect.stringContaining('Chrome')); // Check the CLS span carries the segment name it belongs to - expect(clsSpan.attributes['sentry.segment.name']).toEqual({ type: 'string', value: 'Pageload' }); + expect(clsSpan.attributes[SENTRY_SEGMENT_NAME]).toEqual({ type: 'string', value: 'Pageload' }); // Check browser.web_vital.cls.source attributes expect(clsSpan.attributes['browser.web_vital.cls.source.1']?.value).toEqual( @@ -47,7 +56,7 @@ sentryTest('captures CLS as a streamed span with source attributes', async ({ ge ); // Check pageload span id is present - expect(clsSpan.attributes['sentry.pageload.span_id']?.value).toBe(pageloadSpan.span_id); + expect(clsSpan.attributes[SENTRY_PAGELOAD_SPAN_ID]?.value).toBe(pageloadSpan.span_id); // CLS is a point-in-time metric expect(clsSpan.start_timestamp).toEqual(clsSpan.end_timestamp); @@ -72,8 +81,8 @@ sentryTest('captures CLS as a streamed span with source attributes', async ({ ge await hidePage(page); const span = await clsPromise; - expect(span.attributes['browser.web_vital.cls.value']?.type).toBe('double'); - const value = span.attributes['browser.web_vital.cls.value']?.value; + expect(span.attributes[BROWSER_WEB_VITAL_CLS_VALUE]?.type).toBe('double'); + const value = span.attributes[BROWSER_WEB_VITAL_CLS_VALUE]?.value; expect(value).toBeGreaterThan(min); expect(value).toBeLessThan(max); expect(span.attributes['browser.web_vital.cls.source.1']).toEqual({ type: 'string', value: source }); diff --git a/dev-packages/browser-integration-tests/suites/tracing/metrics/web-vitals-inp-before-send-span/test.ts b/dev-packages/browser-integration-tests/suites/tracing/metrics/web-vitals-inp-before-send-span/test.ts index 8f62e9e283ce..f725e3909278 100644 --- a/dev-packages/browser-integration-tests/suites/tracing/metrics/web-vitals-inp-before-send-span/test.ts +++ b/dev-packages/browser-integration-tests/suites/tracing/metrics/web-vitals-inp-before-send-span/test.ts @@ -1,3 +1,4 @@ +import { BROWSER_WEB_VITAL_INP_VALUE } from '@sentry/conventions/attributes'; import { expect } from '@playwright/test'; import { sentryTest } from '../../../../utils/fixtures'; import { hidePage, shouldSkipTracingTest } from '../../../../utils/helpers'; @@ -35,6 +36,6 @@ sentryTest('runs `beforeSendSpan` for the streamed INP span', async ({ browserNa expect(inpSpan.attributes['custom.attribute']).toEqual({ value: 'from-before-send-span', type: 'string' }); // The span is still a valid v2 INP span carrying its web vital value. - const inpValue = inpSpan.attributes['browser.web_vital.inp.value']?.value as number; + const inpValue = inpSpan.attributes[BROWSER_WEB_VITAL_INP_VALUE]?.value as number; expect(inpValue).toBeGreaterThan(0); }); diff --git a/dev-packages/browser-integration-tests/suites/tracing/metrics/web-vitals-inp-late/test.ts b/dev-packages/browser-integration-tests/suites/tracing/metrics/web-vitals-inp-late/test.ts index 0312fd23d23f..a45e278385b5 100644 --- a/dev-packages/browser-integration-tests/suites/tracing/metrics/web-vitals-inp-late/test.ts +++ b/dev-packages/browser-integration-tests/suites/tracing/metrics/web-vitals-inp-late/test.ts @@ -1,3 +1,23 @@ +import { + BROWSER_WEB_VITAL_INP_VALUE, + SENTRY_IS_LOCALHOST, + SENTRY_ORIGIN, + SENTRY_OP, + UI_COMPONENT_NAME, + BROWSER_WEB_VITAL_INP_TARGET, + SENTRY_EXCLUSIVE_TIME, + BROWSER_WEB_VITAL_INP_INTERACTION_TYPE, + BROWSER_NAVIGATION_TYPE, + SENTRY_TRANSACTION, + SENTRY_SEGMENT_NAME, + USER_AGENT_ORIGINAL, + SENTRY_PAGELOAD_SPAN_ID, + SENTRY_TRACE_LIFECYCLE, + SENTRY_SEGMENT_ID, + SENTRY_SDK_NAME, + SENTRY_SDK_VERSION, + SENTRY_ENVIRONMENT, +} from '@sentry/conventions/attributes'; import { expect } from '@playwright/test'; import { SDK_VERSION } from '@sentry/core'; import { sentryTest } from '../../../../utils/fixtures'; @@ -58,7 +78,7 @@ sentryTest( content_type: 'application/vnd.sentry.items.span.v2+json', }); - const inpValue = inpSpan.attributes['browser.web_vital.inp.value']?.value as number; + const inpValue = inpSpan.attributes[BROWSER_WEB_VITAL_INP_VALUE]?.value as number; expect(inpValue).toBeGreaterThan(0); const pageloadSpanId = inpSpan.parent_span_id; @@ -73,24 +93,24 @@ sentryTest( is_segment: false, status: 'ok', attributes: { - 'sentry.is_localhost': { value: false, type: 'boolean' }, - 'sentry.origin': { value: 'auto.http.browser.inp', type: 'string' }, - 'sentry.op': { value: 'ui.interaction.click', type: 'string' }, - 'ui.component_name': { value: 'NormalButton', type: 'string' }, - 'browser.web_vital.inp.target': { value: 'body > NormalButton', type: 'string' }, - 'sentry.exclusive_time': { value: inpValue, type: expect.stringMatching(/^(integer)|(double)$/) }, - 'browser.web_vital.inp.value': { value: inpValue, type: expect.stringMatching(/^(integer)|(double)$/) }, - 'browser.web_vital.inp.interaction_type': { value: 'click', type: 'string' }, - 'browser.navigation.type': { value: 'navigate', type: 'string' }, - 'sentry.transaction': { value: 'test-url', type: 'string' }, - 'sentry.segment.name': { value: 'test-url', type: 'string' }, - 'user_agent.original': { value: expect.stringContaining('Chrome'), type: 'string' }, - 'sentry.pageload.span_id': { value: pageloadSpanId, type: 'string' }, - 'sentry.trace_lifecycle': { value: 'stream', type: 'string' }, - 'sentry.segment.id': { value: pageloadSpanId, type: 'string' }, - 'sentry.sdk.name': { value: 'sentry.javascript.browser', type: 'string' }, - 'sentry.sdk.version': { value: SDK_VERSION, type: 'string' }, - 'sentry.environment': { value: 'production', type: 'string' }, + [SENTRY_IS_LOCALHOST]: { value: false, type: 'boolean' }, + [SENTRY_ORIGIN]: { value: 'auto.http.browser.inp', type: 'string' }, + [SENTRY_OP]: { value: 'ui.interaction.click', type: 'string' }, + [UI_COMPONENT_NAME]: { value: 'NormalButton', type: 'string' }, + [BROWSER_WEB_VITAL_INP_TARGET]: { value: 'body > NormalButton', type: 'string' }, + [SENTRY_EXCLUSIVE_TIME]: { value: inpValue, type: expect.stringMatching(/^(integer)|(double)$/) }, + [BROWSER_WEB_VITAL_INP_VALUE]: { value: inpValue, type: expect.stringMatching(/^(integer)|(double)$/) }, + [BROWSER_WEB_VITAL_INP_INTERACTION_TYPE]: { value: 'click', type: 'string' }, + [BROWSER_NAVIGATION_TYPE]: { value: 'navigate', type: 'string' }, + [SENTRY_TRANSACTION]: { value: 'test-url', type: 'string' }, + [SENTRY_SEGMENT_NAME]: { value: 'test-url', type: 'string' }, + [USER_AGENT_ORIGINAL]: { value: expect.stringContaining('Chrome'), type: 'string' }, + [SENTRY_PAGELOAD_SPAN_ID]: { value: pageloadSpanId, type: 'string' }, + [SENTRY_TRACE_LIFECYCLE]: { value: 'stream', type: 'string' }, + [SENTRY_SEGMENT_ID]: { value: pageloadSpanId, type: 'string' }, + [SENTRY_SDK_NAME]: { value: 'sentry.javascript.browser', type: 'string' }, + [SENTRY_SDK_VERSION]: { value: SDK_VERSION, type: 'string' }, + [SENTRY_ENVIRONMENT]: { value: 'production', type: 'string' }, }, }); }, diff --git a/dev-packages/browser-integration-tests/suites/tracing/metrics/web-vitals-inp-navigate/test.ts b/dev-packages/browser-integration-tests/suites/tracing/metrics/web-vitals-inp-navigate/test.ts index 57174bc4d23f..26b5b176d2cb 100644 --- a/dev-packages/browser-integration-tests/suites/tracing/metrics/web-vitals-inp-navigate/test.ts +++ b/dev-packages/browser-integration-tests/suites/tracing/metrics/web-vitals-inp-navigate/test.ts @@ -1,3 +1,23 @@ +import { + BROWSER_WEB_VITAL_INP_VALUE, + SENTRY_IS_LOCALHOST, + SENTRY_ORIGIN, + SENTRY_OP, + UI_COMPONENT_NAME, + BROWSER_WEB_VITAL_INP_TARGET, + SENTRY_EXCLUSIVE_TIME, + BROWSER_WEB_VITAL_INP_INTERACTION_TYPE, + BROWSER_NAVIGATION_TYPE, + SENTRY_TRANSACTION, + SENTRY_SEGMENT_NAME, + USER_AGENT_ORIGINAL, + SENTRY_PAGELOAD_SPAN_ID, + SENTRY_TRACE_LIFECYCLE, + SENTRY_SEGMENT_ID, + SENTRY_SDK_NAME, + SENTRY_SDK_VERSION, + SENTRY_ENVIRONMENT, +} from '@sentry/conventions/attributes'; import { expect } from '@playwright/test'; import { SDK_VERSION } from '@sentry/core'; import { sentryTest } from '../../../../utils/fixtures'; @@ -58,7 +78,7 @@ sentryTest( content_type: 'application/vnd.sentry.items.span.v2+json', }); - const inpValue = inpSpan.attributes['browser.web_vital.inp.value']?.value as number; + const inpValue = inpSpan.attributes[BROWSER_WEB_VITAL_INP_VALUE]?.value as number; expect(inpValue).toBeGreaterThan(0); const pageloadSpanId = inpSpan.parent_span_id; @@ -73,24 +93,24 @@ sentryTest( is_segment: false, status: 'ok', attributes: { - 'sentry.is_localhost': { value: false, type: 'boolean' }, - 'sentry.origin': { value: 'auto.http.browser.inp', type: 'string' }, - 'sentry.op': { value: 'ui.interaction.click', type: 'string' }, - 'ui.component_name': { value: 'NavigationLink', type: 'string' }, - 'browser.web_vital.inp.target': { value: 'body > nav#navigation > NavigationLink', type: 'string' }, - 'sentry.exclusive_time': { value: inpValue, type: expect.stringMatching(/^(integer)|(double)$/) }, - 'browser.web_vital.inp.value': { value: inpValue, type: expect.stringMatching(/^(integer)|(double)$/) }, - 'browser.web_vital.inp.interaction_type': { value: 'click', type: 'string' }, - 'browser.navigation.type': { value: 'navigate', type: 'string' }, - 'sentry.transaction': { value: 'test-url', type: 'string' }, - 'sentry.segment.name': { value: 'test-url', type: 'string' }, - 'user_agent.original': { value: expect.stringContaining('Chrome'), type: 'string' }, - 'sentry.pageload.span_id': { value: pageloadSpanId, type: 'string' }, - 'sentry.trace_lifecycle': { value: 'stream', type: 'string' }, - 'sentry.segment.id': { value: pageloadSpanId, type: 'string' }, - 'sentry.sdk.name': { value: 'sentry.javascript.browser', type: 'string' }, - 'sentry.sdk.version': { value: SDK_VERSION, type: 'string' }, - 'sentry.environment': { value: 'production', type: 'string' }, + [SENTRY_IS_LOCALHOST]: { value: false, type: 'boolean' }, + [SENTRY_ORIGIN]: { value: 'auto.http.browser.inp', type: 'string' }, + [SENTRY_OP]: { value: 'ui.interaction.click', type: 'string' }, + [UI_COMPONENT_NAME]: { value: 'NavigationLink', type: 'string' }, + [BROWSER_WEB_VITAL_INP_TARGET]: { value: 'body > nav#navigation > NavigationLink', type: 'string' }, + [SENTRY_EXCLUSIVE_TIME]: { value: inpValue, type: expect.stringMatching(/^(integer)|(double)$/) }, + [BROWSER_WEB_VITAL_INP_VALUE]: { value: inpValue, type: expect.stringMatching(/^(integer)|(double)$/) }, + [BROWSER_WEB_VITAL_INP_INTERACTION_TYPE]: { value: 'click', type: 'string' }, + [BROWSER_NAVIGATION_TYPE]: { value: 'navigate', type: 'string' }, + [SENTRY_TRANSACTION]: { value: 'test-url', type: 'string' }, + [SENTRY_SEGMENT_NAME]: { value: 'test-url', type: 'string' }, + [USER_AGENT_ORIGINAL]: { value: expect.stringContaining('Chrome'), type: 'string' }, + [SENTRY_PAGELOAD_SPAN_ID]: { value: pageloadSpanId, type: 'string' }, + [SENTRY_TRACE_LIFECYCLE]: { value: 'stream', type: 'string' }, + [SENTRY_SEGMENT_ID]: { value: pageloadSpanId, type: 'string' }, + [SENTRY_SDK_NAME]: { value: 'sentry.javascript.browser', type: 'string' }, + [SENTRY_SDK_VERSION]: { value: SDK_VERSION, type: 'string' }, + [SENTRY_ENVIRONMENT]: { value: 'production', type: 'string' }, }, }); }, @@ -148,7 +168,7 @@ sentryTest( content_type: 'application/vnd.sentry.items.span.v2+json', }); - const inpValue = inpSpan.attributes['browser.web_vital.inp.value']?.value as number; + const inpValue = inpSpan.attributes[BROWSER_WEB_VITAL_INP_VALUE]?.value as number; expect(inpValue).toBeGreaterThan(0); const pageloadSpanId = inpSpan.parent_span_id; @@ -163,23 +183,23 @@ sentryTest( is_segment: false, status: 'ok', attributes: { - 'sentry.is_localhost': { value: false, type: 'boolean' }, - 'sentry.origin': { value: 'auto.http.browser.inp', type: 'string' }, - 'sentry.op': { value: 'ui.interaction.click', type: 'string' }, - 'browser.web_vital.inp.target': { value: 'body > nav#navigation > NavigationLink', type: 'string' }, - 'sentry.exclusive_time': { value: inpValue, type: expect.stringMatching(/^(integer)|(double)$/) }, - 'browser.web_vital.inp.value': { value: inpValue, type: expect.stringMatching(/^(integer)|(double)$/) }, - 'browser.web_vital.inp.interaction_type': { value: 'click', type: 'string' }, - 'browser.navigation.type': { value: 'navigate', type: 'string' }, - 'sentry.transaction': { value: 'test-url', type: 'string' }, - 'sentry.segment.name': { value: 'test-url', type: 'string' }, - 'user_agent.original': { value: expect.stringContaining('Chrome'), type: 'string' }, - 'sentry.pageload.span_id': { value: pageloadSpanId, type: 'string' }, - 'sentry.trace_lifecycle': { value: 'stream', type: 'string' }, - 'sentry.segment.id': { value: pageloadSpanId, type: 'string' }, - 'sentry.sdk.name': { value: 'sentry.javascript.browser', type: 'string' }, - 'sentry.sdk.version': { value: SDK_VERSION, type: 'string' }, - 'sentry.environment': { value: 'production', type: 'string' }, + [SENTRY_IS_LOCALHOST]: { value: false, type: 'boolean' }, + [SENTRY_ORIGIN]: { value: 'auto.http.browser.inp', type: 'string' }, + [SENTRY_OP]: { value: 'ui.interaction.click', type: 'string' }, + [BROWSER_WEB_VITAL_INP_TARGET]: { value: 'body > nav#navigation > NavigationLink', type: 'string' }, + [SENTRY_EXCLUSIVE_TIME]: { value: inpValue, type: expect.stringMatching(/^(integer)|(double)$/) }, + [BROWSER_WEB_VITAL_INP_VALUE]: { value: inpValue, type: expect.stringMatching(/^(integer)|(double)$/) }, + [BROWSER_WEB_VITAL_INP_INTERACTION_TYPE]: { value: 'click', type: 'string' }, + [BROWSER_NAVIGATION_TYPE]: { value: 'navigate', type: 'string' }, + [SENTRY_TRANSACTION]: { value: 'test-url', type: 'string' }, + [SENTRY_SEGMENT_NAME]: { value: 'test-url', type: 'string' }, + [USER_AGENT_ORIGINAL]: { value: expect.stringContaining('Chrome'), type: 'string' }, + [SENTRY_PAGELOAD_SPAN_ID]: { value: pageloadSpanId, type: 'string' }, + [SENTRY_TRACE_LIFECYCLE]: { value: 'stream', type: 'string' }, + [SENTRY_SEGMENT_ID]: { value: pageloadSpanId, type: 'string' }, + [SENTRY_SDK_NAME]: { value: 'sentry.javascript.browser', type: 'string' }, + [SENTRY_SDK_VERSION]: { value: SDK_VERSION, type: 'string' }, + [SENTRY_ENVIRONMENT]: { value: 'production', type: 'string' }, }, }); }, diff --git a/dev-packages/browser-integration-tests/suites/tracing/metrics/web-vitals-inp-parametrized-late/test.ts b/dev-packages/browser-integration-tests/suites/tracing/metrics/web-vitals-inp-parametrized-late/test.ts index 2d951480c402..b84ccaf33751 100644 --- a/dev-packages/browser-integration-tests/suites/tracing/metrics/web-vitals-inp-parametrized-late/test.ts +++ b/dev-packages/browser-integration-tests/suites/tracing/metrics/web-vitals-inp-parametrized-late/test.ts @@ -1,3 +1,23 @@ +import { + BROWSER_WEB_VITAL_INP_VALUE, + SENTRY_IS_LOCALHOST, + SENTRY_ORIGIN, + SENTRY_OP, + UI_COMPONENT_NAME, + BROWSER_WEB_VITAL_INP_TARGET, + SENTRY_EXCLUSIVE_TIME, + BROWSER_WEB_VITAL_INP_INTERACTION_TYPE, + BROWSER_NAVIGATION_TYPE, + SENTRY_TRANSACTION, + SENTRY_SEGMENT_NAME, + USER_AGENT_ORIGINAL, + SENTRY_PAGELOAD_SPAN_ID, + SENTRY_TRACE_LIFECYCLE, + SENTRY_SEGMENT_ID, + SENTRY_SDK_NAME, + SENTRY_SDK_VERSION, + SENTRY_ENVIRONMENT, +} from '@sentry/conventions/attributes'; import { expect } from '@playwright/test'; import { SDK_VERSION } from '@sentry/core'; import { sentryTest } from '../../../../utils/fixtures'; @@ -59,7 +79,7 @@ sentryTest( content_type: 'application/vnd.sentry.items.span.v2+json', }); - const inpValue = inpSpan.attributes['browser.web_vital.inp.value']?.value as number; + const inpValue = inpSpan.attributes[BROWSER_WEB_VITAL_INP_VALUE]?.value as number; expect(inpValue).toBeGreaterThan(0); const pageloadSpanId = inpSpan.parent_span_id; @@ -74,25 +94,25 @@ sentryTest( is_segment: false, status: 'ok', attributes: { - 'sentry.is_localhost': { value: false, type: 'boolean' }, - 'sentry.origin': { value: 'auto.http.browser.inp', type: 'string' }, - 'sentry.op': { value: 'ui.interaction.click', type: 'string' }, - 'ui.component_name': { value: 'NormalButton', type: 'string' }, - 'browser.web_vital.inp.target': { value: 'body > NormalButton', type: 'string' }, - 'sentry.exclusive_time': { value: inpValue, type: expect.stringMatching(/^(integer)|(double)$/) }, - 'browser.web_vital.inp.value': { value: inpValue, type: expect.stringMatching(/^(integer)|(double)$/) }, - 'browser.web_vital.inp.interaction_type': { value: 'click', type: 'string' }, - 'browser.navigation.type': { value: 'navigate', type: 'string' }, + [SENTRY_IS_LOCALHOST]: { value: false, type: 'boolean' }, + [SENTRY_ORIGIN]: { value: 'auto.http.browser.inp', type: 'string' }, + [SENTRY_OP]: { value: 'ui.interaction.click', type: 'string' }, + [UI_COMPONENT_NAME]: { value: 'NormalButton', type: 'string' }, + [BROWSER_WEB_VITAL_INP_TARGET]: { value: 'body > NormalButton', type: 'string' }, + [SENTRY_EXCLUSIVE_TIME]: { value: inpValue, type: expect.stringMatching(/^(integer)|(double)$/) }, + [BROWSER_WEB_VITAL_INP_VALUE]: { value: inpValue, type: expect.stringMatching(/^(integer)|(double)$/) }, + [BROWSER_WEB_VITAL_INP_INTERACTION_TYPE]: { value: 'click', type: 'string' }, + [BROWSER_NAVIGATION_TYPE]: { value: 'navigate', type: 'string' }, // the parametrized route name flows onto the INP span - 'sentry.transaction': { value: 'test-route', type: 'string' }, - 'sentry.segment.name': { value: 'test-route', type: 'string' }, - 'user_agent.original': { value: expect.stringContaining('Chrome'), type: 'string' }, - 'sentry.pageload.span_id': { value: pageloadSpanId, type: 'string' }, - 'sentry.trace_lifecycle': { value: 'stream', type: 'string' }, - 'sentry.segment.id': { value: pageloadSpanId, type: 'string' }, - 'sentry.sdk.name': { value: 'sentry.javascript.browser', type: 'string' }, - 'sentry.sdk.version': { value: SDK_VERSION, type: 'string' }, - 'sentry.environment': { value: 'production', type: 'string' }, + [SENTRY_TRANSACTION]: { value: 'test-route', type: 'string' }, + [SENTRY_SEGMENT_NAME]: { value: 'test-route', type: 'string' }, + [USER_AGENT_ORIGINAL]: { value: expect.stringContaining('Chrome'), type: 'string' }, + [SENTRY_PAGELOAD_SPAN_ID]: { value: pageloadSpanId, type: 'string' }, + [SENTRY_TRACE_LIFECYCLE]: { value: 'stream', type: 'string' }, + [SENTRY_SEGMENT_ID]: { value: pageloadSpanId, type: 'string' }, + [SENTRY_SDK_NAME]: { value: 'sentry.javascript.browser', type: 'string' }, + [SENTRY_SDK_VERSION]: { value: SDK_VERSION, type: 'string' }, + [SENTRY_ENVIRONMENT]: { value: 'production', type: 'string' }, }, }); }, diff --git a/dev-packages/browser-integration-tests/suites/tracing/metrics/web-vitals-inp-parametrized/test.ts b/dev-packages/browser-integration-tests/suites/tracing/metrics/web-vitals-inp-parametrized/test.ts index bd3ceb1c19c4..0eab0e6cf287 100644 --- a/dev-packages/browser-integration-tests/suites/tracing/metrics/web-vitals-inp-parametrized/test.ts +++ b/dev-packages/browser-integration-tests/suites/tracing/metrics/web-vitals-inp-parametrized/test.ts @@ -1,3 +1,23 @@ +import { + BROWSER_WEB_VITAL_INP_VALUE, + SENTRY_IS_LOCALHOST, + SENTRY_ORIGIN, + SENTRY_OP, + UI_COMPONENT_NAME, + BROWSER_WEB_VITAL_INP_TARGET, + SENTRY_EXCLUSIVE_TIME, + BROWSER_WEB_VITAL_INP_INTERACTION_TYPE, + BROWSER_NAVIGATION_TYPE, + SENTRY_TRANSACTION, + SENTRY_SEGMENT_NAME, + USER_AGENT_ORIGINAL, + SENTRY_PAGELOAD_SPAN_ID, + SENTRY_TRACE_LIFECYCLE, + SENTRY_SEGMENT_ID, + SENTRY_SDK_NAME, + SENTRY_SDK_VERSION, + SENTRY_ENVIRONMENT, +} from '@sentry/conventions/attributes'; import { expect } from '@playwright/test'; import { SDK_VERSION } from '@sentry/core'; import { sentryTest } from '../../../../utils/fixtures'; @@ -59,7 +79,7 @@ sentryTest( content_type: 'application/vnd.sentry.items.span.v2+json', }); - const inpValue = inpSpan.attributes['browser.web_vital.inp.value']?.value as number; + const inpValue = inpSpan.attributes[BROWSER_WEB_VITAL_INP_VALUE]?.value as number; expect(inpValue).toBeGreaterThan(0); const pageloadSpanId = inpSpan.parent_span_id; @@ -74,25 +94,25 @@ sentryTest( is_segment: false, status: 'ok', attributes: { - 'sentry.is_localhost': { value: false, type: 'boolean' }, - 'sentry.origin': { value: 'auto.http.browser.inp', type: 'string' }, - 'sentry.op': { value: 'ui.interaction.click', type: 'string' }, - 'ui.component_name': { value: 'NormalButton', type: 'string' }, - 'browser.web_vital.inp.target': { value: 'body > NormalButton', type: 'string' }, - 'sentry.exclusive_time': { value: inpValue, type: expect.stringMatching(/^(integer)|(double)$/) }, - 'browser.web_vital.inp.value': { value: inpValue, type: expect.stringMatching(/^(integer)|(double)$/) }, - 'browser.web_vital.inp.interaction_type': { value: 'click', type: 'string' }, - 'browser.navigation.type': { value: 'navigate', type: 'string' }, + [SENTRY_IS_LOCALHOST]: { value: false, type: 'boolean' }, + [SENTRY_ORIGIN]: { value: 'auto.http.browser.inp', type: 'string' }, + [SENTRY_OP]: { value: 'ui.interaction.click', type: 'string' }, + [UI_COMPONENT_NAME]: { value: 'NormalButton', type: 'string' }, + [BROWSER_WEB_VITAL_INP_TARGET]: { value: 'body > NormalButton', type: 'string' }, + [SENTRY_EXCLUSIVE_TIME]: { value: inpValue, type: expect.stringMatching(/^(integer)|(double)$/) }, + [BROWSER_WEB_VITAL_INP_VALUE]: { value: inpValue, type: expect.stringMatching(/^(integer)|(double)$/) }, + [BROWSER_WEB_VITAL_INP_INTERACTION_TYPE]: { value: 'click', type: 'string' }, + [BROWSER_NAVIGATION_TYPE]: { value: 'navigate', type: 'string' }, // the parametrized route name flows onto the INP span - 'sentry.transaction': { value: 'test-route', type: 'string' }, - 'sentry.segment.name': { value: 'test-route', type: 'string' }, - 'user_agent.original': { value: expect.stringContaining('Chrome'), type: 'string' }, - 'sentry.pageload.span_id': { value: pageloadSpanId, type: 'string' }, - 'sentry.trace_lifecycle': { value: 'stream', type: 'string' }, - 'sentry.segment.id': { value: pageloadSpanId, type: 'string' }, - 'sentry.sdk.name': { value: 'sentry.javascript.browser', type: 'string' }, - 'sentry.sdk.version': { value: SDK_VERSION, type: 'string' }, - 'sentry.environment': { value: 'production', type: 'string' }, + [SENTRY_TRANSACTION]: { value: 'test-route', type: 'string' }, + [SENTRY_SEGMENT_NAME]: { value: 'test-route', type: 'string' }, + [USER_AGENT_ORIGINAL]: { value: expect.stringContaining('Chrome'), type: 'string' }, + [SENTRY_PAGELOAD_SPAN_ID]: { value: pageloadSpanId, type: 'string' }, + [SENTRY_TRACE_LIFECYCLE]: { value: 'stream', type: 'string' }, + [SENTRY_SEGMENT_ID]: { value: pageloadSpanId, type: 'string' }, + [SENTRY_SDK_NAME]: { value: 'sentry.javascript.browser', type: 'string' }, + [SENTRY_SDK_VERSION]: { value: SDK_VERSION, type: 'string' }, + [SENTRY_ENVIRONMENT]: { value: 'production', type: 'string' }, }, }); }, diff --git a/dev-packages/browser-integration-tests/suites/tracing/metrics/web-vitals-inp/test.ts b/dev-packages/browser-integration-tests/suites/tracing/metrics/web-vitals-inp/test.ts index f1ba7c9138f7..22807de56d92 100644 --- a/dev-packages/browser-integration-tests/suites/tracing/metrics/web-vitals-inp/test.ts +++ b/dev-packages/browser-integration-tests/suites/tracing/metrics/web-vitals-inp/test.ts @@ -1,3 +1,14 @@ +import { + SENTRY_OP, + SENTRY_ORIGIN, + USER_AGENT_ORIGINAL, + SENTRY_TRANSACTION, + SENTRY_SEGMENT_NAME, + BROWSER_WEB_VITAL_INP_VALUE, + SENTRY_EXCLUSIVE_TIME, + UI_COMPONENT_NAME, + BROWSER_WEB_VITAL_INP_TARGET, +} from '@sentry/conventions/attributes'; import { expect } from '@playwright/test'; import { sentryTest } from '../../../../utils/fixtures'; import { hidePage, shouldSkipTracingTest } from '../../../../utils/helpers'; @@ -27,22 +38,22 @@ sentryTest('captures INP click as a streamed span', async ({ getLocalTestUrl, pa const inpSpan = await inpSpanPromise; const pageloadSpan = await pageloadSpanPromise; - expect(inpSpan.attributes['sentry.op']).toEqual({ type: 'string', value: 'ui.interaction.click' }); - expect(inpSpan.attributes['sentry.origin']).toEqual({ type: 'string', value: 'auto.http.browser.inp' }); - expect(inpSpan.attributes['user_agent.original']?.value).toEqual(expect.stringContaining('Chrome')); + expect(inpSpan.attributes[SENTRY_OP]).toEqual({ type: 'string', value: 'ui.interaction.click' }); + expect(inpSpan.attributes[SENTRY_ORIGIN]).toEqual({ type: 'string', value: 'auto.http.browser.inp' }); + expect(inpSpan.attributes[USER_AGENT_ORIGINAL]?.value).toEqual(expect.stringContaining('Chrome')); // Check the INP span carries the transaction/segment name it belongs to - expect(inpSpan.attributes['sentry.transaction']).toEqual({ type: 'string', value: 'Pageload' }); - expect(inpSpan.attributes['sentry.segment.name']).toEqual({ type: 'string', value: 'Pageload' }); + expect(inpSpan.attributes[SENTRY_TRANSACTION]).toEqual({ type: 'string', value: 'Pageload' }); + expect(inpSpan.attributes[SENTRY_SEGMENT_NAME]).toEqual({ type: 'string', value: 'Pageload' }); - const inpValue = inpSpan.attributes['browser.web_vital.inp.value']?.value as number; + const inpValue = inpSpan.attributes[BROWSER_WEB_VITAL_INP_VALUE]?.value as number; expect(inpValue).toBeGreaterThan(0); - expect(inpSpan.attributes['sentry.exclusive_time']?.value).toBeGreaterThan(0); + expect(inpSpan.attributes[SENTRY_EXCLUSIVE_TIME]?.value).toBeGreaterThan(0); expect(inpSpan.name).toBe('NormalButton'); - expect(inpSpan.attributes['ui.component_name']).toEqual({ type: 'string', value: 'NormalButton' }); - expect(inpSpan.attributes['browser.web_vital.inp.target']).toEqual({ type: 'string', value: 'body > NormalButton' }); + expect(inpSpan.attributes[UI_COMPONENT_NAME]).toEqual({ type: 'string', value: 'NormalButton' }); + expect(inpSpan.attributes[BROWSER_WEB_VITAL_INP_TARGET]).toEqual({ type: 'string', value: 'body > NormalButton' }); expect(inpSpan.end_timestamp).toBeGreaterThan(inpSpan.start_timestamp); @@ -78,10 +89,10 @@ sentryTest('captures the slowest interaction as streamed INP span', async ({ get const inpSpan = await inpSpanPromise; expect(inpSpan.name).toBe('SlowButton'); - expect(inpSpan.attributes['ui.component_name']).toEqual({ type: 'string', value: 'SlowButton' }); - expect(inpSpan.attributes['browser.web_vital.inp.target']).toEqual({ type: 'string', value: 'body > SlowButton' }); - expect(inpSpan.attributes['sentry.exclusive_time']?.value).toBeGreaterThan(400); + expect(inpSpan.attributes[UI_COMPONENT_NAME]).toEqual({ type: 'string', value: 'SlowButton' }); + expect(inpSpan.attributes[BROWSER_WEB_VITAL_INP_TARGET]).toEqual({ type: 'string', value: 'body > SlowButton' }); + expect(inpSpan.attributes[SENTRY_EXCLUSIVE_TIME]?.value).toBeGreaterThan(400); - const inpValue = inpSpan.attributes['browser.web_vital.inp.value']?.value as number; + const inpValue = inpSpan.attributes[BROWSER_WEB_VITAL_INP_VALUE]?.value as number; expect(inpValue).toBeGreaterThan(400); }); diff --git a/dev-packages/browser-integration-tests/suites/tracing/metrics/web-vitals-lcp/test.ts b/dev-packages/browser-integration-tests/suites/tracing/metrics/web-vitals-lcp/test.ts index 427bfa2111a1..263b31d96660 100644 --- a/dev-packages/browser-integration-tests/suites/tracing/metrics/web-vitals-lcp/test.ts +++ b/dev-packages/browser-integration-tests/suites/tracing/metrics/web-vitals-lcp/test.ts @@ -1,7 +1,21 @@ +import { + SENTRY_OP, + SENTRY_ORIGIN, + SENTRY_EXCLUSIVE_TIME, + USER_AGENT_ORIGINAL, + SENTRY_SEGMENT_NAME, + BROWSER_WEB_VITAL_LCP_ELEMENT, + BROWSER_WEB_VITAL_LCP_URL, + BROWSER_WEB_VITAL_LCP_SIZE, + BROWSER_WEB_VITAL_LCP_VALUE, + BROWSER_WEB_VITAL_LCP_RENDER_TIME, + BROWSER_WEB_VITAL_LCP_LOAD_TIME, + SENTRY_PAGELOAD_SPAN_ID, +} from '@sentry/conventions/attributes'; import type { Route } from '@playwright/test'; import { expect } from '@playwright/test'; import { sentryTest } from '../../../../utils/fixtures'; -import { shouldSkipTracingTest } from '../../../../utils/helpers'; +import { shouldSkipTracingTest, waitForLcpCandidate } from '../../../../utils/helpers'; import { getSpanOp, waitForStreamedSpan } from '../../../../utils/spanUtils'; sentryTest.beforeEach(async ({ browserName, page }) => { @@ -27,8 +41,7 @@ sentryTest('captures LCP as a streamed span with element attributes', async ({ g await page.goto(url); - // Wait for LCP to be captured - await page.waitForTimeout(1000); + await waitForLcpCandidate(page, 'img'); // LCP finalizes on the first trusted input or visibility change, and web-vitals checks // `isTrusted`, so a synthetically dispatched `visibilitychange` does not finalize it. Click to @@ -38,30 +51,30 @@ sentryTest('captures LCP as a streamed span with element attributes', async ({ g const lcpSpan = await lcpSpanPromise; const pageloadSpan = await pageloadSpanPromise; - expect(lcpSpan.attributes['sentry.op']).toEqual({ type: 'string', value: 'ui.webvital.lcp' }); - expect(lcpSpan.attributes['sentry.origin']).toEqual({ type: 'string', value: 'auto.http.browser.lcp' }); - expect(lcpSpan.attributes['sentry.exclusive_time']).toEqual({ type: 'integer', value: 0 }); - expect(lcpSpan.attributes['user_agent.original']?.value).toEqual(expect.stringContaining('Chrome')); + expect(lcpSpan.attributes[SENTRY_OP]).toEqual({ type: 'string', value: 'ui.webvital.lcp' }); + expect(lcpSpan.attributes[SENTRY_ORIGIN]).toEqual({ type: 'string', value: 'auto.http.browser.lcp' }); + expect(lcpSpan.attributes[SENTRY_EXCLUSIVE_TIME]).toEqual({ type: 'integer', value: 0 }); + expect(lcpSpan.attributes[USER_AGENT_ORIGINAL]?.value).toEqual(expect.stringContaining('Chrome')); // Check the LCP span carries the segment name it belongs to - expect(lcpSpan.attributes['sentry.segment.name']).toEqual({ type: 'string', value: 'Pageload' }); + expect(lcpSpan.attributes[SENTRY_SEGMENT_NAME]).toEqual({ type: 'string', value: 'Pageload' }); // Check browser.web_vital.lcp.* attributes - expect(lcpSpan.attributes['browser.web_vital.lcp.element']?.value).toEqual(expect.stringContaining('body > img')); - expect(lcpSpan.attributes['browser.web_vital.lcp.url']?.value).toBe('https://sentry-test-site.example/my/image.png'); - expect(lcpSpan.attributes['browser.web_vital.lcp.size']?.value).toEqual(expect.any(Number)); + expect(lcpSpan.attributes[BROWSER_WEB_VITAL_LCP_ELEMENT]?.value).toEqual(expect.stringContaining('body > img')); + expect(lcpSpan.attributes[BROWSER_WEB_VITAL_LCP_URL]?.value).toBe('https://sentry-test-site.example/my/image.png'); + expect(lcpSpan.attributes[BROWSER_WEB_VITAL_LCP_SIZE]?.value).toEqual(expect.any(Number)); // Check web vital value attribute - expect(lcpSpan.attributes['browser.web_vital.lcp.value']?.type).toMatch(/^(double)|(integer)$/); - expect(lcpSpan.attributes['browser.web_vital.lcp.value']?.value).toBeGreaterThan(0); + expect(lcpSpan.attributes[BROWSER_WEB_VITAL_LCP_VALUE]?.type).toMatch(/^(double)|(integer)$/); + expect(lcpSpan.attributes[BROWSER_WEB_VITAL_LCP_VALUE]?.value).toBeGreaterThan(0); - const renderTime = lcpSpan.attributes['browser.web_vital.lcp.render_time']?.value as number; + const renderTime = lcpSpan.attributes[BROWSER_WEB_VITAL_LCP_RENDER_TIME]?.value as number; expect(renderTime).toBeGreaterThan(0); - expect(lcpSpan.attributes['browser.web_vital.lcp.load_time']?.value).toBeGreaterThan(0); - expect(lcpSpan.attributes['browser.web_vital.lcp.value']?.value).toBeCloseTo(renderTime); + expect(lcpSpan.attributes[BROWSER_WEB_VITAL_LCP_LOAD_TIME]?.value).toBeGreaterThan(0); + expect(lcpSpan.attributes[BROWSER_WEB_VITAL_LCP_VALUE]?.value).toBeCloseTo(renderTime); // Check pageload span id is present - expect(lcpSpan.attributes['sentry.pageload.span_id']?.value).toBe(pageloadSpan.span_id); + expect(lcpSpan.attributes[SENTRY_PAGELOAD_SPAN_ID]?.value).toBe(pageloadSpan.span_id); // Span should have meaningful duration (navigation start -> LCP event) expect(lcpSpan.end_timestamp).toBeGreaterThan(lcpSpan.start_timestamp); diff --git a/dev-packages/browser-integration-tests/suites/tracing/metrics/web-vitals-ttfb-static/test.ts b/dev-packages/browser-integration-tests/suites/tracing/metrics/web-vitals-ttfb-static/test.ts index d2997f712c39..32102cfa652c 100644 --- a/dev-packages/browser-integration-tests/suites/tracing/metrics/web-vitals-ttfb-static/test.ts +++ b/dev-packages/browser-integration-tests/suites/tracing/metrics/web-vitals-ttfb-static/test.ts @@ -1,3 +1,4 @@ +import { TTFB_REQUESTTIME } from '@sentry/conventions/attributes'; import { expect } from '@playwright/test'; import type { Event } from '@sentry/core'; import { sentryTest } from '../../../../utils/fixtures'; @@ -20,5 +21,5 @@ sentryTest('should capture TTFB vital.', async ({ getLocalTestUrl, page }) => { expect(eventData.measurements?.ttfb?.value).toBeDefined(); } - expect(eventData.measurements?.['ttfb.requestTime']?.value).toBeDefined(); + expect(eventData.measurements?.[TTFB_REQUESTTIME]?.value).toBeDefined(); }); diff --git a/dev-packages/browser-integration-tests/suites/tracing/metrics/web-vitals-ttfb/test.ts b/dev-packages/browser-integration-tests/suites/tracing/metrics/web-vitals-ttfb/test.ts index 4a51ad0f6948..7c5693468874 100644 --- a/dev-packages/browser-integration-tests/suites/tracing/metrics/web-vitals-ttfb/test.ts +++ b/dev-packages/browser-integration-tests/suites/tracing/metrics/web-vitals-ttfb/test.ts @@ -1,3 +1,4 @@ +import { BROWSER_WEB_VITAL_TTFB_VALUE, BROWSER_WEB_VITAL_TTFB_REQUEST_TIME } from '@sentry/conventions/attributes'; import { expect } from '@playwright/test'; import { sentryTest } from '../../../../utils/fixtures'; import { hidePage, shouldSkipTracingTest } from '../../../../utils/helpers'; @@ -27,10 +28,10 @@ sentryTest( // This seems to happen somewhat randomly, so we handle it. const responseStart = await page.evaluate("performance.getEntriesByType('navigation')[0].responseStart;"); if (responseStart !== 0) { - expect(pageloadSpan.attributes['browser.web_vital.ttfb.value']?.type).toMatch(/^(double)|(integer)$/); - expect(pageloadSpan.attributes['browser.web_vital.ttfb.value']?.value).toBeGreaterThan(0); + expect(pageloadSpan.attributes[BROWSER_WEB_VITAL_TTFB_VALUE]?.type).toMatch(/^(double)|(integer)$/); + expect(pageloadSpan.attributes[BROWSER_WEB_VITAL_TTFB_VALUE]?.value).toBeGreaterThan(0); } - expect(pageloadSpan.attributes['browser.web_vital.ttfb.request_time']?.type).toMatch(/^(double)|(integer)$/); + expect(pageloadSpan.attributes[BROWSER_WEB_VITAL_TTFB_REQUEST_TIME]?.type).toMatch(/^(double)|(integer)$/); }, ); diff --git a/dev-packages/browser-integration-tests/suites/tracing/metrics/web-vitals/test.ts b/dev-packages/browser-integration-tests/suites/tracing/metrics/web-vitals/test.ts index 482dd815af14..7b1c241dee2d 100644 --- a/dev-packages/browser-integration-tests/suites/tracing/metrics/web-vitals/test.ts +++ b/dev-packages/browser-integration-tests/suites/tracing/metrics/web-vitals/test.ts @@ -1,7 +1,15 @@ +import { + BROWSER_WEB_VITAL_TTFB_VALUE, + BROWSER_WEB_VITAL_LCP_VALUE, + BROWSER_WEB_VITAL_FCP_VALUE, + BROWSER_WEB_VITAL_FP_VALUE, + BROWSER_PERFORMANCE_TIME_ORIGIN, + BROWSER_PERFORMANCE_NAVIGATION_ACTIVATION_START, +} from '@sentry/conventions/attributes'; import type { Route } from '@playwright/test'; import { expect } from '@playwright/test'; import { sentryTest } from '../../../../utils/fixtures'; -import { shouldSkipTracingTest } from '../../../../utils/helpers'; +import { shouldSkipTracingTest, waitForLcpCandidate } from '../../../../utils/helpers'; import { getSpanOp, waitForStreamedSpan } from '../../../../utils/spanUtils'; /** @@ -26,12 +34,11 @@ sentryTest('paint web vitals values are greater than TTFB', async ({ browserName const pageloadPromise = waitForStreamedSpan(page, span => span.is_segment && getSpanOp(span) === 'pageload'); const lcpPromise = waitForStreamedSpan(page, span => getSpanOp(span) === 'ui.webvital.lcp'); await page.goto(url); - await page.locator('img').evaluate((img: HTMLImageElement) => img.decode()); - await page.waitForTimeout(1000); + await waitForLcpCandidate(page, 'img'); await page.locator('button').click(); const [eventData, lcpSpan] = await Promise.all([pageloadPromise, lcpPromise]); - const ttfbValue = eventData.attributes['browser.web_vital.ttfb.value']?.value as number | undefined; + const ttfbValue = eventData.attributes[BROWSER_WEB_VITAL_TTFB_VALUE]?.value as number | undefined; if (!ttfbValue) { // TTFB is unfortunately quite flaky. Sometimes, the web-vitals library doesn't report TTFB because @@ -44,9 +51,9 @@ sentryTest('paint web vitals values are greater than TTFB', async ({ browserName sentryTest.skip(); } - const lcpValue = lcpSpan.attributes['browser.web_vital.lcp.value']?.value; - const fcpValue = eventData.attributes['browser.web_vital.fcp.value']?.value; - const fpValue = eventData.attributes['browser.web_vital.fp.value']?.value; + const lcpValue = lcpSpan.attributes[BROWSER_WEB_VITAL_LCP_VALUE]?.value; + const fcpValue = eventData.attributes[BROWSER_WEB_VITAL_FCP_VALUE]?.value; + const fpValue = eventData.attributes[BROWSER_WEB_VITAL_FP_VALUE]?.value; expect(lcpValue).toBeDefined(); expect(fcpValue).toBeDefined(); @@ -72,8 +79,8 @@ sentryTest( page.goto(url), ]); - const timeOriginAttribute = eventData.attributes['browser.performance.time_origin']?.value as number; - const activationStart = eventData.attributes['browser.performance.navigation.activation_start']?.value; + const timeOriginAttribute = eventData.attributes[BROWSER_PERFORMANCE_TIME_ORIGIN]?.value as number; + const activationStart = eventData.attributes[BROWSER_PERFORMANCE_NAVIGATION_ACTIVATION_START]?.value; const spanStartTimestamp = eventData.start_timestamp; diff --git a/dev-packages/browser-integration-tests/suites/tracing/request/fetch-data-url/test.ts b/dev-packages/browser-integration-tests/suites/tracing/request/fetch-data-url/test.ts index e120b22f5c90..251536177d0c 100644 --- a/dev-packages/browser-integration-tests/suites/tracing/request/fetch-data-url/test.ts +++ b/dev-packages/browser-integration-tests/suites/tracing/request/fetch-data-url/test.ts @@ -1,3 +1,4 @@ +import { HTTP_REQUEST_METHOD, URL_FULL } from '@sentry/conventions/attributes'; import { expect } from '@playwright/test'; import { sentryTest } from '../../../../utils/fixtures'; import { shouldSkipTracingTest } from '../../../../utils/helpers'; @@ -18,9 +19,9 @@ sentryTest('sanitizes data URLs in fetch span name and attributes', async ({ get expect(span.name).toBe('GET'); expect(span.parent_span_id).toBe(pageload.span_id); expect(span.trace_id).toBe(pageload.trace_id); - expect(span.attributes['http.request.method']).toEqual({ type: 'string', value: 'GET' }); + expect(span.attributes[HTTP_REQUEST_METHOD]).toEqual({ type: 'string', value: 'GET' }); expect(span.attributes.type).toEqual({ type: 'string', value: 'fetch' }); - expect(span.attributes['url.full']).toEqual({ + expect(span.attributes[URL_FULL]).toEqual({ type: 'string', value: 'data:text/plain,base64,SGVsbG8gV2... [truncated]', }); diff --git a/dev-packages/browser-integration-tests/suites/tracing/request/fetch-immediate/test.ts b/dev-packages/browser-integration-tests/suites/tracing/request/fetch-immediate/test.ts index f69631959e3d..a8f7e77be1c8 100644 --- a/dev-packages/browser-integration-tests/suites/tracing/request/fetch-immediate/test.ts +++ b/dev-packages/browser-integration-tests/suites/tracing/request/fetch-immediate/test.ts @@ -1,3 +1,4 @@ +import { HTTP_REQUEST_METHOD, URL_FULL, SERVER_ADDRESS } from '@sentry/conventions/attributes'; import { collectStreamedSpans, getSpanOp, waitForStreamedSpan } from '../../../../utils/spanUtils'; import { expect } from '@playwright/test'; import { sentryTest } from '../../../../utils/fixtures'; @@ -30,9 +31,9 @@ sentryTest('should create spans for fetch requests called directly after init', end_timestamp: expect.any(Number), trace_id: pageload.trace_id, attributes: expect.objectContaining({ - 'http.request.method': { type: 'string', value: 'GET' }, - 'url.full': { type: 'string', value: 'http://sentry-test-site.example/0' }, - 'server.address': { type: 'string', value: 'sentry-test-site.example' }, + [HTTP_REQUEST_METHOD]: { type: 'string', value: 'GET' }, + [URL_FULL]: { type: 'string', value: 'http://sentry-test-site.example/0' }, + [SERVER_ADDRESS]: { type: 'string', value: 'sentry-test-site.example' }, type: { type: 'string', value: 'fetch' }, }), }); diff --git a/dev-packages/browser-integration-tests/suites/tracing/request/fetch-relative-url-static/test.ts b/dev-packages/browser-integration-tests/suites/tracing/request/fetch-relative-url-static/test.ts index 323060c7e4b2..704b986d9550 100644 --- a/dev-packages/browser-integration-tests/suites/tracing/request/fetch-relative-url-static/test.ts +++ b/dev-packages/browser-integration-tests/suites/tracing/request/fetch-relative-url-static/test.ts @@ -1,3 +1,4 @@ +import { HTTP_REQUEST_METHOD, URL_FULL, SERVER_ADDRESS } from '@sentry/conventions/attributes'; import { expect } from '@playwright/test'; import { sentryTest, TEST_HOST } from '../../../../utils/fixtures'; import { @@ -28,9 +29,9 @@ sentryTest('should create spans for fetch requests', async ({ getLocalTestUrl, p timestamp: expect.any(Number), trace_id: tracingEvent.contexts?.trace?.trace_id, data: { - 'http.request.method': 'GET', - 'url.full': `${TEST_HOST}/test-req/${index}`, - 'server.address': 'sentry-test.io', + [HTTP_REQUEST_METHOD]: 'GET', + [URL_FULL]: `${TEST_HOST}/test-req/${index}`, + [SERVER_ADDRESS]: 'sentry-test.io', type: 'fetch', }, }), diff --git a/dev-packages/browser-integration-tests/suites/tracing/request/fetch-relative-url/test.ts b/dev-packages/browser-integration-tests/suites/tracing/request/fetch-relative-url/test.ts index 5f42cfef8ef1..8c7335957673 100644 --- a/dev-packages/browser-integration-tests/suites/tracing/request/fetch-relative-url/test.ts +++ b/dev-packages/browser-integration-tests/suites/tracing/request/fetch-relative-url/test.ts @@ -1,3 +1,4 @@ +import { URL_FULL, HTTP_REQUEST_METHOD, URL_DOMAIN, SERVER_ADDRESS } from '@sentry/conventions/attributes'; import { expect } from '@playwright/test'; import { sentryTest, TEST_HOST } from '../../../../utils/fixtures'; import { shouldSkipTracingTest } from '../../../../utils/helpers'; @@ -17,9 +18,7 @@ sentryTest('names spans for relative fetch requests after the page domain', asyn await page.evaluate(() => (window as any).Sentry.flush()); const requestSpans = spans .filter(s => getSpanOp(s) === 'http.client') - .sort((a, b) => - (a.attributes!['url.full']!.value as string).localeCompare(b.attributes!['url.full']!.value as string), - ); + .sort((a, b) => (a.attributes![URL_FULL]!.value as string).localeCompare(b.attributes![URL_FULL]!.value as string)); expect(requestSpans).toHaveLength(3); @@ -30,10 +29,10 @@ sentryTest('names spans for relative fetch requests after the page domain', asyn parent_span_id: pageloadSpan.span_id, trace_id: pageloadSpan.trace_id, attributes: expect.objectContaining({ - 'http.request.method': { type: 'string', value: 'GET' }, - 'url.full': { type: 'string', value: `${TEST_HOST}/test-req/${index}` }, - 'url.domain': { type: 'string', value: 'sentry-test.io' }, - 'server.address': { type: 'string', value: 'sentry-test.io' }, + [HTTP_REQUEST_METHOD]: { type: 'string', value: 'GET' }, + [URL_FULL]: { type: 'string', value: `${TEST_HOST}/test-req/${index}` }, + [URL_DOMAIN]: { type: 'string', value: 'sentry-test.io' }, + [SERVER_ADDRESS]: { type: 'string', value: 'sentry-test.io' }, type: { type: 'string', value: 'fetch' }, }), }), diff --git a/dev-packages/browser-integration-tests/suites/tracing/request/fetch-static/test.ts b/dev-packages/browser-integration-tests/suites/tracing/request/fetch-static/test.ts index a177445bb980..bb8c594ffd42 100644 --- a/dev-packages/browser-integration-tests/suites/tracing/request/fetch-static/test.ts +++ b/dev-packages/browser-integration-tests/suites/tracing/request/fetch-static/test.ts @@ -1,3 +1,4 @@ +import { HTTP_REQUEST_METHOD, URL_FULL, SERVER_ADDRESS } from '@sentry/conventions/attributes'; import { expect } from '@playwright/test'; import { sentryTest } from '../../../../utils/fixtures'; import { @@ -31,9 +32,9 @@ sentryTest('should create spans for fetch requests', async ({ getLocalTestUrl, p timestamp: expect.any(Number), trace_id: tracingEvent.contexts?.trace?.trace_id, data: { - 'http.request.method': 'GET', - 'url.full': `http://sentry-test-site.example/${index}`, - 'server.address': 'sentry-test-site.example', + [HTTP_REQUEST_METHOD]: 'GET', + [URL_FULL]: `http://sentry-test-site.example/${index}`, + [SERVER_ADDRESS]: 'sentry-test-site.example', type: 'fetch', }, }), diff --git a/dev-packages/browser-integration-tests/suites/tracing/request/fetch-strip-query-and-fragment/test.ts b/dev-packages/browser-integration-tests/suites/tracing/request/fetch-strip-query-and-fragment/test.ts index 08a70cc55468..e88a9940f607 100644 --- a/dev-packages/browser-integration-tests/suites/tracing/request/fetch-strip-query-and-fragment/test.ts +++ b/dev-packages/browser-integration-tests/suites/tracing/request/fetch-strip-query-and-fragment/test.ts @@ -1,3 +1,14 @@ +import { + HTTP_REQUEST_METHOD, + URL_FULL, + URL_QUERY, + HTTP_RESPONSE_STATUS_CODE, + HTTP_RESPONSE_BODY_SIZE, + SENTRY_OP, + SENTRY_ORIGIN, + SERVER_ADDRESS, + URL_FRAGMENT, +} from '@sentry/conventions/attributes'; import { expect } from '@playwright/test'; import { sentryTest } from '../../../../utils/fixtures'; import { shouldSkipTracingTest } from '../../../../utils/helpers'; @@ -29,15 +40,15 @@ sentryTest('strips query params in fetch request spans', async ({ getLocalTestUr end_timestamp: expect.any(Number), trace_id: rootSpan.trace_id, attributes: expect.objectContaining({ - 'http.request.method': { type: 'string', value: 'GET' }, - 'url.full': { type: 'string', value: 'http://sentry-test-site.example/0?id=123;page=5' }, - 'url.query': { type: 'string', value: 'id=123;page=5' }, - 'http.response.status_code': { type: 'integer', value: 200 }, - 'http.response.body.size': { type: 'integer', value: 2 }, - 'sentry.op': { type: 'string', value: 'http.client' }, - 'sentry.origin': { type: 'string', value: 'auto.http.browser' }, + [HTTP_REQUEST_METHOD]: { type: 'string', value: 'GET' }, + [URL_FULL]: { type: 'string', value: 'http://sentry-test-site.example/0?id=123;page=5' }, + [URL_QUERY]: { type: 'string', value: 'id=123;page=5' }, + [HTTP_RESPONSE_STATUS_CODE]: { type: 'integer', value: 200 }, + [HTTP_RESPONSE_BODY_SIZE]: { type: 'integer', value: 2 }, + [SENTRY_OP]: { type: 'string', value: 'http.client' }, + [SENTRY_ORIGIN]: { type: 'string', value: 'auto.http.browser' }, type: { type: 'string', value: 'fetch' }, - 'server.address': { type: 'string', value: 'sentry-test-site.example' }, + [SERVER_ADDRESS]: { type: 'string', value: 'sentry-test-site.example' }, }), }); @@ -70,15 +81,15 @@ sentryTest('strips hash fragment in fetch request spans', async ({ getLocalTestU end_timestamp: expect.any(Number), trace_id: rootSpan.trace_id, attributes: expect.objectContaining({ - 'http.request.method': { type: 'string', value: 'GET' }, - 'url.full': { type: 'string', value: 'http://sentry-test-site.example/1#fragment' }, - 'url.fragment': { type: 'string', value: 'fragment' }, - 'http.response.status_code': { type: 'integer', value: 200 }, - 'http.response.body.size': { type: 'integer', value: 2 }, - 'sentry.op': { type: 'string', value: 'http.client' }, - 'sentry.origin': { type: 'string', value: 'auto.http.browser' }, + [HTTP_REQUEST_METHOD]: { type: 'string', value: 'GET' }, + [URL_FULL]: { type: 'string', value: 'http://sentry-test-site.example/1#fragment' }, + [URL_FRAGMENT]: { type: 'string', value: 'fragment' }, + [HTTP_RESPONSE_STATUS_CODE]: { type: 'integer', value: 200 }, + [HTTP_RESPONSE_BODY_SIZE]: { type: 'integer', value: 2 }, + [SENTRY_OP]: { type: 'string', value: 'http.client' }, + [SENTRY_ORIGIN]: { type: 'string', value: 'auto.http.browser' }, type: { type: 'string', value: 'fetch' }, - 'server.address': { type: 'string', value: 'sentry-test-site.example' }, + [SERVER_ADDRESS]: { type: 'string', value: 'sentry-test-site.example' }, }), }); @@ -111,16 +122,16 @@ sentryTest('strips hash fragment and query params in fetch request spans', async end_timestamp: expect.any(Number), trace_id: rootSpan.trace_id, attributes: expect.objectContaining({ - 'http.request.method': { type: 'string', value: 'GET' }, - 'url.full': { type: 'string', value: 'http://sentry-test-site.example/2?id=1#fragment' }, - 'url.query': { type: 'string', value: 'id=1' }, - 'url.fragment': { type: 'string', value: 'fragment' }, - 'http.response.status_code': { type: 'integer', value: 200 }, - 'http.response.body.size': { type: 'integer', value: 2 }, - 'sentry.op': { type: 'string', value: 'http.client' }, - 'sentry.origin': { type: 'string', value: 'auto.http.browser' }, + [HTTP_REQUEST_METHOD]: { type: 'string', value: 'GET' }, + [URL_FULL]: { type: 'string', value: 'http://sentry-test-site.example/2?id=1#fragment' }, + [URL_QUERY]: { type: 'string', value: 'id=1' }, + [URL_FRAGMENT]: { type: 'string', value: 'fragment' }, + [HTTP_RESPONSE_STATUS_CODE]: { type: 'integer', value: 200 }, + [HTTP_RESPONSE_BODY_SIZE]: { type: 'integer', value: 2 }, + [SENTRY_OP]: { type: 'string', value: 'http.client' }, + [SENTRY_ORIGIN]: { type: 'string', value: 'auto.http.browser' }, type: { type: 'string', value: 'fetch' }, - 'server.address': { type: 'string', value: 'sentry-test-site.example' }, + [SERVER_ADDRESS]: { type: 'string', value: 'sentry-test-site.example' }, }), }); }); @@ -153,16 +164,16 @@ sentryTest( end_timestamp: expect.any(Number), trace_id: rootSpan.trace_id, attributes: expect.objectContaining({ - 'http.request.method': { type: 'string', value: 'GET' }, - 'url.full': { type: 'string', value: 'http://sentry-test.io/api/users?id=1#fragment' }, - 'url.query': { type: 'string', value: 'id=1' }, - 'url.fragment': { type: 'string', value: 'fragment' }, - 'http.response.status_code': { type: 'integer', value: 200 }, - 'http.response.body.size': { type: 'integer', value: 2 }, - 'sentry.op': { type: 'string', value: 'http.client' }, - 'sentry.origin': { type: 'string', value: 'auto.http.browser' }, + [HTTP_REQUEST_METHOD]: { type: 'string', value: 'GET' }, + [URL_FULL]: { type: 'string', value: 'http://sentry-test.io/api/users?id=1#fragment' }, + [URL_QUERY]: { type: 'string', value: 'id=1' }, + [URL_FRAGMENT]: { type: 'string', value: 'fragment' }, + [HTTP_RESPONSE_STATUS_CODE]: { type: 'integer', value: 200 }, + [HTTP_RESPONSE_BODY_SIZE]: { type: 'integer', value: 2 }, + [SENTRY_OP]: { type: 'string', value: 'http.client' }, + [SENTRY_ORIGIN]: { type: 'string', value: 'auto.http.browser' }, type: { type: 'string', value: 'fetch' }, - 'server.address': { type: 'string', value: 'sentry-test.io' }, + [SERVER_ADDRESS]: { type: 'string', value: 'sentry-test.io' }, }), }); }, diff --git a/dev-packages/browser-integration-tests/suites/tracing/request/fetch-track-stream-performance/test.ts b/dev-packages/browser-integration-tests/suites/tracing/request/fetch-track-stream-performance/test.ts index e2149b8d96bd..77047908b7da 100644 --- a/dev-packages/browser-integration-tests/suites/tracing/request/fetch-track-stream-performance/test.ts +++ b/dev-packages/browser-integration-tests/suites/tracing/request/fetch-track-stream-performance/test.ts @@ -1,3 +1,4 @@ +import { HTTP_REQUEST_METHOD, URL_FULL, URL_DOMAIN } from '@sentry/conventions/attributes'; import http from 'http'; import type { AddressInfo } from 'net'; import { expect } from '@playwright/test'; @@ -53,9 +54,9 @@ sentryTest( expect(streamSpan).toMatchObject({ name: 'GET sentry-test-site.example', attributes: expect.objectContaining({ - 'http.request.method': { type: 'string', value: 'GET' }, - 'url.full': { type: 'string', value: 'http://sentry-test-site.example/delayed' }, - 'url.domain': { type: 'string', value: 'sentry-test-site.example' }, + [HTTP_REQUEST_METHOD]: { type: 'string', value: 'GET' }, + [URL_FULL]: { type: 'string', value: 'http://sentry-test-site.example/delayed' }, + [URL_DOMAIN]: { type: 'string', value: 'sentry-test-site.example' }, type: { type: 'string', value: 'fetch' }, }), }); diff --git a/dev-packages/browser-integration-tests/suites/tracing/request/fetch/test.ts b/dev-packages/browser-integration-tests/suites/tracing/request/fetch/test.ts index 7c6fc57cf95d..1f8ec9ba655e 100644 --- a/dev-packages/browser-integration-tests/suites/tracing/request/fetch/test.ts +++ b/dev-packages/browser-integration-tests/suites/tracing/request/fetch/test.ts @@ -1,3 +1,4 @@ +import { URL_FULL, HTTP_REQUEST_METHOD, URL_DOMAIN, SERVER_ADDRESS } from '@sentry/conventions/attributes'; import { expect } from '@playwright/test'; import { sentryTest } from '../../../../utils/fixtures'; import { shouldSkipTracingTest } from '../../../../utils/helpers'; @@ -20,9 +21,7 @@ sentryTest('creates spans for fetch requests', async ({ getLocalTestUrl, page }) const allSpans = spans; const requestSpans = allSpans .filter(s => getSpanOp(s) === 'http.client') - .sort((a, b) => - (a.attributes!['url.full']!.value as string).localeCompare(b.attributes!['url.full']!.value as string), - ); + .sort((a, b) => (a.attributes![URL_FULL]!.value as string).localeCompare(b.attributes![URL_FULL]!.value as string)); expect(requestSpans).toHaveLength(3); @@ -36,10 +35,10 @@ sentryTest('creates spans for fetch requests', async ({ getLocalTestUrl, page }) end_timestamp: expect.any(Number), trace_id: pageloadSpan?.trace_id, attributes: expect.objectContaining({ - 'http.request.method': { type: 'string', value: 'GET' }, - 'url.full': { type: 'string', value: `http://sentry-test-site.example/${index}` }, - 'url.domain': { type: 'string', value: 'sentry-test-site.example' }, - 'server.address': { type: 'string', value: 'sentry-test-site.example' }, + [HTTP_REQUEST_METHOD]: { type: 'string', value: 'GET' }, + [URL_FULL]: { type: 'string', value: `http://sentry-test-site.example/${index}` }, + [URL_DOMAIN]: { type: 'string', value: 'sentry-test-site.example' }, + [SERVER_ADDRESS]: { type: 'string', value: 'sentry-test-site.example' }, type: { type: 'string', value: 'fetch' }, }), }), diff --git a/dev-packages/browser-integration-tests/suites/tracing/request/xhr-data-url/test.ts b/dev-packages/browser-integration-tests/suites/tracing/request/xhr-data-url/test.ts index 199ade590012..29f49ae5a6b9 100644 --- a/dev-packages/browser-integration-tests/suites/tracing/request/xhr-data-url/test.ts +++ b/dev-packages/browser-integration-tests/suites/tracing/request/xhr-data-url/test.ts @@ -1,3 +1,4 @@ +import { HTTP_REQUEST_METHOD, URL_FULL } from '@sentry/conventions/attributes'; import { expect } from '@playwright/test'; import { sentryTest } from '../../../../utils/fixtures'; import { shouldSkipTracingTest } from '../../../../utils/helpers'; @@ -18,9 +19,9 @@ sentryTest('sanitizes data URLs in xhr span name and attributes', async ({ getLo expect(span.name).toBe('GET'); expect(span.parent_span_id).toBe(pageload.span_id); expect(span.trace_id).toBe(pageload.trace_id); - expect(span.attributes['http.request.method']).toEqual({ type: 'string', value: 'GET' }); + expect(span.attributes[HTTP_REQUEST_METHOD]).toEqual({ type: 'string', value: 'GET' }); expect(span.attributes.type).toEqual({ type: 'string', value: 'xhr' }); - expect(span.attributes['url.full']).toEqual({ + expect(span.attributes[URL_FULL]).toEqual({ type: 'string', value: 'data:text/plain,base64,SGVsbG8gV2... [truncated]', }); diff --git a/dev-packages/browser-integration-tests/suites/tracing/request/xhr-relative-url-static/test.ts b/dev-packages/browser-integration-tests/suites/tracing/request/xhr-relative-url-static/test.ts index 110df467837f..096ca7325688 100644 --- a/dev-packages/browser-integration-tests/suites/tracing/request/xhr-relative-url-static/test.ts +++ b/dev-packages/browser-integration-tests/suites/tracing/request/xhr-relative-url-static/test.ts @@ -1,3 +1,4 @@ +import { HTTP_REQUEST_METHOD, URL_FULL, SERVER_ADDRESS } from '@sentry/conventions/attributes'; import { expect } from '@playwright/test'; import { sentryTest, TEST_HOST } from '../../../../utils/fixtures'; import { @@ -28,9 +29,9 @@ sentryTest('should create spans for xhr requests', async ({ getLocalTestUrl, pag timestamp: expect.any(Number), trace_id: tracingEvent.contexts?.trace?.trace_id, data: { - 'http.request.method': 'GET', - 'url.full': `${TEST_HOST}/test-req/${index}`, - 'server.address': 'sentry-test.io', + [HTTP_REQUEST_METHOD]: 'GET', + [URL_FULL]: `${TEST_HOST}/test-req/${index}`, + [SERVER_ADDRESS]: 'sentry-test.io', type: 'xhr', }, }), diff --git a/dev-packages/browser-integration-tests/suites/tracing/request/xhr-relative-url/test.ts b/dev-packages/browser-integration-tests/suites/tracing/request/xhr-relative-url/test.ts index 960030218253..ec485b0bf340 100644 --- a/dev-packages/browser-integration-tests/suites/tracing/request/xhr-relative-url/test.ts +++ b/dev-packages/browser-integration-tests/suites/tracing/request/xhr-relative-url/test.ts @@ -1,3 +1,4 @@ +import { URL_FULL, HTTP_REQUEST_METHOD, URL_DOMAIN, SERVER_ADDRESS } from '@sentry/conventions/attributes'; import { expect } from '@playwright/test'; import { sentryTest, TEST_HOST } from '../../../../utils/fixtures'; import { shouldSkipTracingTest } from '../../../../utils/helpers'; @@ -17,9 +18,7 @@ sentryTest('names spans for relative XHR requests after the page domain', async await page.evaluate(() => (window as any).Sentry.flush()); const requestSpans = spans .filter(s => getSpanOp(s) === 'http.client') - .sort((a, b) => - (a.attributes!['url.full']!.value as string).localeCompare(b.attributes!['url.full']!.value as string), - ); + .sort((a, b) => (a.attributes![URL_FULL]!.value as string).localeCompare(b.attributes![URL_FULL]!.value as string)); expect(requestSpans).toHaveLength(3); @@ -30,10 +29,10 @@ sentryTest('names spans for relative XHR requests after the page domain', async parent_span_id: pageloadSpan.span_id, trace_id: pageloadSpan.trace_id, attributes: expect.objectContaining({ - 'http.request.method': { type: 'string', value: 'GET' }, - 'url.full': { type: 'string', value: `${TEST_HOST}/test-req/${index}` }, - 'url.domain': { type: 'string', value: 'sentry-test.io' }, - 'server.address': { type: 'string', value: 'sentry-test.io' }, + [HTTP_REQUEST_METHOD]: { type: 'string', value: 'GET' }, + [URL_FULL]: { type: 'string', value: `${TEST_HOST}/test-req/${index}` }, + [URL_DOMAIN]: { type: 'string', value: 'sentry-test.io' }, + [SERVER_ADDRESS]: { type: 'string', value: 'sentry-test.io' }, type: { type: 'string', value: 'xhr' }, }), }), diff --git a/dev-packages/browser-integration-tests/suites/tracing/request/xhr-static/test.ts b/dev-packages/browser-integration-tests/suites/tracing/request/xhr-static/test.ts index 971cb0f5c030..585937e157b1 100644 --- a/dev-packages/browser-integration-tests/suites/tracing/request/xhr-static/test.ts +++ b/dev-packages/browser-integration-tests/suites/tracing/request/xhr-static/test.ts @@ -1,3 +1,4 @@ +import { HTTP_REQUEST_METHOD, URL_FULL, SERVER_ADDRESS } from '@sentry/conventions/attributes'; import { expect } from '@playwright/test'; import type { Event } from '@sentry/core'; import { sentryTest } from '../../../../utils/fixtures'; @@ -26,9 +27,9 @@ sentryTest('should create spans for XHR requests', async ({ getLocalTestUrl, pag timestamp: expect.any(Number), trace_id: eventData.contexts?.trace?.trace_id, data: { - 'http.request.method': 'GET', - 'url.full': `http://sentry-test-site.example/${index}`, - 'server.address': 'sentry-test-site.example', + [HTTP_REQUEST_METHOD]: 'GET', + [URL_FULL]: `http://sentry-test-site.example/${index}`, + [SERVER_ADDRESS]: 'sentry-test-site.example', type: 'xhr', }, }), diff --git a/dev-packages/browser-integration-tests/suites/tracing/request/xhr-strip-query-and-fragment/test.ts b/dev-packages/browser-integration-tests/suites/tracing/request/xhr-strip-query-and-fragment/test.ts index 9404bbbd65a2..1474afa00f89 100644 --- a/dev-packages/browser-integration-tests/suites/tracing/request/xhr-strip-query-and-fragment/test.ts +++ b/dev-packages/browser-integration-tests/suites/tracing/request/xhr-strip-query-and-fragment/test.ts @@ -1,3 +1,13 @@ +import { + HTTP_REQUEST_METHOD, + URL_FULL, + URL_QUERY, + HTTP_RESPONSE_STATUS_CODE, + SENTRY_OP, + SENTRY_ORIGIN, + SERVER_ADDRESS, + URL_FRAGMENT, +} from '@sentry/conventions/attributes'; import { expect } from '@playwright/test'; import { sentryTest } from '../../../../utils/fixtures'; import { shouldSkipTracingTest } from '../../../../utils/helpers'; @@ -29,14 +39,14 @@ sentryTest('strips query params in XHR request spans', async ({ getLocalTestUrl, end_timestamp: expect.any(Number), trace_id: rootSpan.trace_id, attributes: expect.objectContaining({ - 'http.request.method': { type: 'string', value: 'GET' }, - 'url.full': { type: 'string', value: 'http://sentry-test-site.example/0?id=123;page=5' }, - 'url.query': { type: 'string', value: 'id=123;page=5' }, - 'http.response.status_code': { type: 'integer', value: 200 }, - 'sentry.op': { type: 'string', value: 'http.client' }, - 'sentry.origin': { type: 'string', value: 'auto.http.browser' }, + [HTTP_REQUEST_METHOD]: { type: 'string', value: 'GET' }, + [URL_FULL]: { type: 'string', value: 'http://sentry-test-site.example/0?id=123;page=5' }, + [URL_QUERY]: { type: 'string', value: 'id=123;page=5' }, + [HTTP_RESPONSE_STATUS_CODE]: { type: 'integer', value: 200 }, + [SENTRY_OP]: { type: 'string', value: 'http.client' }, + [SENTRY_ORIGIN]: { type: 'string', value: 'auto.http.browser' }, type: { type: 'string', value: 'xhr' }, - 'server.address': { type: 'string', value: 'sentry-test-site.example' }, + [SERVER_ADDRESS]: { type: 'string', value: 'sentry-test-site.example' }, }), }); @@ -69,14 +79,14 @@ sentryTest('strips hash fragment in XHR request spans', async ({ getLocalTestUrl end_timestamp: expect.any(Number), trace_id: rootSpan.trace_id, attributes: expect.objectContaining({ - 'http.request.method': { type: 'string', value: 'GET' }, - 'url.full': { type: 'string', value: 'http://sentry-test-site.example/1#fragment' }, - 'url.fragment': { type: 'string', value: 'fragment' }, - 'http.response.status_code': { type: 'integer', value: 200 }, - 'sentry.op': { type: 'string', value: 'http.client' }, - 'sentry.origin': { type: 'string', value: 'auto.http.browser' }, + [HTTP_REQUEST_METHOD]: { type: 'string', value: 'GET' }, + [URL_FULL]: { type: 'string', value: 'http://sentry-test-site.example/1#fragment' }, + [URL_FRAGMENT]: { type: 'string', value: 'fragment' }, + [HTTP_RESPONSE_STATUS_CODE]: { type: 'integer', value: 200 }, + [SENTRY_OP]: { type: 'string', value: 'http.client' }, + [SENTRY_ORIGIN]: { type: 'string', value: 'auto.http.browser' }, type: { type: 'string', value: 'xhr' }, - 'server.address': { type: 'string', value: 'sentry-test-site.example' }, + [SERVER_ADDRESS]: { type: 'string', value: 'sentry-test-site.example' }, }), }); @@ -109,15 +119,15 @@ sentryTest('strips hash fragment and query params in XHR request spans', async ( end_timestamp: expect.any(Number), trace_id: rootSpan.trace_id, attributes: expect.objectContaining({ - 'http.request.method': { type: 'string', value: 'GET' }, - 'url.full': { type: 'string', value: 'http://sentry-test-site.example/2?id=1#fragment' }, - 'url.query': { type: 'string', value: 'id=1' }, - 'url.fragment': { type: 'string', value: 'fragment' }, - 'http.response.status_code': { type: 'integer', value: 200 }, - 'sentry.op': { type: 'string', value: 'http.client' }, - 'sentry.origin': { type: 'string', value: 'auto.http.browser' }, + [HTTP_REQUEST_METHOD]: { type: 'string', value: 'GET' }, + [URL_FULL]: { type: 'string', value: 'http://sentry-test-site.example/2?id=1#fragment' }, + [URL_QUERY]: { type: 'string', value: 'id=1' }, + [URL_FRAGMENT]: { type: 'string', value: 'fragment' }, + [HTTP_RESPONSE_STATUS_CODE]: { type: 'integer', value: 200 }, + [SENTRY_OP]: { type: 'string', value: 'http.client' }, + [SENTRY_ORIGIN]: { type: 'string', value: 'auto.http.browser' }, type: { type: 'string', value: 'xhr' }, - 'server.address': { type: 'string', value: 'sentry-test-site.example' }, + [SERVER_ADDRESS]: { type: 'string', value: 'sentry-test-site.example' }, }), }); }); @@ -150,15 +160,15 @@ sentryTest( end_timestamp: expect.any(Number), trace_id: rootSpan.trace_id, attributes: expect.objectContaining({ - 'http.request.method': { type: 'string', value: 'GET' }, - 'url.full': { type: 'string', value: 'http://sentry-test.io/api/users?id=1#fragment' }, - 'url.query': { type: 'string', value: 'id=1' }, - 'url.fragment': { type: 'string', value: 'fragment' }, - 'http.response.status_code': { type: 'integer', value: 200 }, - 'sentry.op': { type: 'string', value: 'http.client' }, - 'sentry.origin': { type: 'string', value: 'auto.http.browser' }, + [HTTP_REQUEST_METHOD]: { type: 'string', value: 'GET' }, + [URL_FULL]: { type: 'string', value: 'http://sentry-test.io/api/users?id=1#fragment' }, + [URL_QUERY]: { type: 'string', value: 'id=1' }, + [URL_FRAGMENT]: { type: 'string', value: 'fragment' }, + [HTTP_RESPONSE_STATUS_CODE]: { type: 'integer', value: 200 }, + [SENTRY_OP]: { type: 'string', value: 'http.client' }, + [SENTRY_ORIGIN]: { type: 'string', value: 'auto.http.browser' }, type: { type: 'string', value: 'xhr' }, - 'server.address': { type: 'string', value: 'sentry-test.io' }, + [SERVER_ADDRESS]: { type: 'string', value: 'sentry-test.io' }, }), }); }, diff --git a/dev-packages/browser-integration-tests/suites/tracing/request/xhr/test.ts b/dev-packages/browser-integration-tests/suites/tracing/request/xhr/test.ts index 980188468e43..87f89ba4b05c 100644 --- a/dev-packages/browser-integration-tests/suites/tracing/request/xhr/test.ts +++ b/dev-packages/browser-integration-tests/suites/tracing/request/xhr/test.ts @@ -1,3 +1,4 @@ +import { URL_FULL, HTTP_REQUEST_METHOD, URL_DOMAIN, SERVER_ADDRESS } from '@sentry/conventions/attributes'; import { expect } from '@playwright/test'; import { sentryTest } from '../../../../utils/fixtures'; import { shouldSkipTracingTest } from '../../../../utils/helpers'; @@ -20,9 +21,7 @@ sentryTest('creates spans for XHR requests', async ({ getLocalTestUrl, page }) = const allSpans = spans; const requestSpans = allSpans .filter(s => getSpanOp(s) === 'http.client') - .sort((a, b) => - (a.attributes!['url.full']!.value as string).localeCompare(b.attributes!['url.full']!.value as string), - ); + .sort((a, b) => (a.attributes![URL_FULL]!.value as string).localeCompare(b.attributes![URL_FULL]!.value as string)); expect(requestSpans).toHaveLength(3); @@ -36,10 +35,10 @@ sentryTest('creates spans for XHR requests', async ({ getLocalTestUrl, page }) = end_timestamp: expect.any(Number), trace_id: pageloadSpan?.trace_id, attributes: expect.objectContaining({ - 'http.request.method': { type: 'string', value: 'GET' }, - 'url.full': { type: 'string', value: `http://sentry-test-site.example/${index}` }, - 'url.domain': { type: 'string', value: 'sentry-test-site.example' }, - 'server.address': { type: 'string', value: 'sentry-test-site.example' }, + [HTTP_REQUEST_METHOD]: { type: 'string', value: 'GET' }, + [URL_FULL]: { type: 'string', value: `http://sentry-test-site.example/${index}` }, + [URL_DOMAIN]: { type: 'string', value: 'sentry-test-site.example' }, + [SERVER_ADDRESS]: { type: 'string', value: 'sentry-test-site.example' }, type: { type: 'string', value: 'xhr' }, }), }), diff --git a/dev-packages/browser-integration-tests/suites/tracing/user-timing/pageload-measure-spans-domexception-details/test.ts b/dev-packages/browser-integration-tests/suites/tracing/user-timing/pageload-measure-spans-domexception-details/test.ts index 6d4c11e930c3..b3bb77c23c64 100644 --- a/dev-packages/browser-integration-tests/suites/tracing/user-timing/pageload-measure-spans-domexception-details/test.ts +++ b/dev-packages/browser-integration-tests/suites/tracing/user-timing/pageload-measure-spans-domexception-details/test.ts @@ -1,3 +1,4 @@ +import { SENTRY_ORIGIN } from '@sentry/conventions/attributes'; import { expect } from '@playwright/test'; import { sentryTest } from '../../../../utils/fixtures'; import { shouldSkipTracingTest } from '../../../../utils/helpers'; @@ -18,7 +19,7 @@ sentryTest( expect(measures).toHaveLength(3); measures.forEach(span => { expect(span.parent_span_id).toBe(pageload.span_id); - expect(span.attributes['sentry.origin']).toEqual({ type: 'string', value: 'auto.browser.user_timing.measure' }); + expect(span.attributes[SENTRY_ORIGIN]).toEqual({ type: 'string', value: 'auto.browser.user_timing.measure' }); }); const restricted = measures.find(span => span.name === 'restricted-test-measure')!; expect(Object.keys(restricted.attributes).filter(key => key.includes('detail'))).toHaveLength(0); diff --git a/dev-packages/browser-integration-tests/suites/tracing/user-timing/pageload-measure-spans/test.ts b/dev-packages/browser-integration-tests/suites/tracing/user-timing/pageload-measure-spans/test.ts index 39bcf8b1fed8..d85d6b7fc662 100644 --- a/dev-packages/browser-integration-tests/suites/tracing/user-timing/pageload-measure-spans/test.ts +++ b/dev-packages/browser-integration-tests/suites/tracing/user-timing/pageload-measure-spans/test.ts @@ -1,3 +1,4 @@ +import { URL_FULL, SENTRY_OP, SENTRY_ORIGIN } from '@sentry/conventions/attributes'; import { expect } from '@playwright/test'; import { sentryTest } from '../../../../utils/fixtures'; import { shouldSkipTracingTest } from '../../../../utils/helpers'; @@ -16,7 +17,7 @@ sentryTest('clamps pre-SDK measures to the document request start', async ({ get expect(browserSpans.length).toBeGreaterThanOrEqual(4); const request = browserSpans.find(span => getSpanOp(span) === 'browser.request')!; expect(request.name).toBe('Request'); - expect(request.attributes['url.full']).toEqual({ type: 'string', value: page.url() }); + expect(request.attributes[URL_FULL]).toEqual({ type: 'string', value: page.url() }); const measures = spans.filter(span => getSpanOp(span) === 'measure'); expect(measures).toHaveLength(1); const measure = measures[0]; @@ -28,7 +29,7 @@ sentryTest('clamps pre-SDK measures to the document request start', async ({ get type: expect.stringMatching(/^(double|integer)$/), value: expect.any(Number), }, - 'sentry.op': { type: 'string', value: 'measure' }, - 'sentry.origin': { type: 'string', value: 'auto.browser.user_timing.measure' }, + [SENTRY_OP]: { type: 'string', value: 'measure' }, + [SENTRY_ORIGIN]: { type: 'string', value: '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 f7b06272f603..536f5c2f4607 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,3 +1,4 @@ +import { SENTRY_ORIGIN } from '@sentry/conventions/attributes'; import { expect } from '@playwright/test'; import { sentryTest } from '../../../../utils/fixtures'; import { shouldSkipTracingTest } from '../../../../utils/helpers'; @@ -19,7 +20,7 @@ sentryTest('captures non-ignored mark and measure spans', async ({ getLocalTestU .map(span => ({ name: span.name, op: getSpanOp(span), - origin: span.attributes['sentry.origin']?.value, + origin: span.attributes[SENTRY_ORIGIN]?.value, })) .sort((a, b) => a.name.localeCompare(b.name)); diff --git a/dev-packages/browser-integration-tests/utils/helpers.ts b/dev-packages/browser-integration-tests/utils/helpers.ts index 05b5aebcf5f4..a8a45008e738 100644 --- a/dev-packages/browser-integration-tests/utils/helpers.ts +++ b/dev-packages/browser-integration-tests/utils/helpers.ts @@ -548,6 +548,22 @@ export async function getFirstSentryEnvelopeRequest( return req; } +export async function waitForLcpCandidate(page: Page, selector: string): Promise { + await page.locator(selector).evaluate(element => { + return new Promise(resolve => { + const observer = new PerformanceObserver(list => { + if (list.getEntries().some(entry => 'element' in entry && entry.element === element)) { + observer.disconnect(); + resolve(); + } + }); + + // The image may already have painted before the test attaches its observer. + observer.observe({ type: 'largest-contentful-paint', buffered: true }); + }); + }); +} + export async function hidePage(page: Page): Promise { // web-vitals processes an interaction's event entries in `requestIdleCallback(..., { timeout: // 1000 })`, and Chromium only reaches idle here once that timeout elapses. Hiding before that From b43b999cd9c88cf16e89a8fd64b77410d5d16dc5 Mon Sep 17 00:00:00 2001 From: Martin Sonnberger Date: Wed, 7 Oct 2026 15:58:43 +0200 Subject: [PATCH 4/6] test(browser): tighten request and web vital assertions Co-Authored-By: GPT-6 --- .../metrics/pageload-browser-spans/test.ts | 11 ++--- .../pageload-resource-spans-static/test.ts | 27 +++++++----- .../tracing/metrics/web-vitals-fp-fcp/test.ts | 14 +++--- .../web-vitals-inp-before-send-span/test.ts | 25 +++++++---- .../metrics/web-vitals-inp-late/test.ts | 25 +++++++---- .../metrics/web-vitals-inp-navigate/test.ts | 42 +++++++++++------- .../web-vitals-inp-parametrized-late/test.ts | 25 +++++++---- .../web-vitals-inp-parametrized/test.ts | 25 +++++++---- .../tracing/metrics/web-vitals-inp/test.ts | 44 ++++++++++--------- .../metrics/web-vitals-ttfb-static/test.ts | 7 ++- .../fetch-strip-query-and-fragment/test.ts | 21 ++++----- .../utils/helpers.ts | 33 ++++++++++---- 12 files changed, 181 insertions(+), 118 deletions(-) diff --git a/dev-packages/browser-integration-tests/suites/tracing/metrics/pageload-browser-spans/test.ts b/dev-packages/browser-integration-tests/suites/tracing/metrics/pageload-browser-spans/test.ts index 977dbd399033..8be9de6373d1 100644 --- a/dev-packages/browser-integration-tests/suites/tracing/metrics/pageload-browser-spans/test.ts +++ b/dev-packages/browser-integration-tests/suites/tracing/metrics/pageload-browser-spans/test.ts @@ -1,3 +1,4 @@ +import { PAGELOAD, BROWSER, BROWSER_LOAD_EVENT, BROWSER_REQUEST, BROWSER_RESPONSE } from '@sentry/conventions/op'; import { URL_FULL } from '@sentry/conventions/attributes'; import { expect } from '@playwright/test'; import { sentryTest } from '../../../../utils/fixtures'; @@ -7,18 +8,18 @@ import { collectStreamedSpans, getSpanOp, waitForStreamedSpan } from '../../../. sentryTest('adds browser performance spans to the pageload segment', async ({ getLocalTestUrl, page }) => { sentryTest.skip(shouldSkipTracingTest()); const spans = collectStreamedSpans(page); - const pageloadPromise = waitForStreamedSpan(page, span => span.is_segment && getSpanOp(span) === 'pageload'); + const pageloadPromise = waitForStreamedSpan(page, span => span.is_segment && getSpanOp(span) === PAGELOAD); const url = await getLocalTestUrl({ testDir: __dirname }); await page.goto(url); const pageload = await pageloadPromise; await page.evaluate(() => (window as any).Sentry.flush()); - const browserSpans = spans.filter(span => getSpanOp(span)?.startsWith('browser')); + const browserSpans = spans.filter(span => getSpanOp(span)?.startsWith(BROWSER)); expect(browserSpans.length).toBeGreaterThanOrEqual(4); [ - { op: 'browser.load_event', name: 'Load event' }, - { op: 'browser.request', name: 'Request' }, - { op: 'browser.response', name: 'Response' }, + { op: BROWSER_LOAD_EVENT, name: 'Load event' }, + { op: BROWSER_REQUEST, name: 'Request' }, + { op: BROWSER_RESPONSE, name: 'Response' }, ].forEach(({ op, name }) => { const matches = browserSpans.filter(span => getSpanOp(span) === op); expect(matches).toHaveLength(1); diff --git a/dev-packages/browser-integration-tests/suites/tracing/metrics/pageload-resource-spans-static/test.ts b/dev-packages/browser-integration-tests/suites/tracing/metrics/pageload-resource-spans-static/test.ts index c0b943433605..c15ef574991a 100644 --- a/dev-packages/browser-integration-tests/suites/tracing/metrics/pageload-resource-spans-static/test.ts +++ b/dev-packages/browser-integration-tests/suites/tracing/metrics/pageload-resource-spans-static/test.ts @@ -1,3 +1,4 @@ +import { PAGELOAD, RESOURCE, RESOURCE_SCRIPT, RESOURCE_LINK, RESOURCE_IMG } from '@sentry/conventions/op'; import { HTTP_REQUEST_SAME_ORIGIN, URL_FULL, @@ -31,7 +32,7 @@ import type { Route } from '@playwright/test'; import { expect } from '@playwright/test'; import { type Event } from '@sentry/core'; import { sentryTest } from '../../../../utils/fixtures'; -import { getFirstSentryEnvelopeRequest, shouldSkipTracingTest } from '../../../../utils/helpers'; +import { envelopeRequestParser, shouldSkipTracingTest, waitForTransactionRequest } from '../../../../utils/helpers'; sentryTest('adds resource spans to pageload transaction', async ({ getLocalTestUrl, page, browserName }) => { if (shouldSkipTracingTest()) { @@ -71,12 +72,14 @@ sentryTest('adds resource spans to pageload transaction', async ({ getLocalTestU const url = await getLocalTestUrl({ testDir: __dirname }); - const eventData = await getFirstSentryEnvelopeRequest(page, url); - const resourceSpans = eventData.spans?.filter(({ op }) => op?.startsWith('resource')); + const pageloadPromise = waitForTransactionRequest(page, event => event.contexts?.trace?.op === PAGELOAD); + await page.goto(url); + const eventData = envelopeRequestParser(await pageloadPromise); + const resourceSpans = eventData.spans?.filter(({ op }) => op?.startsWith(RESOURCE)); - const scriptSpans = resourceSpans?.filter(({ op }) => op === 'resource.script'); - const linkSpan = resourceSpans?.filter(({ op }) => op === 'resource.link')[0]; - const imgSpan = resourceSpans?.filter(({ op }) => op === 'resource.img')[0]; + const scriptSpans = resourceSpans?.filter(({ op }) => op === RESOURCE_SCRIPT); + const linkSpan = resourceSpans?.filter(({ op }) => op === RESOURCE_LINK)[0]; + const imgSpan = resourceSpans?.filter(({ op }) => op === RESOURCE_IMG)[0]; const spanId = eventData.contexts?.trace?.span_id; const traceId = eventData.contexts?.trace?.trace_id; @@ -124,7 +127,7 @@ sentryTest('adds resource spans to pageload transaction', async ({ getLocalTestU [HTTP_REQUEST_TIME_TO_FIRST_BYTE]: expect.any(Number), [NETWORK_PROTOCOL_NAME]: '', [NETWORK_PROTOCOL_VERSION]: 'unknown', - [SENTRY_OP]: 'resource.img', + [SENTRY_OP]: RESOURCE_IMG, [SENTRY_ORIGIN]: 'auto.resource.browser.metrics', [SERVER_ADDRESS]: 'sentry-test-site.example', [URL_DOMAIN]: 'sentry-test-site.example', @@ -140,7 +143,7 @@ sentryTest('adds resource spans to pageload transaction', async ({ getLocalTestU }), }, description: 'https://sentry-test-site.example/path/to/image.svg', - op: 'resource.img', + op: RESOURCE_IMG, origin: 'auto.resource.browser.metrics', status: 'ok', parent_span_id: spanId, @@ -177,7 +180,7 @@ sentryTest('adds resource spans to pageload transaction', async ({ getLocalTestU [HTTP_REQUEST_TIME_TO_FIRST_BYTE]: expect.any(Number), [NETWORK_PROTOCOL_NAME]: '', [NETWORK_PROTOCOL_VERSION]: 'unknown', - [SENTRY_OP]: 'resource.link', + [SENTRY_OP]: RESOURCE_LINK, [SENTRY_ORIGIN]: 'auto.resource.browser.metrics', [SERVER_ADDRESS]: 'sentry-test-site.example', [URL_DOMAIN]: 'sentry-test-site.example', @@ -193,7 +196,7 @@ sentryTest('adds resource spans to pageload transaction', async ({ getLocalTestU }), }, description: 'https://sentry-test-site.example/path/to/style.css', - op: 'resource.link', + op: RESOURCE_LINK, origin: 'auto.resource.browser.metrics', status: 'ok', parent_span_id: spanId, @@ -223,7 +226,7 @@ sentryTest('adds resource spans to pageload transaction', async ({ getLocalTestU [HTTP_REQUEST_TIME_TO_FIRST_BYTE]: expect.any(Number), [NETWORK_PROTOCOL_NAME]: '', [NETWORK_PROTOCOL_VERSION]: 'unknown', - [SENTRY_OP]: 'resource.script', + [SENTRY_OP]: RESOURCE_SCRIPT, [SENTRY_ORIGIN]: 'auto.resource.browser.metrics', [SERVER_ADDRESS]: 'sentry-test-site.example', [URL_DOMAIN]: 'sentry-test-site.example', @@ -239,7 +242,7 @@ sentryTest('adds resource spans to pageload transaction', async ({ getLocalTestU }), }, description: 'https://sentry-test-site.example/path/to/script.js', - op: 'resource.script', + op: RESOURCE_SCRIPT, origin: 'auto.resource.browser.metrics', status: 'ok', parent_span_id: spanId, diff --git a/dev-packages/browser-integration-tests/suites/tracing/metrics/web-vitals-fp-fcp/test.ts b/dev-packages/browser-integration-tests/suites/tracing/metrics/web-vitals-fp-fcp/test.ts index 93329de1675d..f3769c45992e 100644 --- a/dev-packages/browser-integration-tests/suites/tracing/metrics/web-vitals-fp-fcp/test.ts +++ b/dev-packages/browser-integration-tests/suites/tracing/metrics/web-vitals-fp-fcp/test.ts @@ -1,25 +1,27 @@ +import { PAGELOAD, BROWSER_PAINT } from '@sentry/conventions/op'; +import { BROWSER_WEB_VITAL_FP_VALUE, BROWSER_WEB_VITAL_FCP_VALUE } from '@sentry/conventions/attributes'; import { expect } from '@playwright/test'; import { sentryTest } from '../../../../utils/fixtures'; import { shouldSkipTracingTest } from '../../../../utils/helpers'; import { collectStreamedSpans, getSpanOp, waitForStreamedSpan } from '../../../../utils/spanUtils'; [ - { vital: 'fp', name: 'first-paint', chromiumOnly: true }, - { vital: 'fcp', name: 'first-contentful-paint', chromiumOnly: false }, -].forEach(({ vital, name, chromiumOnly }) => { + { vital: 'fp', attribute: BROWSER_WEB_VITAL_FP_VALUE, name: 'first-paint', chromiumOnly: true }, + { vital: 'fcp', attribute: BROWSER_WEB_VITAL_FCP_VALUE, name: 'first-contentful-paint', chromiumOnly: false }, +].forEach(({ vital, attribute, name, chromiumOnly }) => { sentryTest(`captures ${vital.toUpperCase()} and its paint span`, async ({ getLocalTestUrl, page, browserName }) => { sentryTest.skip(shouldSkipTracingTest() || (chromiumOnly && browserName !== 'chromium')); const spans = collectStreamedSpans(page); - const pageloadPromise = waitForStreamedSpan(page, span => span.is_segment && getSpanOp(span) === 'pageload'); + const pageloadPromise = waitForStreamedSpan(page, span => span.is_segment && getSpanOp(span) === PAGELOAD); const url = await getLocalTestUrl({ testDir: __dirname }); await page.goto(url); const pageload = await pageloadPromise; await page.evaluate(() => (window as any).Sentry.flush()); - expect(pageload.attributes[`browser.web_vital.${vital}.value`]?.value).toBeGreaterThan(0); + expect(pageload.attributes[attribute]?.value).toBeGreaterThan(0); const paintSpans = spans.filter(span => span.name === name); expect(paintSpans).toHaveLength(1); - expect(getSpanOp(paintSpans[0])).toBe('browser.paint'); + expect(getSpanOp(paintSpans[0])).toBe(BROWSER_PAINT); expect(paintSpans[0].parent_span_id).toBe(pageload.span_id); expect(paintSpans[0].trace_id).toBe(pageload.trace_id); }); diff --git a/dev-packages/browser-integration-tests/suites/tracing/metrics/web-vitals-inp-before-send-span/test.ts b/dev-packages/browser-integration-tests/suites/tracing/metrics/web-vitals-inp-before-send-span/test.ts index f725e3909278..e65c3e9b90de 100644 --- a/dev-packages/browser-integration-tests/suites/tracing/metrics/web-vitals-inp-before-send-span/test.ts +++ b/dev-packages/browser-integration-tests/suites/tracing/metrics/web-vitals-inp-before-send-span/test.ts @@ -1,8 +1,14 @@ +import { UI_INTERACTION_CLICK } from '@sentry/conventions/op'; import { BROWSER_WEB_VITAL_INP_VALUE } from '@sentry/conventions/attributes'; import { expect } from '@playwright/test'; import { sentryTest } from '../../../../utils/fixtures'; import { hidePage, shouldSkipTracingTest } from '../../../../utils/helpers'; -import { getSpanOp, getSpansFromEnvelope, waitForStreamedSpanEnvelope } from '../../../../utils/spanUtils'; +import { + collectStreamedSpans, + getSpanOp, + getSpansFromEnvelope, + waitForStreamedSpanEnvelope, +} from '../../../../utils/spanUtils'; sentryTest('runs `beforeSendSpan` for the streamed INP span', async ({ browserName, getLocalTestUrl, page }) => { const supportedBrowsers = ['chromium']; @@ -11,25 +17,26 @@ sentryTest('runs `beforeSendSpan` for the streamed INP span', async ({ browserNa sentryTest.skip(); } + const spans = collectStreamedSpans(page); const url = await getLocalTestUrl({ testDir: __dirname }); - const spanEnvelopePromise = waitForStreamedSpanEnvelope( - page, - env => !!getSpansFromEnvelope(env).find(s => getSpanOp(s) === 'ui.interaction.click'), + const spanEnvelopePromise = waitForStreamedSpanEnvelope(page, env => + getSpansFromEnvelope(env).some(span => getSpanOp(span) === UI_INTERACTION_CLICK), ); await page.goto(url); await page.locator('[data-test-id=normal-button]').click(); - await page.locator('.clicked[data-test-id=normal-button]').isVisible(); - - await page.waitForTimeout(500); + await expect(page.locator('.clicked[data-test-id=normal-button]')).toBeVisible(); // Page hide to trigger INP await hidePage(page); - const spanEnvelope = await spanEnvelopePromise; - const inpSpan = getSpansFromEnvelope(spanEnvelope).find(s => getSpanOp(s) === 'ui.interaction.click')!; + await spanEnvelopePromise; + await page.evaluate(() => (window as any).Sentry.flush()); + const inpSpans = spans.filter(span => getSpanOp(span) === UI_INTERACTION_CLICK); + expect(inpSpans).toHaveLength(1); + const [inpSpan] = inpSpans; // The callback rewrote the name and added a custom attribute. expect(inpSpan.name).toBe('scrubbed'); diff --git a/dev-packages/browser-integration-tests/suites/tracing/metrics/web-vitals-inp-late/test.ts b/dev-packages/browser-integration-tests/suites/tracing/metrics/web-vitals-inp-late/test.ts index a45e278385b5..63f6f5ca17a6 100644 --- a/dev-packages/browser-integration-tests/suites/tracing/metrics/web-vitals-inp-late/test.ts +++ b/dev-packages/browser-integration-tests/suites/tracing/metrics/web-vitals-inp-late/test.ts @@ -1,3 +1,4 @@ +import { UI_INTERACTION_CLICK } from '@sentry/conventions/op'; import { BROWSER_WEB_VITAL_INP_VALUE, SENTRY_IS_LOCALHOST, @@ -22,7 +23,12 @@ import { expect } from '@playwright/test'; import { SDK_VERSION } from '@sentry/core'; import { sentryTest } from '../../../../utils/fixtures'; import { hidePage, shouldSkipTracingTest } from '../../../../utils/helpers'; -import { getSpanOp, getSpansFromEnvelope, waitForStreamedSpanEnvelope } from '../../../../utils/spanUtils'; +import { + collectStreamedSpans, + getSpanOp, + getSpansFromEnvelope, + waitForStreamedSpanEnvelope, +} from '../../../../utils/spanUtils'; sentryTest( 'captures an INP click as a streamed span after pageload', @@ -33,19 +39,17 @@ sentryTest( sentryTest.skip(); } + const spans = collectStreamedSpans(page); const url = await getLocalTestUrl({ testDir: __dirname }); - const spanEnvelopePromise = waitForStreamedSpanEnvelope( - page, - env => !!getSpansFromEnvelope(env).find(s => getSpanOp(s) === 'ui.interaction.click'), + const spanEnvelopePromise = waitForStreamedSpanEnvelope(page, env => + getSpansFromEnvelope(env).some(span => getSpanOp(span) === UI_INTERACTION_CLICK), ); await page.goto(url); await page.locator('[data-test-id=normal-button]').click(); - await page.locator('.clicked[data-test-id=normal-button]').isVisible(); - - await page.waitForTimeout(500); + await expect(page.locator('.clicked[data-test-id=normal-button]')).toBeVisible(); // Page hide to trigger INP await hidePage(page); @@ -53,7 +57,10 @@ sentryTest( const spanEnvelope = await spanEnvelopePromise; const envelopeHeader = spanEnvelope[0]; const itemHeader = spanEnvelope[1][0][0]; - const inpSpan = getSpansFromEnvelope(spanEnvelope).find(s => getSpanOp(s) === 'ui.interaction.click')!; + await page.evaluate(() => (window as any).Sentry.flush()); + const inpSpans = spans.filter(span => getSpanOp(span) === UI_INTERACTION_CLICK); + expect(inpSpans).toHaveLength(1); + const [inpSpan] = inpSpans; const traceId = envelopeHeader.trace!.trace_id; expect(traceId).toMatch(/^[\da-f]{32}$/); @@ -95,7 +102,7 @@ sentryTest( attributes: { [SENTRY_IS_LOCALHOST]: { value: false, type: 'boolean' }, [SENTRY_ORIGIN]: { value: 'auto.http.browser.inp', type: 'string' }, - [SENTRY_OP]: { value: 'ui.interaction.click', type: 'string' }, + [SENTRY_OP]: { value: UI_INTERACTION_CLICK, type: 'string' }, [UI_COMPONENT_NAME]: { value: 'NormalButton', type: 'string' }, [BROWSER_WEB_VITAL_INP_TARGET]: { value: 'body > NormalButton', type: 'string' }, [SENTRY_EXCLUSIVE_TIME]: { value: inpValue, type: expect.stringMatching(/^(integer)|(double)$/) }, diff --git a/dev-packages/browser-integration-tests/suites/tracing/metrics/web-vitals-inp-navigate/test.ts b/dev-packages/browser-integration-tests/suites/tracing/metrics/web-vitals-inp-navigate/test.ts index 26b5b176d2cb..8cdafe4870c0 100644 --- a/dev-packages/browser-integration-tests/suites/tracing/metrics/web-vitals-inp-navigate/test.ts +++ b/dev-packages/browser-integration-tests/suites/tracing/metrics/web-vitals-inp-navigate/test.ts @@ -1,3 +1,4 @@ +import { UI_INTERACTION_CLICK } from '@sentry/conventions/op'; import { BROWSER_WEB_VITAL_INP_VALUE, SENTRY_IS_LOCALHOST, @@ -22,7 +23,12 @@ import { expect } from '@playwright/test'; import { SDK_VERSION } from '@sentry/core'; import { sentryTest } from '../../../../utils/fixtures'; import { hidePage, shouldSkipTracingTest } from '../../../../utils/helpers'; -import { getSpanOp, getSpansFromEnvelope, waitForStreamedSpanEnvelope } from '../../../../utils/spanUtils'; +import { + collectStreamedSpans, + getSpanOp, + getSpansFromEnvelope, + waitForStreamedSpanEnvelope, +} from '../../../../utils/spanUtils'; const supportedBrowsers = ['chromium']; @@ -33,20 +39,18 @@ sentryTest( sentryTest.skip(); } + const spans = collectStreamedSpans(page); const url = await getLocalTestUrl({ testDir: __dirname }); - const spanEnvelopePromise = waitForStreamedSpanEnvelope( - page, - env => !!getSpansFromEnvelope(env).find(s => getSpanOp(s) === 'ui.interaction.click'), + const spanEnvelopePromise = waitForStreamedSpanEnvelope(page, env => + getSpansFromEnvelope(env).some(span => getSpanOp(span) === UI_INTERACTION_CLICK), ); await page.goto(url); // Simulating route change (keeping