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..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,19 +1,19 @@ +import { SENTRY_ORIGIN } from '@sentry/conventions/attributes'; 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..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, @@ -17,7 +23,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 +31,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,26 +47,30 @@ 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: { - '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), }, @@ -66,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), }, @@ -130,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', @@ -167,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/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..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 @@ -1,37 +1,24 @@ 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 { + BROWSER_WEB_VITAL_FCP_VALUE, + BROWSER_WEB_VITAL_TTFB_VALUE, + 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..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,8 +1,13 @@ +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 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 +25,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..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,29 +1,31 @@ +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 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-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/request/fetch-relative-url-streamed/init.js b/dev-packages/browser-integration-tests/suites/tracing/metrics/pageload-resource-spans-static/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-static/init.js index 5ab240338c8c..bb6d4918d1bf 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-static/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/metrics/pageload-resource-spans-streamed/template.html b/dev-packages/browser-integration-tests/suites/tracing/metrics/pageload-resource-spans-static/template.html similarity index 100% rename from dev-packages/browser-integration-tests/suites/tracing/metrics/pageload-resource-spans-streamed/template.html rename to dev-packages/browser-integration-tests/suites/tracing/metrics/pageload-resource-spans-static/template.html 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..c15ef574991a --- /dev/null +++ b/dev-packages/browser-integration-tests/suites/tracing/metrics/pageload-resource-spans-static/test.ts @@ -0,0 +1,254 @@ +import { PAGELOAD, RESOURCE, RESOURCE_SCRIPT, RESOURCE_LINK, RESOURCE_IMG } from '@sentry/conventions/op'; +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'; +import { sentryTest } from '../../../../utils/fixtures'; +import { envelopeRequestParser, shouldSkipTracingTest, waitForTransactionRequest } 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 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 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/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/xhr-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/xhr-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/xhr-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..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,223 +1,291 @@ -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 { 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-inp-streamed-spans/init.js b/dev-packages/browser-integration-tests/suites/tracing/metrics/web-vitals-cls-static/init.js similarity index 72% rename from dev-packages/browser-integration-tests/suites/tracing/metrics/web-vitals-inp-streamed-spans/init.js rename to dev-packages/browser-integration-tests/suites/tracing/metrics/web-vitals-cls-static/init.js index 8be8d8420f85..38429010914d 100644 --- 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-cls-static/init.js @@ -1,15 +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: 5000, }), - Sentry.spanStreamingIntegration(), ], tracesSampleRate: 1, }); diff --git a/dev-packages/browser-integration-tests/suites/tracing/metrics/web-vitals-cls-static/subject.js b/dev-packages/browser-integration-tests/suites/tracing/metrics/web-vitals-cls-static/subject.js new file mode 100644 index 000000000000..b33aea72d7c9 --- /dev/null +++ b/dev-packages/browser-integration-tests/suites/tracing/metrics/web-vitals-cls-static/subject.js @@ -0,0 +1,9 @@ +import { simulateCLS } from '../../../../utils/web-vitals/cls.ts'; + +// Getting expected CLS parameter from URL hash +const expectedCLS = Number(location.hash.slice(1)); + +// 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); 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-static/template.html similarity index 84% rename from dev-packages/browser-integration-tests/suites/tracing/metrics/web-vitals-cls-streamed-spans/template.html rename to dev-packages/browser-integration-tests/suites/tracing/metrics/web-vitals-cls-static/template.html index 10e2e22f7d6a..72c22be8e49a 100644 --- 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-static/template.html @@ -5,6 +5,5 @@
-

Some content

diff --git a/dev-packages/browser-integration-tests/suites/tracing/metrics/web-vitals-cls-static/test.ts b/dev-packages/browser-integration-tests/suites/tracing/metrics/web-vitals-cls-static/test.ts new file mode 100644 index 000000000000..0ee5194fca6e --- /dev/null +++ b/dev-packages/browser-integration-tests/suites/tracing/metrics/web-vitals-cls-static/test.ts @@ -0,0 +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.beforeEach(async ({ browserName, page }) => { + if (shouldSkipTracingTest() || browserName !== 'chromium') { + sentryTest.skip(); + } + + await page.setViewportSize({ width: 800, height: 1200 }); +}); + +sentryTest('records CLS as a pageload measurement with its source', 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(); + + // 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); + + expect(eventData.contexts?.trace?.data?.['cls.source.1']).toBe('body > div#content > p#partial'); +}); 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/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..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,7 +1,17 @@ +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 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 +21,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/pageload-resource-spans-streamed/init.js b/dev-packages/browser-integration-tests/suites/tracing/metrics/web-vitals-fp-fcp-static/init.js similarity index 57% rename from dev-packages/browser-integration-tests/suites/tracing/metrics/pageload-resource-spans-streamed/init.js rename to dev-packages/browser-integration-tests/suites/tracing/metrics/web-vitals-fp-fcp-static/init.js index 4deba4c452dd..bb6d4918d1bf 100644 --- a/dev-packages/browser-integration-tests/suites/tracing/metrics/pageload-resource-spans-streamed/init.js +++ b/dev-packages/browser-integration-tests/suites/tracing/metrics/web-vitals-fp-fcp-static/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(), Sentry.spanStreamingIntegration()], - traceLifecycle: 'stream', + integrations: [Sentry.browserTracingIntegration()], tracesSampleRate: 1, }); diff --git a/dev-packages/browser-integration-tests/suites/tracing/metrics/web-vitals-fp-fcp-static/template.html b/dev-packages/browser-integration-tests/suites/tracing/metrics/web-vitals-fp-fcp-static/template.html new file mode 100644 index 000000000000..b81f11e967e3 --- /dev/null +++ b/dev-packages/browser-integration-tests/suites/tracing/metrics/web-vitals-fp-fcp-static/template.html @@ -0,0 +1,9 @@ + + + + + + +
Rendered
+ + diff --git a/dev-packages/browser-integration-tests/suites/tracing/metrics/web-vitals-fp-fcp-static/test.ts b/dev-packages/browser-integration-tests/suites/tracing/metrics/web-vitals-fp-fcp-static/test.ts new file mode 100644 index 000000000000..cd63b699cc06 --- /dev/null +++ b/dev-packages/browser-integration-tests/suites/tracing/metrics/web-vitals-fp-fcp-static/test.ts @@ -0,0 +1,41 @@ +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); +}); 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..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,41 +1,28 @@ +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 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', 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 url = await getLocalTestUrl({ testDir: __dirname }); + await page.goto(url); + const pageload = await pageloadPromise; + await page.evaluate(() => (window as any).Sentry.flush()); + + 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(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-static/init.js b/dev-packages/browser-integration-tests/suites/tracing/metrics/web-vitals-inp-before-send-span-static/init.js new file mode 100644 index 000000000000..5dd891ccc2f8 --- /dev/null +++ b/dev-packages/browser-integration-tests/suites/tracing/metrics/web-vitals-inp-before-send-span-static/init.js @@ -0,0 +1,39 @@ +import * as Sentry from '@sentry/browser'; + +window.Sentry = Sentry; + +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, + }), + ], + 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'; + } + + return span; + }), + 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-streamed-spans/subject.js b/dev-packages/browser-integration-tests/suites/tracing/metrics/web-vitals-inp-before-send-span-static/subject.js similarity index 70% rename from dev-packages/browser-integration-tests/suites/tracing/metrics/web-vitals-inp-streamed-spans/subject.js rename to dev-packages/browser-integration-tests/suites/tracing/metrics/web-vitals-inp-before-send-span-static/subject.js index f4ea1cd46d67..64524952dfa7 100644 --- 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-before-send-span-static/subject.js @@ -3,17 +3,18 @@ const blockUI = e => { const startTime = Date.now(); - function getElapsed() { + function getElasped() { const time = Date.now(); return time - startTime; } - while (getElapsed() < delay) { + while (getElasped() < 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-streamed-spans/template.html b/dev-packages/browser-integration-tests/suites/tracing/metrics/web-vitals-inp-before-send-span-static/template.html similarity index 65% rename from dev-packages/browser-integration-tests/suites/tracing/metrics/web-vitals-inp-streamed-spans/template.html rename to dev-packages/browser-integration-tests/suites/tracing/metrics/web-vitals-inp-before-send-span-static/template.html index d5f28c7c8847..437426e9ab01 100644 --- 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-before-send-span-static/template.html @@ -4,7 +4,9 @@ +
Rendered Before Long Task
+ diff --git a/dev-packages/browser-integration-tests/suites/tracing/metrics/web-vitals-inp-before-send-span-static/test.ts b/dev-packages/browser-integration-tests/suites/tracing/metrics/web-vitals-inp-before-send-span-static/test.ts new file mode 100644 index 000000000000..6368c5df996f --- /dev/null +++ b/dev-packages/browser-integration-tests/suites/tracing/metrics/web-vitals-inp-before-send-span-static/test.ts @@ -0,0 +1,48 @@ +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 { + collectStreamedSpans, + getSpanOp, + getSpansFromEnvelope, + waitForStreamedSpanEnvelope, +} from '../../../../utils/spanUtils'; + +sentryTest('runs a static `beforeSendSpan` for the INP span', async ({ browserName, getLocalTestUrl, page }) => { + const supportedBrowsers = ['chromium']; + + if (shouldSkipTracingTest() || !supportedBrowsers.includes(browserName)) { + sentryTest.skip(); + } + + const spans = collectStreamedSpans(page); + const url = await getLocalTestUrl({ testDir: __dirname }); + + 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 expect(page.locator('.clicked[data-test-id=normal-button]')).toBeVisible(); + + // Page hide to trigger INP + await hidePage(page); + + 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'); + 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; + expect(inpValue).toBeGreaterThan(0); +}); 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..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,44 +1,48 @@ +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'; - -// 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 }) => { +import { + collectStreamedSpans, + getSpanOp, + getSpansFromEnvelope, + waitForStreamedSpanEnvelope, +} from '../../../../utils/spanUtils'; + +sentryTest('runs `beforeSendSpan` for the streamed INP span', async ({ browserName, getLocalTestUrl, page }) => { const supportedBrowsers = ['chromium']; if (shouldSkipTracingTest() || !supportedBrowsers.includes(browserName)) { 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'); 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/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..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,8 +1,34 @@ +import { UI_INTERACTION_CLICK } from '@sentry/conventions/op'; +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'; 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', @@ -13,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); @@ -33,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}$/); @@ -54,17 +81,17 @@ sentryTest( expect(itemHeader).toEqual({ type: 'span', - item_count: 1, + item_count: getSpansFromEnvelope(spanEnvelope).length, 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; 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}$/), @@ -73,24 +100,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/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..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,8 +1,34 @@ +import { UI_INTERACTION_CLICK } from '@sentry/conventions/op'; +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'; import { hidePage, shouldSkipTracingTest } from '../../../../utils/helpers'; -import { getSpanOp, getSpansFromEnvelope, waitForStreamedSpanEnvelope } from '../../../../utils/spanUtils'; +import { + collectStreamedSpans, + getSpanOp, + getSpansFromEnvelope, + waitForStreamedSpanEnvelope, +} from '../../../../utils/spanUtils'; const supportedBrowsers = ['chromium']; @@ -13,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