diff --git a/dev-packages/browser-integration-tests/suites/tracing/metrics/web-vitals-cls-standalone-spans/init.js b/dev-packages/browser-integration-tests/suites/tracing/metrics/web-vitals-cls-standalone-spans/init.js deleted file mode 100644 index dce8cd2508fd..000000000000 --- a/dev-packages/browser-integration-tests/suites/tracing/metrics/web-vitals-cls-standalone-spans/init.js +++ /dev/null @@ -1,16 +0,0 @@ -import * as Sentry from '@sentry/browser'; - -window.Sentry = Sentry; - -Sentry.init({ - dsn: 'https://public@dsn.ingest.sentry.io/1337', - integrations: [ - Sentry.browserTracingIntegration({ - idleTimeout: 5000, - _experiments: { - enableStandaloneClsSpans: true, - }, - }), - ], - tracesSampleRate: 1, -}); diff --git a/dev-packages/browser-integration-tests/suites/tracing/metrics/web-vitals-cls-standalone-spans/subject.js b/dev-packages/browser-integration-tests/suites/tracing/metrics/web-vitals-cls-standalone-spans/subject.js deleted file mode 100644 index ed1b9b790bb9..000000000000 --- a/dev-packages/browser-integration-tests/suites/tracing/metrics/web-vitals-cls-standalone-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 cia 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-standalone-spans/template.html b/dev-packages/browser-integration-tests/suites/tracing/metrics/web-vitals-cls-standalone-spans/template.html deleted file mode 100644 index 10e2e22f7d6a..000000000000 --- a/dev-packages/browser-integration-tests/suites/tracing/metrics/web-vitals-cls-standalone-spans/template.html +++ /dev/null @@ -1,10 +0,0 @@ - - - - - - -
-

Some content

- - diff --git a/dev-packages/browser-integration-tests/suites/tracing/metrics/web-vitals-cls-standalone-spans/test.ts b/dev-packages/browser-integration-tests/suites/tracing/metrics/web-vitals-cls-standalone-spans/test.ts deleted file mode 100644 index fd4b3b8fa06b..000000000000 --- a/dev-packages/browser-integration-tests/suites/tracing/metrics/web-vitals-cls-standalone-spans/test.ts +++ /dev/null @@ -1,516 +0,0 @@ -import type { Page } from '@playwright/test'; -import { expect } from '@playwright/test'; -import type { Event as SentryEvent, EventEnvelope, SpanEnvelope } from '@sentry/core'; -import { sentryTest } from '../../../../utils/fixtures'; -import { - getFirstSentryEnvelopeRequest, - getMultipleSentryEnvelopeRequests, - properFullEnvelopeRequestParser, - shouldSkipTracingTest, -} from '../../../../utils/helpers'; - -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()); - }); - }); -} - -function triggerAndWaitForLayoutShift(page: Page): Promise { - return page.evaluate(() => { - window.dispatchEvent(new CustomEvent('trigger-cls')); - return new Promise(resolve => { - window.addEventListener('cls-done', () => resolve()); - }); - }); -} - -function hidePage(page: Page): Promise { - return page.evaluate(() => { - window.dispatchEvent(new Event('pagehide')); - }); -} - -sentryTest('captures a "GOOD" CLS vital with its source as a standalone span', async ({ getLocalTestUrl, page }) => { - const spanEnvelopePromise = getMultipleSentryEnvelopeRequests( - page, - 1, - { envelopeType: 'span' }, - properFullEnvelopeRequestParser, - ); - - const url = await getLocalTestUrl({ testDir: __dirname }); - await page.goto(`${url}#0.05`); - - await waitForLayoutShift(page); - - await hidePage(page); - - const spanEnvelope = (await spanEnvelopePromise)[0]; - - const spanEnvelopeHeaders = spanEnvelope[0]; - const spanEnvelopeItem = spanEnvelope[1][0][1]; - - expect(spanEnvelopeItem).toEqual({ - data: { - 'sentry.exclusive_time': 0, - 'sentry.op': 'ui.webvital.cls', - 'sentry.origin': 'auto.http.browser.cls', - 'sentry.report_event': 'pagehide', - transaction: expect.stringContaining('index.html'), - 'user_agent.original': expect.stringContaining('Chrome'), - 'sentry.pageload.span_id': expect.stringMatching(/[a-f\d]{16}/), - 'cls.source.1': expect.stringContaining('body > div#content > p'), - }, - description: expect.stringContaining('body > div#content > p'), - exclusive_time: 0, - measurements: { - cls: { - unit: '', - value: expect.any(Number), // better check below, - }, - }, - op: 'ui.webvital.cls', - origin: 'auto.http.browser.cls', - parent_span_id: expect.stringMatching(/[a-f\d]{16}/), - span_id: expect.stringMatching(/[a-f\d]{16}/), - segment_id: expect.stringMatching(/[a-f\d]{16}/), - start_timestamp: expect.any(Number), - timestamp: spanEnvelopeItem.start_timestamp, - trace_id: expect.stringMatching(/[a-f\d]{32}/), - }); - - // Flakey value dependent on timings -> we check for a range - expect(spanEnvelopeItem.measurements?.cls?.value).toBeGreaterThan(0.03); - expect(spanEnvelopeItem.measurements?.cls?.value).toBeLessThan(0.07); - - expect(spanEnvelopeHeaders).toEqual({ - sent_at: expect.any(String), - trace: { - environment: 'production', - public_key: 'public', - sample_rate: '1', - sampled: 'true', - trace_id: spanEnvelopeItem.trace_id, - sample_rand: expect.any(String), - // no transaction, because span source is URL - }, - }); -}); - -sentryTest('captures a "MEH" CLS vital with its source as a standalone span', async ({ getLocalTestUrl, page }) => { - const spanEnvelopePromise = getMultipleSentryEnvelopeRequests( - page, - 1, - { envelopeType: 'span' }, - properFullEnvelopeRequestParser, - ); - - const url = await getLocalTestUrl({ testDir: __dirname }); - await page.goto(`${url}#0.21`); - - await waitForLayoutShift(page); - - // Page hide to trigger CLS emission - await page.evaluate(() => { - window.dispatchEvent(new Event('pagehide')); - }); - - const spanEnvelope = (await spanEnvelopePromise)[0]; - - const spanEnvelopeHeaders = spanEnvelope[0]; - const spanEnvelopeItem = spanEnvelope[1][0][1]; - - expect(spanEnvelopeItem).toEqual({ - data: { - 'sentry.exclusive_time': 0, - 'sentry.op': 'ui.webvital.cls', - 'sentry.origin': 'auto.http.browser.cls', - 'sentry.report_event': 'pagehide', - transaction: expect.stringContaining('index.html'), - 'user_agent.original': expect.stringContaining('Chrome'), - 'sentry.pageload.span_id': expect.stringMatching(/[a-f\d]{16}/), - 'cls.source.1': expect.stringContaining('body > div#content > p'), - }, - description: expect.stringContaining('body > div#content > p'), - exclusive_time: 0, - measurements: { - cls: { - unit: '', - value: expect.any(Number), // better check below, - }, - }, - op: 'ui.webvital.cls', - origin: 'auto.http.browser.cls', - parent_span_id: expect.stringMatching(/[a-f\d]{16}/), - span_id: expect.stringMatching(/[a-f\d]{16}/), - segment_id: expect.stringMatching(/[a-f\d]{16}/), - start_timestamp: expect.any(Number), - timestamp: spanEnvelopeItem.start_timestamp, - trace_id: expect.stringMatching(/[a-f\d]{32}/), - }); - - // Flakey value dependent on timings -> we check for a range - expect(spanEnvelopeItem.measurements?.cls?.value).toBeGreaterThan(0.18); - expect(spanEnvelopeItem.measurements?.cls?.value).toBeLessThan(0.23); - - expect(spanEnvelopeHeaders).toEqual({ - sent_at: expect.any(String), - trace: { - environment: 'production', - public_key: 'public', - sample_rate: '1', - sampled: 'true', - trace_id: spanEnvelopeItem.trace_id, - sample_rand: expect.any(String), - // no transaction, because span source is URL - }, - }); -}); - -sentryTest('captures a "POOR" CLS vital with its source as a standalone span.', async ({ getLocalTestUrl, page }) => { - const spanEnvelopePromise = getMultipleSentryEnvelopeRequests( - page, - 1, - { envelopeType: 'span' }, - properFullEnvelopeRequestParser, - ); - - const url = await getLocalTestUrl({ testDir: __dirname }); - await page.goto(`${url}#0.35`); - - await waitForLayoutShift(page); - - // Page hide to trigger CLS emission - await hidePage(page); - - const spanEnvelope = (await spanEnvelopePromise)[0]; - - const spanEnvelopeHeaders = spanEnvelope[0]; - const spanEnvelopeItem = spanEnvelope[1][0][1]; - - expect(spanEnvelopeItem).toEqual({ - data: { - 'sentry.exclusive_time': 0, - 'sentry.op': 'ui.webvital.cls', - 'sentry.origin': 'auto.http.browser.cls', - 'sentry.report_event': 'pagehide', - transaction: expect.stringContaining('index.html'), - 'user_agent.original': expect.stringContaining('Chrome'), - 'sentry.pageload.span_id': expect.stringMatching(/[a-f\d]{16}/), - 'cls.source.1': expect.stringContaining('body > div#content > p'), - }, - description: expect.stringContaining('body > div#content > p'), - exclusive_time: 0, - measurements: { - cls: { - unit: '', - value: expect.any(Number), // better check below, - }, - }, - op: 'ui.webvital.cls', - origin: 'auto.http.browser.cls', - parent_span_id: expect.stringMatching(/[a-f\d]{16}/), - span_id: expect.stringMatching(/[a-f\d]{16}/), - segment_id: expect.stringMatching(/[a-f\d]{16}/), - start_timestamp: expect.any(Number), - timestamp: spanEnvelopeItem.start_timestamp, - trace_id: expect.stringMatching(/[a-f\d]{32}/), - }); - - // Flakey value dependent on timings -> we check for a range - expect(spanEnvelopeItem.measurements?.cls?.value).toBeGreaterThan(0.33); - expect(spanEnvelopeItem.measurements?.cls?.value).toBeLessThan(0.38); - - expect(spanEnvelopeHeaders).toEqual({ - sent_at: expect.any(String), - trace: { - environment: 'production', - public_key: 'public', - sample_rate: '1', - sampled: 'true', - trace_id: spanEnvelopeItem.trace_id, - sample_rand: expect.any(String), - // no transaction, because span source is URL - }, - }); -}); - -sentryTest( - 'captures a 0 CLS vital as a standalone span if no layout shift occurred', - async ({ getLocalTestUrl, page }) => { - const spanEnvelopePromise = getMultipleSentryEnvelopeRequests( - page, - 1, - { envelopeType: 'span' }, - properFullEnvelopeRequestParser, - ); - - const url = await getLocalTestUrl({ testDir: __dirname }); - await page.goto(url); - - await page.waitForTimeout(1000); - - await hidePage(page); - - const spanEnvelope = (await spanEnvelopePromise)[0]; - - const spanEnvelopeHeaders = spanEnvelope[0]; - const spanEnvelopeItem = spanEnvelope[1][0][1]; - - expect(spanEnvelopeItem).toEqual({ - data: { - 'sentry.exclusive_time': 0, - 'sentry.op': 'ui.webvital.cls', - 'sentry.origin': 'auto.http.browser.cls', - 'sentry.report_event': 'pagehide', - transaction: expect.stringContaining('index.html'), - 'user_agent.original': expect.stringContaining('Chrome'), - 'sentry.pageload.span_id': expect.stringMatching(/[a-f\d]{16}/), - }, - description: 'Layout shift', - exclusive_time: 0, - measurements: { - cls: { - unit: '', - value: 0, - }, - }, - op: 'ui.webvital.cls', - origin: 'auto.http.browser.cls', - parent_span_id: expect.stringMatching(/[a-f\d]{16}/), - span_id: expect.stringMatching(/[a-f\d]{16}/), - segment_id: expect.stringMatching(/[a-f\d]{16}/), - start_timestamp: expect.any(Number), - timestamp: spanEnvelopeItem.start_timestamp, - trace_id: expect.stringMatching(/[a-f\d]{32}/), - }); - - expect(spanEnvelopeHeaders).toEqual({ - sent_at: expect.any(String), - trace: { - environment: 'production', - public_key: 'public', - sample_rate: '1', - sampled: 'true', - trace_id: spanEnvelopeItem.trace_id, - sample_rand: expect.any(String), - // no transaction, because span source is URL - }, - }); - }, -); - -sentryTest( - 'captures CLS increases after the pageload span ended, when page is hidden', - async ({ getLocalTestUrl, page }) => { - const url = await getLocalTestUrl({ testDir: __dirname }); - - const eventData = await getFirstSentryEnvelopeRequest(page, url); - - expect(eventData.type).toBe('transaction'); - expect(eventData.contexts?.trace?.op).toBe('pageload'); - - const pageloadSpanId = eventData.contexts?.trace?.span_id; - const pageloadTraceId = eventData.contexts?.trace?.trace_id; - - expect(pageloadSpanId).toMatch(/[a-f\d]{16}/); - expect(pageloadTraceId).toMatch(/[a-f\d]{32}/); - - const spanEnvelopePromise = getMultipleSentryEnvelopeRequests( - page, - 1, - { envelopeType: 'span' }, - properFullEnvelopeRequestParser, - ); - - await triggerAndWaitForLayoutShift(page); - - await hidePage(page); - - const spanEnvelope = (await spanEnvelopePromise)[0]; - const spanEnvelopeItem = spanEnvelope[1][0][1]; - // Flakey value dependent on timings -> we check for a range - expect(spanEnvelopeItem.measurements?.cls?.value).toBeGreaterThan(0.05); - expect(spanEnvelopeItem.measurements?.cls?.value).toBeLessThan(0.15); - - // Ensure the CLS span is connected to the pageload span and trace - expect(spanEnvelopeItem.data?.['sentry.pageload.span_id']).toBe(pageloadSpanId); - expect(spanEnvelopeItem.trace_id).toEqual(pageloadTraceId); - - expect(spanEnvelopeItem.data?.['sentry.report_event']).toBe('pagehide'); - }, -); - -sentryTest('sends CLS of the initial page when soft-navigating to a new page', async ({ getLocalTestUrl, page }) => { - const url = await getLocalTestUrl({ testDir: __dirname }); - - const pageloadEventData = await getFirstSentryEnvelopeRequest(page, url); - - expect(pageloadEventData.type).toBe('transaction'); - expect(pageloadEventData.contexts?.trace?.op).toBe('pageload'); - - const spanEnvelopePromise = getMultipleSentryEnvelopeRequests( - page, - 1, - { envelopeType: 'span' }, - properFullEnvelopeRequestParser, - ); - - await triggerAndWaitForLayoutShift(page); - - await page.goto(`${url}#soft-navigation`); - - const pageloadTraceId = pageloadEventData.contexts?.trace?.trace_id; - expect(pageloadTraceId).toMatch(/[a-f\d]{32}/); - - const spanEnvelope = (await spanEnvelopePromise)[0]; - const spanEnvelopeItem = spanEnvelope[1][0][1]; - // Flakey value dependent on timings -> we check for a range - expect(spanEnvelopeItem.measurements?.cls?.value).toBeGreaterThan(0.05); - expect(spanEnvelopeItem.measurements?.cls?.value).toBeLessThan(0.15); - expect(spanEnvelopeItem.data?.['sentry.pageload.span_id']).toBe(pageloadEventData.contexts?.trace?.span_id); - expect(spanEnvelopeItem.trace_id).toEqual(pageloadTraceId); - - expect(spanEnvelopeItem.data?.['sentry.report_event']).toBe('navigation'); -}); - -sentryTest("doesn't send further CLS after the first navigation", async ({ getLocalTestUrl, page }) => { - const url = await getLocalTestUrl({ testDir: __dirname }); - - const eventData = await getFirstSentryEnvelopeRequest(page, url); - - expect(eventData.type).toBe('transaction'); - expect(eventData.contexts?.trace?.op).toBe('pageload'); - - const spanEnvelopePromise = getMultipleSentryEnvelopeRequests( - page, - 1, - { envelopeType: 'span' }, - properFullEnvelopeRequestParser, - ); - - await triggerAndWaitForLayoutShift(page); - - await page.goto(`${url}#soft-navigation`); - - const spanEnvelope = (await spanEnvelopePromise)[0]; - const spanEnvelopeItem = spanEnvelope[1][0][1]; - expect(spanEnvelopeItem.measurements?.cls?.value).toBeGreaterThan(0); - expect(spanEnvelopeItem.data?.['sentry.report_event']).toBe('navigation'); - - getMultipleSentryEnvelopeRequests(page, 1, { envelopeType: 'span' }, () => { - throw new Error('Unexpected span - This should not happen!'); - }); - - const navigationTxnPromise = getMultipleSentryEnvelopeRequests( - page, - 1, - { envelopeType: 'transaction' }, - properFullEnvelopeRequestParser, - ); - - // activate both CLS emission triggers: - await page.goto(`${url}#soft-navigation-2`); - await hidePage(page); - - // assumption: If we would send another CLS span on the 2nd navigation, it would be sent before the navigation - // transaction ends. This isn't 100% safe to ensure we don't send something but otherwise we'd need to wait for - // a timeout or something similar. - await navigationTxnPromise; -}); - -sentryTest("doesn't send further CLS after the first page hide", async ({ getLocalTestUrl, page }) => { - const url = await getLocalTestUrl({ testDir: __dirname }); - - const eventData = await getFirstSentryEnvelopeRequest(page, url); - - expect(eventData.type).toBe('transaction'); - expect(eventData.contexts?.trace?.op).toBe('pageload'); - - const spanEnvelopePromise = getMultipleSentryEnvelopeRequests( - page, - 1, - { envelopeType: 'span' }, - properFullEnvelopeRequestParser, - ); - - await triggerAndWaitForLayoutShift(page); - - await hidePage(page); - - const spanEnvelope = (await spanEnvelopePromise)[0]; - const spanEnvelopeItem = spanEnvelope[1][0][1]; - expect(spanEnvelopeItem.measurements?.cls?.value).toBeGreaterThan(0); - expect(spanEnvelopeItem.data?.['sentry.report_event']).toBe('pagehide'); - - getMultipleSentryEnvelopeRequests(page, 1, { envelopeType: 'span' }, () => { - throw new Error('Unexpected span - This should not happen!'); - }); - - const navigationTxnPromise = getMultipleSentryEnvelopeRequests( - page, - 1, - { envelopeType: 'transaction' }, - properFullEnvelopeRequestParser, - ); - - // activate both CLS emission triggers: - await page.goto(`${url}#soft-navigation-2`); - await hidePage(page); - - // assumption: If we would send another CLS span on the 2nd navigation, it would be sent before the navigation - // transaction ends. This isn't 100% safe to ensure we don't send something but otherwise we'd need to wait for - // a timeout or something similar. - await navigationTxnPromise; -}); - -sentryTest('CLS span timestamps are set correctly', async ({ getLocalTestUrl, page }) => { - const url = await getLocalTestUrl({ testDir: __dirname }); - - const eventData = await getFirstSentryEnvelopeRequest(page, url); - - expect(eventData.type).toBe('transaction'); - expect(eventData.contexts?.trace?.op).toBe('pageload'); - expect(eventData.timestamp).toBeDefined(); - - const pageloadEndTimestamp = eventData.timestamp!; - - const spanEnvelopePromise = getMultipleSentryEnvelopeRequests( - page, - 1, - { envelopeType: 'span' }, - properFullEnvelopeRequestParser, - ); - - await triggerAndWaitForLayoutShift(page); - - await hidePage(page); - - const spanEnvelope = (await spanEnvelopePromise)[0]; - const spanEnvelopeItem = spanEnvelope[1][0][1]; - - expect(spanEnvelopeItem.start_timestamp).toBeDefined(); - expect(spanEnvelopeItem.timestamp).toBeDefined(); - - const clsSpanStartTimestamp = spanEnvelopeItem.start_timestamp!; - const clsSpanEndTimestamp = spanEnvelopeItem.timestamp!; - - // CLS performance entries have no duration ==> start and end timestamp should be the same - expect(clsSpanStartTimestamp).toEqual(clsSpanEndTimestamp); - - // We don't really care that they are very close together but rather about the order of magnitude - // Previously, we had a bug where the timestamps would be significantly off (by multiple hours) - // so we only ensure that this bug is fixed. 60 seconds should be more than enough. - expect(clsSpanStartTimestamp - pageloadEndTimestamp).toBeLessThan(60); - expect(clsSpanStartTimestamp).toBeGreaterThan(pageloadEndTimestamp); -}); diff --git a/dev-packages/browser-integration-tests/suites/tracing/metrics/web-vitals-lcp-standalone-spans/assets/sentry-logo-600x179.png b/dev-packages/browser-integration-tests/suites/tracing/metrics/web-vitals-lcp-standalone-spans/assets/sentry-logo-600x179.png deleted file mode 100644 index 353b7233d6bf..000000000000 Binary files a/dev-packages/browser-integration-tests/suites/tracing/metrics/web-vitals-lcp-standalone-spans/assets/sentry-logo-600x179.png and /dev/null differ diff --git a/dev-packages/browser-integration-tests/suites/tracing/metrics/web-vitals-lcp-standalone-spans/init.js b/dev-packages/browser-integration-tests/suites/tracing/metrics/web-vitals-lcp-standalone-spans/init.js deleted file mode 100644 index d09eeab5f565..000000000000 --- a/dev-packages/browser-integration-tests/suites/tracing/metrics/web-vitals-lcp-standalone-spans/init.js +++ /dev/null @@ -1,17 +0,0 @@ -import * as Sentry from '@sentry/browser'; - -window.Sentry = Sentry; - -Sentry.init({ - dsn: 'https://public@dsn.ingest.sentry.io/1337', - integrations: [ - Sentry.browserTracingIntegration({ - idleTimeout: 5000, - _experiments: { - enableStandaloneLcpSpans: true, - }, - }), - ], - tracesSampleRate: 1, - debug: true, -}); diff --git a/dev-packages/browser-integration-tests/suites/tracing/metrics/web-vitals-lcp-standalone-spans/template.html b/dev-packages/browser-integration-tests/suites/tracing/metrics/web-vitals-lcp-standalone-spans/template.html deleted file mode 100644 index b613a556aca4..000000000000 --- a/dev-packages/browser-integration-tests/suites/tracing/metrics/web-vitals-lcp-standalone-spans/template.html +++ /dev/null @@ -1,10 +0,0 @@ - - - - - - -
- - - diff --git a/dev-packages/browser-integration-tests/suites/tracing/metrics/web-vitals-lcp-standalone-spans/test.ts b/dev-packages/browser-integration-tests/suites/tracing/metrics/web-vitals-lcp-standalone-spans/test.ts deleted file mode 100644 index e2b8a3e66e44..000000000000 --- a/dev-packages/browser-integration-tests/suites/tracing/metrics/web-vitals-lcp-standalone-spans/test.ts +++ /dev/null @@ -1,369 +0,0 @@ -import type { Page, Route } from '@playwright/test'; -import { expect } from '@playwright/test'; -import type { Event as SentryEvent, EventEnvelope, SpanEnvelope } from '@sentry/core'; -import { sentryTest } from '../../../../utils/fixtures'; -import { - envelopeRequestParser, - getFirstSentryEnvelopeRequest, - getMultipleSentryEnvelopeRequests, - properFullEnvelopeRequestParser, - shouldSkipTracingTest, - waitForTransactionRequest, -} from '../../../../utils/helpers'; - -sentryTest.beforeEach(async ({ browserName, page }) => { - if (shouldSkipTracingTest() || browserName !== 'chromium') { - sentryTest.skip(); - } - - await page.setViewportSize({ width: 800, height: 1200 }); -}); - -function hidePage(page: Page): Promise { - return page.evaluate(() => { - window.dispatchEvent(new Event('pagehide')); - }); -} - -sentryTest('captures LCP vital as a standalone span', async ({ getLocalTestUrl, page }) => { - const spanEnvelopePromise = getMultipleSentryEnvelopeRequests( - page, - 1, - { envelopeType: 'span' }, - properFullEnvelopeRequestParser, - ); - - const pageloadEnvelopePromise = waitForTransactionRequest(page, e => e.contexts?.trace?.op === 'pageload'); - - page.route('**', route => route.continue()); - page.route('**/my/image.png', async (route: Route) => { - return route.fulfill({ - path: `${__dirname}/assets/sentry-logo-600x179.png`, - }); - }); - - const url = await getLocalTestUrl({ testDir: __dirname }); - await page.goto(url); - - // Wait for LCP to be captured - await page.waitForTimeout(1000); - - await hidePage(page); - - const spanEnvelope = (await spanEnvelopePromise)[0]; - const pageloadTransactionEvent = envelopeRequestParser(await pageloadEnvelopePromise); - - const spanEnvelopeHeaders = spanEnvelope[0]; - const spanEnvelopeItem = spanEnvelope[1][0][1]; - - const pageloadTraceId = pageloadTransactionEvent.contexts?.trace?.trace_id; - expect(pageloadTraceId).toMatch(/[a-f\d]{32}/); - - expect(spanEnvelopeItem).toEqual({ - data: { - 'sentry.exclusive_time': 0, - 'sentry.op': 'ui.webvital.lcp', - 'sentry.origin': 'auto.http.browser.lcp', - 'sentry.report_event': 'pagehide', - transaction: expect.stringContaining('index.html'), - 'user_agent.original': expect.stringContaining('Chrome'), - 'sentry.pageload.span_id': expect.stringMatching(/[a-f\d]{16}/), - 'lcp.element': 'body > img', - 'lcp.loadTime': expect.any(Number), - 'lcp.renderTime': expect.any(Number), - 'lcp.size': expect.any(Number), - 'lcp.url': 'https://sentry-test-site.example/my/image.png', - }, - description: expect.stringContaining('body > img'), - exclusive_time: 0, - measurements: { - lcp: { - unit: 'millisecond', - value: expect.any(Number), - }, - }, - op: 'ui.webvital.lcp', - origin: 'auto.http.browser.lcp', - parent_span_id: expect.stringMatching(/[a-f\d]{16}/), - span_id: expect.stringMatching(/[a-f\d]{16}/), - segment_id: expect.stringMatching(/[a-f\d]{16}/), - start_timestamp: expect.any(Number), - timestamp: spanEnvelopeItem.start_timestamp, // LCP is a point-in-time metric - trace_id: pageloadTraceId, - }); - - // LCP value should be greater than 0 - expect(spanEnvelopeItem.measurements?.lcp?.value).toBeGreaterThan(0); - - expect(spanEnvelopeHeaders).toEqual({ - sent_at: expect.any(String), - trace: { - environment: 'production', - public_key: 'public', - sample_rate: '1', - sampled: 'true', - trace_id: spanEnvelopeItem.trace_id, - sample_rand: expect.any(String), - }, - }); -}); - -sentryTest('LCP span is linked to pageload transaction', async ({ getLocalTestUrl, page }) => { - page.route('**', route => route.continue()); - page.route('**/my/image.png', async (route: Route) => { - return route.fulfill({ - path: `${__dirname}/assets/sentry-logo-600x179.png`, - }); - }); - - const url = await getLocalTestUrl({ testDir: __dirname }); - - const eventData = await getFirstSentryEnvelopeRequest(page, url); - - expect(eventData.type).toBe('transaction'); - expect(eventData.contexts?.trace?.op).toBe('pageload'); - - const pageloadSpanId = eventData.contexts?.trace?.span_id; - const pageloadTraceId = eventData.contexts?.trace?.trace_id; - - expect(pageloadSpanId).toMatch(/[a-f\d]{16}/); - expect(pageloadTraceId).toMatch(/[a-f\d]{32}/); - - const spanEnvelopePromise = getMultipleSentryEnvelopeRequests( - page, - 1, - { envelopeType: 'span' }, - properFullEnvelopeRequestParser, - ); - - // Wait for LCP to be captured - await page.waitForTimeout(1000); - - await hidePage(page); - - const spanEnvelope = (await spanEnvelopePromise)[0]; - const spanEnvelopeItem = spanEnvelope[1][0][1]; - - // Ensure the LCP span is connected to the pageload span and trace - expect(spanEnvelopeItem.data?.['sentry.pageload.span_id']).toBe(pageloadSpanId); - expect(spanEnvelopeItem.trace_id).toEqual(pageloadTraceId); - expect(spanEnvelopeItem.measurements?.lcp?.value).toBeGreaterThan(0); -}); - -sentryTest('sends LCP of the initial page when soft-navigating to a new page', async ({ getLocalTestUrl, page }) => { - page.route('**', route => route.continue()); - page.route('**/my/image.png', async (route: Route) => { - return route.fulfill({ - path: `${__dirname}/assets/sentry-logo-600x179.png`, - }); - }); - - const url = await getLocalTestUrl({ testDir: __dirname }); - - const pageloadEventData = await getFirstSentryEnvelopeRequest(page, url); - - expect(pageloadEventData.type).toBe('transaction'); - expect(pageloadEventData.contexts?.trace?.op).toBe('pageload'); - - const spanEnvelopePromise = getMultipleSentryEnvelopeRequests( - page, - 1, - { envelopeType: 'span' }, - properFullEnvelopeRequestParser, - ); - - // Wait for LCP to be captured - await page.waitForTimeout(1000); - - await page.goto(`${url}#soft-navigation`); - - const spanEnvelope = (await spanEnvelopePromise)[0]; - const spanEnvelopeItem = spanEnvelope[1][0][1]; - - expect(spanEnvelopeItem.measurements?.lcp?.value).toBeGreaterThan(0); - expect(spanEnvelopeItem.data?.['sentry.pageload.span_id']).toBe(pageloadEventData.contexts?.trace?.span_id); - expect(spanEnvelopeItem.data?.['sentry.report_event']).toBe('navigation'); - expect(spanEnvelopeItem.trace_id).toBe(pageloadEventData.contexts?.trace?.trace_id); -}); - -sentryTest("doesn't send further LCP after the first navigation", async ({ getLocalTestUrl, page }) => { - page.route('**', route => route.continue()); - page.route('**/my/image.png', async (route: Route) => { - return route.fulfill({ - path: `${__dirname}/assets/sentry-logo-600x179.png`, - }); - }); - - const url = await getLocalTestUrl({ testDir: __dirname }); - - const pageloadEventData = await getFirstSentryEnvelopeRequest(page, url); - - expect(pageloadEventData.type).toBe('transaction'); - expect(pageloadEventData.contexts?.trace?.op).toBe('pageload'); - - const spanEnvelopePromise = getMultipleSentryEnvelopeRequests( - page, - 1, - { envelopeType: 'span' }, - properFullEnvelopeRequestParser, - ); - - // Wait for LCP to be captured - await page.waitForTimeout(1000); - - await page.goto(`${url}#soft-navigation`); - - const spanEnvelope = (await spanEnvelopePromise)[0]; - const spanEnvelopeItem = spanEnvelope[1][0][1]; - expect(spanEnvelopeItem.measurements?.lcp?.value).toBeGreaterThan(0); - expect(spanEnvelopeItem.data?.['sentry.report_event']).toBe('navigation'); - expect(spanEnvelopeItem.trace_id).toBe(pageloadEventData.contexts?.trace?.trace_id); - - getMultipleSentryEnvelopeRequests(page, 1, { envelopeType: 'span' }, () => { - throw new Error('Unexpected span - This should not happen!'); - }); - - const navigationTxnPromise = getMultipleSentryEnvelopeRequests( - page, - 1, - { envelopeType: 'transaction' }, - properFullEnvelopeRequestParser, - ); - - // activate both LCP emission triggers: - await page.goto(`${url}#soft-navigation-2`); - await hidePage(page); - - // assumption: If we would send another LCP span on the 2nd navigation, it would be sent before the navigation - // transaction ends. This isn't 100% safe to ensure we don't send something but otherwise we'd need to wait for - // a timeout or something similar. - await navigationTxnPromise; -}); - -sentryTest("doesn't send further LCP after the first page hide", async ({ getLocalTestUrl, page }) => { - page.route('**', route => route.continue()); - page.route('**/my/image.png', async (route: Route) => { - return route.fulfill({ - path: `${__dirname}/assets/sentry-logo-600x179.png`, - }); - }); - - const url = await getLocalTestUrl({ testDir: __dirname }); - - const pageloadEventData = await getFirstSentryEnvelopeRequest(page, url); - - expect(pageloadEventData.type).toBe('transaction'); - expect(pageloadEventData.contexts?.trace?.op).toBe('pageload'); - - const spanEnvelopePromise = getMultipleSentryEnvelopeRequests( - page, - 1, - { envelopeType: 'span' }, - properFullEnvelopeRequestParser, - ); - - // Wait for LCP to be captured - await page.waitForTimeout(1000); - - await hidePage(page); - - const spanEnvelope = (await spanEnvelopePromise)[0]; - const spanEnvelopeItem = spanEnvelope[1][0][1]; - expect(spanEnvelopeItem.measurements?.lcp?.value).toBeGreaterThan(0); - expect(spanEnvelopeItem.data?.['sentry.report_event']).toBe('pagehide'); - expect(spanEnvelopeItem.trace_id).toBe(pageloadEventData.contexts?.trace?.trace_id); - - getMultipleSentryEnvelopeRequests(page, 1, { envelopeType: 'span' }, () => { - throw new Error('Unexpected span - This should not happen!'); - }); - - const navigationTxnPromise = getMultipleSentryEnvelopeRequests( - page, - 1, - { envelopeType: 'transaction' }, - properFullEnvelopeRequestParser, - ); - - // activate both LCP emission triggers: - await page.goto(`${url}#soft-navigation-2`); - await hidePage(page); - - // assumption: If we would send another LCP span on the 2nd navigation, it would be sent before the navigation - // transaction ends. This isn't 100% safe to ensure we don't send something but otherwise we'd need to wait for - // a timeout or something similar. - await navigationTxnPromise; -}); - -sentryTest('LCP span timestamps are set correctly', async ({ getLocalTestUrl, page }) => { - page.route('**', route => route.continue()); - page.route('**/my/image.png', async (route: Route) => { - return route.fulfill({ - path: `${__dirname}/assets/sentry-logo-600x179.png`, - }); - }); - - const url = await getLocalTestUrl({ testDir: __dirname }); - - const eventData = await getFirstSentryEnvelopeRequest(page, url); - - expect(eventData.type).toBe('transaction'); - expect(eventData.contexts?.trace?.op).toBe('pageload'); - expect(eventData.timestamp).toBeDefined(); - - const pageloadEndTimestamp = eventData.timestamp!; - - const spanEnvelopePromise = getMultipleSentryEnvelopeRequests( - page, - 1, - { envelopeType: 'span' }, - properFullEnvelopeRequestParser, - ); - - // Wait for LCP to be captured - await page.waitForTimeout(1000); - - await hidePage(page); - - const spanEnvelope = (await spanEnvelopePromise)[0]; - const spanEnvelopeItem = spanEnvelope[1][0][1]; - - expect(spanEnvelopeItem.start_timestamp).toBeDefined(); - expect(spanEnvelopeItem.timestamp).toBeDefined(); - - const lcpSpanStartTimestamp = spanEnvelopeItem.start_timestamp!; - const lcpSpanEndTimestamp = spanEnvelopeItem.timestamp!; - - // LCP is a point-in-time metric ==> start and end timestamp should be the same - expect(lcpSpanStartTimestamp).toEqual(lcpSpanEndTimestamp); - - // We don't really care that they are very close together but rather about the order of magnitude - // Previously, we had a bug where the timestamps would be significantly off (by multiple hours) - // so we only ensure that this bug is fixed. 60 seconds should be more than enough. - expect(lcpSpanStartTimestamp - pageloadEndTimestamp).toBeLessThan(60); -}); - -sentryTest( - 'pageload transaction does not contain LCP measurement when standalone spans are enabled', - async ({ getLocalTestUrl, page }) => { - page.route('**', route => route.continue()); - page.route('**/my/image.png', async (route: Route) => { - return route.fulfill({ - path: `${__dirname}/assets/sentry-logo-600x179.png`, - }); - }); - - const url = await getLocalTestUrl({ testDir: __dirname }); - const eventData = await getFirstSentryEnvelopeRequest(page, url); - - expect(eventData.type).toBe('transaction'); - expect(eventData.contexts?.trace?.op).toBe('pageload'); - - // LCP measurement should NOT be present on the pageload transaction when standalone spans are enabled - expect(eventData.measurements?.lcp).toBeUndefined(); - - // LCP attributes should also NOT be present on the pageload transaction when standalone spans are enabled - // because the LCP data is sent as a standalone span instead - expect(eventData.contexts?.trace?.data?.['lcp.element']).toBeUndefined(); - expect(eventData.contexts?.trace?.data?.['lcp.size']).toBeUndefined(); - }, -); diff --git a/packages/browser/src/integrations/webVitals.ts b/packages/browser/src/integrations/webVitals.ts index b7b325f81f8f..8f4d98affb13 100644 --- a/packages/browser/src/integrations/webVitals.ts +++ b/packages/browser/src/integrations/webVitals.ts @@ -18,14 +18,6 @@ export interface WebVitalsOptions { * Web vitals to skip. */ disable?: WebVitalName[]; - - /** - * @experimental - */ - _experiments?: Partial<{ - enableStandaloneClsSpans: boolean; - enableStandaloneLcpSpans: boolean; - }>; } const collectWebVitalsCallbacks = new WeakMap void>(); @@ -48,15 +40,12 @@ export const webVitalsIntegration = defineIntegration((options: WebVitalsOptions name: WEB_VITALS_INTEGRATION_NAME, setup(client) { const spanStreamingEnabled = hasSpanStreamingEnabled(client); - const { enableStandaloneClsSpans, enableStandaloneLcpSpans } = options._experiments ?? {}; collectWebVitalsCallbacks.set( client, startTrackingWebVitals({ - recordClsStandaloneSpans: - spanStreamingEnabled || disabled.has('cls') ? undefined : enableStandaloneClsSpans || false, - recordLcpStandaloneSpans: - spanStreamingEnabled || disabled.has('lcp') ? undefined : enableStandaloneLcpSpans || false, + recordClsStandaloneSpans: spanStreamingEnabled || disabled.has('cls') ? undefined : false, + recordLcpStandaloneSpans: spanStreamingEnabled || disabled.has('lcp') ? undefined : false, client, }), ); diff --git a/packages/browser/src/tracing/browserTracingIntegration.ts b/packages/browser/src/tracing/browserTracingIntegration.ts index 1f6094a37fdc..debf2bac6626 100644 --- a/packages/browser/src/tracing/browserTracingIntegration.ts +++ b/packages/browser/src/tracing/browserTracingIntegration.ts @@ -303,8 +303,6 @@ export interface BrowserTracingOptions { */ _experiments: Partial<{ enableInteractions: boolean; - enableStandaloneClsSpans: boolean; - enableStandaloneLcpSpans: boolean; }>; /** @@ -386,7 +384,7 @@ export const browserTracingIntegration = ((options: Partial { expect(mockTrackInpAsSpan).not.toHaveBeenCalled(); }); - it('keeps standalone LCP and CLS experiments working', () => { - const client = { getOptions: () => ({}) }; - const integration = webVitalsIntegration({ - _experiments: { - enableStandaloneClsSpans: true, - enableStandaloneLcpSpans: true, - }, - }); - - integration.setup?.(client as never); - - expect(mockStartTrackingWebVitals).toHaveBeenCalledWith({ - recordClsStandaloneSpans: true, - recordLcpStandaloneSpans: true, - client, - }); - }); - it('tracks LCP, CLS and INP as streamed spans when span streaming is enabled', () => { const client = { getOptions: () => ({ traceLifecycle: 'stream' }) }; const integration = webVitalsIntegration();