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-cls-streamed-spans/init.js b/dev-packages/browser-integration-tests/suites/tracing/metrics/web-vitals-cls-streamed-spans/init.js deleted file mode 100644 index bd3b6ed17872..000000000000 --- a/dev-packages/browser-integration-tests/suites/tracing/metrics/web-vitals-cls-streamed-spans/init.js +++ /dev/null @@ -1,11 +0,0 @@ -import * as Sentry from '@sentry/browser'; - -window.Sentry = Sentry; -window._testBaseTimestamp = performance.timeOrigin / 1000; - -Sentry.init({ - dsn: 'https://public@dsn.ingest.sentry.io/1337', - integrations: [Sentry.browserTracingIntegration(), Sentry.spanStreamingIntegration()], - traceLifecycle: 'stream', - tracesSampleRate: 1, -}); diff --git a/dev-packages/browser-integration-tests/suites/tracing/metrics/web-vitals-cls-streamed-spans/subject.js b/dev-packages/browser-integration-tests/suites/tracing/metrics/web-vitals-cls-streamed-spans/subject.js deleted file mode 100644 index 9742a4a5cc29..000000000000 --- a/dev-packages/browser-integration-tests/suites/tracing/metrics/web-vitals-cls-streamed-spans/subject.js +++ /dev/null @@ -1,17 +0,0 @@ -import { simulateCLS } from '../../../../utils/web-vitals/cls.ts'; - -// Simulate Layout shift right at the beginning of the page load, depending on the URL hash -// don't run if expected CLS is NaN -const expectedCLS = Number(location.hash.slice(1)); -if (expectedCLS && expectedCLS >= 0) { - simulateCLS(expectedCLS).then(() => window.dispatchEvent(new Event('cls-done'))); -} - -// Simulate layout shift whenever the trigger-cls event is dispatched -// Cannot trigger via a button click because expected layout shift after -// an interaction doesn't contribute to CLS. -window.addEventListener('trigger-cls', () => { - simulateCLS(0.1).then(() => { - window.dispatchEvent(new Event('cls-done')); - }); -}); diff --git a/dev-packages/browser-integration-tests/suites/tracing/metrics/web-vitals-cls-streamed-spans/template.html b/dev-packages/browser-integration-tests/suites/tracing/metrics/web-vitals-cls-streamed-spans/template.html deleted file mode 100644 index 10e2e22f7d6a..000000000000 --- a/dev-packages/browser-integration-tests/suites/tracing/metrics/web-vitals-cls-streamed-spans/template.html +++ /dev/null @@ -1,10 +0,0 @@ - - - - - - -
-

Some content

- - diff --git a/dev-packages/browser-integration-tests/suites/tracing/metrics/web-vitals-cls-streamed-spans/test.ts b/dev-packages/browser-integration-tests/suites/tracing/metrics/web-vitals-cls-streamed-spans/test.ts deleted file mode 100644 index 409d79327a91..000000000000 --- a/dev-packages/browser-integration-tests/suites/tracing/metrics/web-vitals-cls-streamed-spans/test.ts +++ /dev/null @@ -1,76 +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 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-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/dev-packages/browser-integration-tests/suites/tracing/metrics/web-vitals-lcp-streamed-spans/assets/sentry-logo-600x179.png b/dev-packages/browser-integration-tests/suites/tracing/metrics/web-vitals-lcp-streamed-spans/assets/sentry-logo-600x179.png deleted file mode 100644 index 353b7233d6bf..000000000000 Binary files a/dev-packages/browser-integration-tests/suites/tracing/metrics/web-vitals-lcp-streamed-spans/assets/sentry-logo-600x179.png and /dev/null differ diff --git a/dev-packages/browser-integration-tests/suites/tracing/metrics/web-vitals-lcp-streamed-spans/init.js b/dev-packages/browser-integration-tests/suites/tracing/metrics/web-vitals-lcp-streamed-spans/init.js deleted file mode 100644 index bd3b6ed17872..000000000000 --- a/dev-packages/browser-integration-tests/suites/tracing/metrics/web-vitals-lcp-streamed-spans/init.js +++ /dev/null @@ -1,11 +0,0 @@ -import * as Sentry from '@sentry/browser'; - -window.Sentry = Sentry; -window._testBaseTimestamp = performance.timeOrigin / 1000; - -Sentry.init({ - dsn: 'https://public@dsn.ingest.sentry.io/1337', - integrations: [Sentry.browserTracingIntegration(), Sentry.spanStreamingIntegration()], - traceLifecycle: 'stream', - tracesSampleRate: 1, -}); diff --git a/dev-packages/browser-integration-tests/suites/tracing/metrics/web-vitals-lcp-streamed-spans/template.html b/dev-packages/browser-integration-tests/suites/tracing/metrics/web-vitals-lcp-streamed-spans/template.html deleted file mode 100644 index b613a556aca4..000000000000 --- a/dev-packages/browser-integration-tests/suites/tracing/metrics/web-vitals-lcp-streamed-spans/template.html +++ /dev/null @@ -1,10 +0,0 @@ - - - - - - -
- - - diff --git a/dev-packages/browser-integration-tests/suites/tracing/metrics/web-vitals-lcp-streamed-spans/test.ts b/dev-packages/browser-integration-tests/suites/tracing/metrics/web-vitals-lcp-streamed-spans/test.ts deleted file mode 100644 index 8cff98edfcd0..000000000000 --- a/dev-packages/browser-integration-tests/suites/tracing/metrics/web-vitals-lcp-streamed-spans/test.ts +++ /dev/null @@ -1,65 +0,0 @@ -import type { Route } 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 }); -}); - -sentryTest('captures LCP as a streamed span with element attributes', async ({ getLocalTestUrl, page }) => { - page.route('**', route => route.continue()); - page.route('**/my/image.png', async (route: Route) => { - return route.fulfill({ - path: `${__dirname}/assets/sentry-logo-600x179.png`, - }); - }); - - const url = await getLocalTestUrl({ testDir: __dirname }); - - const lcpSpanPromise = waitForStreamedSpan(page, span => getSpanOp(span) === 'ui.webvital.lcp'); - const pageloadSpanPromise = waitForStreamedSpan(page, span => getSpanOp(span) === 'pageload'); - - await page.goto(url); - - // Wait for LCP to be captured - await page.waitForTimeout(1000); - - await hidePage(page); - - const lcpSpan = await lcpSpanPromise; - const pageloadSpan = await pageloadSpanPromise; - - expect(lcpSpan.attributes?.['sentry.op']).toEqual({ type: 'string', value: 'ui.webvital.lcp' }); - expect(lcpSpan.attributes?.['sentry.origin']).toEqual({ type: 'string', value: 'auto.http.browser.lcp' }); - expect(lcpSpan.attributes?.['sentry.exclusive_time']).toEqual({ type: 'integer', value: 0 }); - expect(lcpSpan.attributes?.['user_agent.original']?.value).toEqual(expect.stringContaining('Chrome')); - - // Check browser.web_vital.lcp.* attributes - expect(lcpSpan.attributes?.['browser.web_vital.lcp.element']?.value).toEqual(expect.stringContaining('body > img')); - expect(lcpSpan.attributes?.['browser.web_vital.lcp.url']?.value).toBe( - 'https://sentry-test-site.example/my/image.png', - ); - expect(lcpSpan.attributes?.['browser.web_vital.lcp.size']?.value).toEqual(expect.any(Number)); - - // Check web vital value attribute - expect(lcpSpan.attributes?.['browser.web_vital.lcp.value']?.type).toMatch(/^(double)|(integer)$/); - expect(lcpSpan.attributes?.['browser.web_vital.lcp.value']?.value).toBeGreaterThan(0); - - // Check pageload span id is present - expect(lcpSpan.attributes?.['sentry.pageload.span_id']?.value).toBe(pageloadSpan.span_id); - - // Span should have meaningful duration (navigation start -> LCP event) - expect(lcpSpan.end_timestamp).toBeGreaterThan(lcpSpan.start_timestamp); - - expect(lcpSpan.span_id).toMatch(/^[\da-f]{16}$/); - expect(lcpSpan.trace_id).toMatch(/^[\da-f]{32}$/); - - expect(lcpSpan.parent_span_id).toBe(pageloadSpan.span_id); - expect(lcpSpan.trace_id).toBe(pageloadSpan.trace_id); -}); diff --git a/packages/browser-utils/src/index.ts b/packages/browser-utils/src/index.ts index fa2752fe38de..f675ae52952a 100644 --- a/packages/browser-utils/src/index.ts +++ b/packages/browser-utils/src/index.ts @@ -21,7 +21,7 @@ export { elementTimingIntegration, startTrackingElementTiming } from './metrics/ export { extractNetworkProtocol } from './metrics/utils'; -export { trackClsAsSpan, trackInpAsSpan, trackLcpAsSpan } from './metrics/webVitalSpans'; +export { trackInpAsSpan } from './metrics/webVitalSpans'; export { addClickKeypressInstrumentationHandler } from './instrument/dom'; diff --git a/packages/browser-utils/src/metrics/browserMetrics.ts b/packages/browser-utils/src/metrics/browserMetrics.ts index db6fc4aa6e37..54c92f927995 100644 --- a/packages/browser-utils/src/metrics/browserMetrics.ts +++ b/packages/browser-utils/src/metrics/browserMetrics.ts @@ -14,7 +14,6 @@ import { } from '@sentry/core'; import { htmlTreeAsString } from '../htmlTreeAsString'; import { WINDOW } from '../types'; -import { trackClsAsStandaloneSpan } from './cls'; import { addClsInstrumentationHandler, addLcpInstrumentationHandler, @@ -22,7 +21,7 @@ import { addTtfbInstrumentationHandler, type PerformanceLongAnimationFrameTiming, } from './instrument'; -import { isValidLcpMetric, trackLcpAsStandaloneSpan } from './lcp'; +import { isValidLcpMetric } from './lcp'; import { resourceTimingToSpanAttributes } from './resourceTiming'; import { getBrowserPerformanceAPI, isMeasurementValue, msToSec, startAndEndSpan } from './utils'; import { getActivationStart } from './web-vitals/lib/getActivationStart'; @@ -76,18 +75,8 @@ let _lcpEntry: LargestContentfulPaint | undefined; let _clsEntry: LayoutShift | undefined; interface StartTrackingWebVitalsOptions { - /** - * When `true`, CLS is tracked as a standalone span. When `false`, CLS is - * recorded as a measurement on the pageload span. When `undefined`, CLS - * tracking is skipped entirely (e.g. because span streaming handles it). - */ - recordClsStandaloneSpans: boolean | undefined; - /** - * When `true`, LCP is tracked as a standalone span. When `false`, LCP is - * recorded as a measurement on the pageload span. When `undefined`, LCP - * tracking is skipped entirely (e.g. because span streaming handles it). - */ - recordLcpStandaloneSpans: boolean | undefined; + trackCls: boolean; + trackLcp: boolean; client: Client; } @@ -95,14 +84,9 @@ interface StartTrackingWebVitalsOptions { * Start tracking web vitals. * The callback returned by this function can be used to stop tracking & ensure all measurements are final & captured. * - * @deprecated this function will be removed and streamlined once we stop supporting standalone v1 * @returns A function that forces web vitals collection */ -export function startTrackingWebVitals({ - recordClsStandaloneSpans, - recordLcpStandaloneSpans, - client, -}: StartTrackingWebVitalsOptions): () => void { +export function startTrackingWebVitals({ trackCls, trackLcp }: StartTrackingWebVitalsOptions): () => void { const performance = getBrowserPerformanceAPI(); if (performance && browserPerformanceTimeOrigin()) { // @ts-expect-error we want to make sure all of these are available, even if TS is sure they are @@ -110,18 +94,8 @@ export function startTrackingWebVitals({ WINDOW.performance.mark('sentry-tracing-init'); } - const lcpCleanupCallback = recordLcpStandaloneSpans - ? trackLcpAsStandaloneSpan(client) - : recordLcpStandaloneSpans === false - ? _trackLCP() - : undefined; - - const clsCleanupCallback = recordClsStandaloneSpans - ? trackClsAsStandaloneSpan(client) - : recordClsStandaloneSpans === false - ? _trackCLS() - : undefined; - + const lcpCleanupCallback = trackLcp ? _trackLCP() : undefined; + const clsCleanupCallback = trackCls ? _trackCLS() : undefined; const ttfbCleanupCallback = _trackTtfb(); const fpFcpCleanupCallback = _trackFpFcp(); @@ -463,8 +437,8 @@ export function addWebVitalsToSpan(span: Span, options: AddWebVitalsToSpanOption DEBUG_BUILD && debug.log('Setting web vital attribute', { [attrKey]: value }, 'on pageload span'); }; // for streamed pageload spans, we add the web vital measurements as attributes. - // We omit LCP, CLS and INP because they're tracked separately as spans - ['ttfb', 'fp', 'fcp'].forEach(measurementName => { + // INP is tracked separately as a span. + ['ttfb', 'fp', 'fcp', 'cls', 'lcp'].forEach(measurementName => { if (_measurements[measurementName]) { setAttr(measurementName, _measurements[measurementName].value); } @@ -472,15 +446,13 @@ export function addWebVitalsToSpan(span: Span, options: AddWebVitalsToSpanOption if (_measurements['ttfb.requestTime']) { setAttr('ttfb.requestTime', _measurements['ttfb.requestTime'].value, 'browser.web_vital.ttfb.request_time'); } - } else { - // TODO (V11): Remove this else branch once we remove v1 standalone spans and transactions - // If CLS standalone spans are enabled, don't record CLS as a measurement + _setStreamedWebVitalAttributes(span); + } else { if (!recordClsOnPageloadSpan) { delete _measurements.cls; } - // If LCP standalone spans are enabled, don't record LCP as a measurement if (!recordLcpOnPageloadSpan) { delete _measurements.lcp; } @@ -895,6 +867,23 @@ function _setWebVitalAttributes(span: Span, options: AddWebVitalsToSpanOptions): } } +function _setStreamedWebVitalAttributes(span: Span): void { + if (_lcpEntry) { + _lcpEntry.element && span.setAttribute('browser.web_vital.lcp.element', htmlTreeAsString(_lcpEntry.element)); + _lcpEntry.id && span.setAttribute('browser.web_vital.lcp.id', _lcpEntry.id); + _lcpEntry.url && span.setAttribute('browser.web_vital.lcp.url', _lcpEntry.url.trim().slice(0, 200)); + _lcpEntry.loadTime != null && span.setAttribute('browser.web_vital.lcp.load_time', _lcpEntry.loadTime); + _lcpEntry.renderTime != null && span.setAttribute('browser.web_vital.lcp.render_time', _lcpEntry.renderTime); + _lcpEntry.size != null && span.setAttribute('browser.web_vital.lcp.size', _lcpEntry.size); + } + + if (_clsEntry?.sources) { + _clsEntry.sources.forEach((source, index) => + span.setAttribute(`browser.web_vital.cls.source.${index + 1}`, htmlTreeAsString(source.node)), + ); + } +} + type ExperimentalResourceTimingProperty = | 'renderBlockingStatus' | 'deliveryType' diff --git a/packages/browser-utils/src/metrics/cls.ts b/packages/browser-utils/src/metrics/cls.ts index 4c09dde19c74..cb0ff5c3b541 100644 --- a/packages/browser-utils/src/metrics/cls.ts +++ b/packages/browser-utils/src/metrics/cls.ts @@ -1,102 +1 @@ -import type { Client, SpanAttributes } from '@sentry/core'; -import { - browserPerformanceTimeOrigin, - debug, - getCurrentScope, - SEMANTIC_ATTRIBUTE_EXCLUSIVE_TIME, - SEMANTIC_ATTRIBUTE_SENTRY_MEASUREMENT_UNIT, - SEMANTIC_ATTRIBUTE_SENTRY_MEASUREMENT_VALUE, - SEMANTIC_ATTRIBUTE_SENTRY_OP, - SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN, - timestampInSeconds, -} from '@sentry/core'; -import { DEBUG_BUILD } from '../debug-build'; -import { htmlTreeAsString } from '../htmlTreeAsString'; -import { addClsInstrumentationHandler } from './instrument'; -import type { WebVitalReportEvent } from './utils'; -import { listenForWebVitalReportEvents, msToSec, startStandaloneWebVitalSpan, supportsWebVital } from './utils'; - -/** - * Starts tracking the Cumulative Layout Shift on the current page and collects the value once - * - * - the page visibility is hidden - * - a navigation span is started (to stop CLS measurement for SPA soft navigations) - * - * Once either of these events triggers, the CLS value is sent as a standalone span and we stop - * measuring CLS. - */ -export function trackClsAsStandaloneSpan(client: Client): void { - let standaloneCLsValue = 0; - let standaloneClsEntry: LayoutShift | undefined; - - if (!supportsWebVital('layout-shift')) { - return; - } - - const cleanupClsHandler = addClsInstrumentationHandler(({ metric }) => { - const entry = metric.entries[metric.entries.length - 1] as LayoutShift | undefined; - if (!entry) { - return; - } - standaloneCLsValue = metric.value; - standaloneClsEntry = entry; - }, true); - - listenForWebVitalReportEvents(client, (reportEvent, pageloadSpanId) => { - _sendStandaloneClsSpan(standaloneCLsValue, standaloneClsEntry, pageloadSpanId, reportEvent); - cleanupClsHandler(); - }); -} - -/** - * Exported only for testing! - */ -export function _sendStandaloneClsSpan( - clsValue: number, - entry: LayoutShift | undefined, - pageloadSpanId: string, - reportEvent: WebVitalReportEvent, -) { - DEBUG_BUILD && debug.log(`Sending CLS span (${clsValue})`); - - const startTime = entry ? msToSec((browserPerformanceTimeOrigin() || 0) + entry.startTime) : timestampInSeconds(); - const routeName = getCurrentScope().getScopeData().transactionName; - - const name = entry ? htmlTreeAsString(entry.sources[0]?.node) : 'Layout shift'; - - const attributes: SpanAttributes = { - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.http.browser.cls', - [SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'ui.webvital.cls', - [SEMANTIC_ATTRIBUTE_EXCLUSIVE_TIME]: 0, - // attach the pageload span id to the CLS span so that we can link them in the UI - 'sentry.pageload.span_id': pageloadSpanId, - // describes what triggered the web vital to be reported - 'sentry.report_event': reportEvent, - }; - - // Add CLS sources as span attributes to help with debugging layout shifts - // See: https://developer.mozilla.org/en-US/docs/Web/API/LayoutShift/sources - if (entry?.sources) { - entry.sources.forEach((source, index) => { - attributes[`cls.source.${index + 1}`] = htmlTreeAsString(source.node); - }); - } - - const span = startStandaloneWebVitalSpan({ - name, - transaction: routeName, - attributes, - startTime, - }); - - if (span) { - span.addEvent('cls', { - [SEMANTIC_ATTRIBUTE_SENTRY_MEASUREMENT_UNIT]: '', - [SEMANTIC_ATTRIBUTE_SENTRY_MEASUREMENT_VALUE]: clsValue, - }); - - // LayoutShift performance entries always have a duration of 0, so we don't need to add `entry.duration` here - // see: https://developer.mozilla.org/en-US/docs/Web/API/PerformanceEntry/duration - span.end(startTime); - } -} +export {}; diff --git a/packages/browser-utils/src/metrics/lcp.ts b/packages/browser-utils/src/metrics/lcp.ts index bcd065e94cf0..756a53670c29 100644 --- a/packages/browser-utils/src/metrics/lcp.ts +++ b/packages/browser-utils/src/metrics/lcp.ts @@ -1,20 +1,3 @@ -import type { Client, SpanAttributes } from '@sentry/core'; -import { - browserPerformanceTimeOrigin, - debug, - getCurrentScope, - SEMANTIC_ATTRIBUTE_EXCLUSIVE_TIME, - SEMANTIC_ATTRIBUTE_SENTRY_MEASUREMENT_UNIT, - SEMANTIC_ATTRIBUTE_SENTRY_MEASUREMENT_VALUE, - SEMANTIC_ATTRIBUTE_SENTRY_OP, - SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN, -} from '@sentry/core'; -import { DEBUG_BUILD } from '../debug-build'; -import { htmlTreeAsString } from '../htmlTreeAsString'; -import { addLcpInstrumentationHandler } from './instrument'; -import type { WebVitalReportEvent } from './utils'; -import { listenForWebVitalReportEvents, msToSec, startStandaloneWebVitalSpan, supportsWebVital } from './utils'; - /** * 60 seconds is the maximum for a plausible LCP value. */ @@ -23,100 +6,3 @@ export const MAX_PLAUSIBLE_LCP_DURATION = 60_000; export function isValidLcpMetric(lcpValue: number | undefined): lcpValue is number { return lcpValue != null && lcpValue > 0 && lcpValue <= MAX_PLAUSIBLE_LCP_DURATION; } - -/** - * Starts tracking the Largest Contentful Paint on the current page and collects the value once - * - * - the page visibility is hidden - * - a navigation span is started (to stop LCP measurement for SPA soft navigations) - * - * Once either of these events triggers, the LCP value is sent as a standalone span and we stop - * measuring LCP for subsequent routes. - */ -export function trackLcpAsStandaloneSpan(client: Client): void { - let standaloneLcpValue = 0; - let standaloneLcpEntry: LargestContentfulPaint | undefined; - - if (!supportsWebVital('largest-contentful-paint')) { - return; - } - - const cleanupLcpHandler = addLcpInstrumentationHandler(({ metric }) => { - const entry = metric.entries[metric.entries.length - 1] as LargestContentfulPaint | undefined; - if (!entry || !isValidLcpMetric(metric.value)) { - return; - } - standaloneLcpValue = metric.value; - standaloneLcpEntry = entry; - }, true); - - listenForWebVitalReportEvents(client, (reportEvent, pageloadSpanId) => { - _sendStandaloneLcpSpan(standaloneLcpValue, standaloneLcpEntry, pageloadSpanId, reportEvent); - cleanupLcpHandler(); - }); -} - -/** - * Exported only for testing! - */ -export function _sendStandaloneLcpSpan( - lcpValue: number, - entry: LargestContentfulPaint | undefined, - pageloadSpanId: string, - reportEvent: WebVitalReportEvent, -) { - if (!isValidLcpMetric(lcpValue)) { - return; - } - - DEBUG_BUILD && debug.log(`Sending LCP span (${lcpValue})`); - - const startTime = msToSec((browserPerformanceTimeOrigin() || 0) + (entry?.startTime || 0)); - const routeName = getCurrentScope().getScopeData().transactionName; - - const name = entry ? htmlTreeAsString(entry.element) : 'Largest contentful paint'; - - const attributes: SpanAttributes = { - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.http.browser.lcp', - [SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'ui.webvital.lcp', - [SEMANTIC_ATTRIBUTE_EXCLUSIVE_TIME]: 0, // LCP is a point-in-time metric - // attach the pageload span id to the LCP span so that we can link them in the UI - 'sentry.pageload.span_id': pageloadSpanId, - // describes what triggered the web vital to be reported - 'sentry.report_event': reportEvent, - }; - - if (entry) { - entry.element && (attributes['lcp.element'] = htmlTreeAsString(entry.element)); - entry.id && (attributes['lcp.id'] = entry.id); - - entry.url && (attributes['lcp.url'] = entry.url); - - // loadTime is the time of LCP that's related to receiving the LCP element response.. - entry.loadTime != null && (attributes['lcp.loadTime'] = entry.loadTime); - - // renderTime is loadTime + rendering time - // it's 0 if the LCP element is loaded from a 3rd party origin that doesn't send the - // `Timing-Allow-Origin` header. - entry.renderTime != null && (attributes['lcp.renderTime'] = entry.renderTime); - - entry.size != null && (attributes['lcp.size'] = entry.size); - } - - const span = startStandaloneWebVitalSpan({ - name, - transaction: routeName, - attributes, - startTime, - }); - - if (span) { - span.addEvent('lcp', { - [SEMANTIC_ATTRIBUTE_SENTRY_MEASUREMENT_UNIT]: 'millisecond', - [SEMANTIC_ATTRIBUTE_SENTRY_MEASUREMENT_VALUE]: lcpValue, - }); - - // LCP is a point-in-time metric, so we end the span immediately - span.end(startTime); - } -} diff --git a/packages/browser-utils/src/metrics/webVitalSpans.ts b/packages/browser-utils/src/metrics/webVitalSpans.ts index 1cb4854a3c18..e916ec362340 100644 --- a/packages/browser-utils/src/metrics/webVitalSpans.ts +++ b/packages/browser-utils/src/metrics/webVitalSpans.ts @@ -1,4 +1,4 @@ -import type { Client, Span, SpanAttributes } from '@sentry/core'; +import type { Span, SpanAttributes } from '@sentry/core'; import { browserPerformanceTimeOrigin, debug, @@ -10,43 +10,22 @@ import { SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN, spanToStreamedSpanJSON, startInactiveSpan, - timestampInSeconds, } from '@sentry/core'; import { DEBUG_BUILD } from '../debug-build'; import { htmlTreeAsString } from '../htmlTreeAsString'; import { WINDOW } from '../types'; import { getCachedInteractionContext, INP_ENTRY_MAP, MAX_PLAUSIBLE_INP_DURATION } from './inp'; import type { InstrumentationHandlerCallback } from './instrument'; -import { addClsInstrumentationHandler, addInpInstrumentationHandler, addLcpInstrumentationHandler } from './instrument'; -import { isValidLcpMetric } from './lcp'; +import { addInpInstrumentationHandler } from './instrument'; import type { WebVitalReportEvent } from './utils'; -import { getBrowserPerformanceAPI, listenForWebVitalReportEvents, msToSec, supportsWebVital } from './utils'; +import { getBrowserPerformanceAPI, msToSec } from './utils'; import type { PerformanceEventTiming } from './instrument'; -// Locally-defined interfaces to avoid leaking bare global type references into the -// generated .d.ts. The `declare global` augmentations in web-vitals/types.ts make these -// available during this package's compilation but are NOT carried to consumers. -// This mirrors the pattern used for PerformanceEventTiming in instrument.ts. -export interface LayoutShift extends PerformanceEntry { - value: number; - sources: Array<{ node: Node | null }>; - hadRecentInput: boolean; -} - -export interface LargestContentfulPaint extends PerformanceEntry { - readonly renderTime: DOMHighResTimeStamp; - readonly loadTime: DOMHighResTimeStamp; - readonly size: number; - readonly id: string; - readonly url: string; - readonly element: Element | null; -} - interface WebVitalSpanOptions { name: string; op: string; origin: string; - metricName: 'lcp' | 'cls' | 'inp'; + metricName: 'inp'; value: number; attributes?: SpanAttributes; parentSpan?: Span; @@ -86,7 +65,6 @@ export function _emitWebVitalSpan(options: WebVitalSpanOptions): void { }; if (parentSpan && spanToStreamedSpanJSON(parentSpan).attributes?.[SEMANTIC_ATTRIBUTE_SENTRY_OP] === 'pageload') { - // for LCP and CLS, we collect the pageload span id as an attribute attributes['sentry.pageload.span_id'] = parentSpan.spanContext().spanId; } @@ -109,136 +87,6 @@ export function _emitWebVitalSpan(options: WebVitalSpanOptions): void { } } -/** - * Tracks LCP as a streamed span. - */ -export function trackLcpAsSpan(client: Client): void { - let lcpValue = 0; - let lcpEntry: LargestContentfulPaint | undefined; - - if (!supportsWebVital('largest-contentful-paint')) { - return; - } - - const cleanupLcpHandler = addLcpInstrumentationHandler(({ metric }) => { - const entry = metric.entries[metric.entries.length - 1] as LargestContentfulPaint | undefined; - if (!entry || !isValidLcpMetric(metric.value)) { - return; - } - lcpValue = metric.value; - lcpEntry = entry; - }, true); - - listenForWebVitalReportEvents(client, (reportEvent, _, pageloadSpan) => { - _sendLcpSpan(lcpValue, lcpEntry, pageloadSpan, reportEvent); - cleanupLcpHandler(); - }); -} - -/** - * Exported only for testing. - */ -export function _sendLcpSpan( - lcpValue: number, - entry: LargestContentfulPaint | undefined, - pageloadSpan?: Span, - reportEvent?: WebVitalReportEvent, -): void { - if (!isValidLcpMetric(lcpValue)) { - return; - } - - DEBUG_BUILD && debug.log(`Sending LCP span (${lcpValue})`); - - const performanceTimeOrigin = browserPerformanceTimeOrigin() || 0; - const timeOrigin = msToSec(performanceTimeOrigin); - const endTime = msToSec(performanceTimeOrigin + (entry?.startTime || 0)); - const name = entry ? htmlTreeAsString(entry.element) : 'Largest contentful paint'; - - const attributes: SpanAttributes = {}; - - entry?.element && (attributes['browser.web_vital.lcp.element'] = htmlTreeAsString(entry.element)); - entry?.id && (attributes['browser.web_vital.lcp.id'] = entry.id); - entry?.url && (attributes['browser.web_vital.lcp.url'] = entry.url); - entry?.loadTime != null && (attributes['browser.web_vital.lcp.load_time'] = entry.loadTime); - entry?.renderTime != null && (attributes['browser.web_vital.lcp.render_time'] = entry.renderTime); - entry?.size != null && (attributes['browser.web_vital.lcp.size'] = entry.size); - - _emitWebVitalSpan({ - name, - op: 'ui.webvital.lcp', - origin: 'auto.http.browser.lcp', - metricName: 'lcp', - value: lcpValue, - attributes, - parentSpan: pageloadSpan, - reportEvent, - startTime: timeOrigin, - endTime, - }); -} - -/** - * Tracks CLS as a streamed span. - */ -export function trackClsAsSpan(client: Client): void { - let clsValue = 0; - let clsEntry: LayoutShift | undefined; - - if (!supportsWebVital('layout-shift')) { - return; - } - - const cleanupClsHandler = addClsInstrumentationHandler(({ metric }) => { - const entry = metric.entries[metric.entries.length - 1] as LayoutShift | undefined; - if (!entry) { - return; - } - clsValue = metric.value; - clsEntry = entry; - }, true); - - listenForWebVitalReportEvents(client, (reportEvent, _, pageloadSpan) => { - _sendClsSpan(clsValue, clsEntry, pageloadSpan, reportEvent); - cleanupClsHandler(); - }); -} - -/** - * Exported only for testing. - */ -export function _sendClsSpan( - clsValue: number, - entry: LayoutShift | undefined, - pageloadSpan?: Span, - reportEvent?: WebVitalReportEvent, -): void { - DEBUG_BUILD && debug.log(`Sending CLS span (${clsValue})`); - - const startTime = entry ? msToSec((browserPerformanceTimeOrigin() || 0) + entry.startTime) : timestampInSeconds(); - const name = entry ? htmlTreeAsString(entry.sources[0]?.node) : 'Layout shift'; - - const attributes: SpanAttributes = {}; - - if (entry?.sources) { - entry.sources.forEach((source, index) => { - attributes[`browser.web_vital.cls.source.${index + 1}`] = htmlTreeAsString(source.node); - }); - } - - _emitWebVitalSpan({ - name, - op: 'ui.webvital.cls', - origin: 'auto.http.browser.cls', - metricName: 'cls', - value: clsValue, - attributes, - parentSpan: pageloadSpan, - reportEvent, - startTime, - }); -} - /** * Tracks INP as a streamed span. * diff --git a/packages/browser-utils/test/browser/browserMetrics.test.ts b/packages/browser-utils/test/browser/browserMetrics.test.ts index 3b19bc78c70d..5f70b515d55c 100644 --- a/packages/browser-utils/test/browser/browserMetrics.test.ts +++ b/packages/browser-utils/test/browser/browserMetrics.test.ts @@ -97,8 +97,8 @@ describe('addWebVitalsToSpan', () => { }); const cleanupWebVitals = startTrackingWebVitals({ - recordClsStandaloneSpans: undefined, - recordLcpStandaloneSpans: undefined, + trackCls: true, + trackLcp: true, client: getClient()!, }); diff --git a/packages/browser-utils/test/metrics/cls.test.ts b/packages/browser-utils/test/metrics/cls.test.ts deleted file mode 100644 index 9a2c94da04d2..000000000000 --- a/packages/browser-utils/test/metrics/cls.test.ts +++ /dev/null @@ -1,235 +0,0 @@ -import * as SentryCore from '@sentry/core'; -import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; -import { htmlTreeAsString } from '../../src/htmlTreeAsString'; -import { _sendStandaloneClsSpan } from '../../src/metrics/cls'; -import * as WebVitalUtils from '../../src/metrics/utils'; - -// Mock all Sentry core dependencies -vi.mock('@sentry/core', async () => { - const actual = await vi.importActual('@sentry/core'); - return { - ...actual, - browserPerformanceTimeOrigin: vi.fn(), - timestampInSeconds: vi.fn(), - getCurrentScope: vi.fn(), - }; -}); - -vi.mock('../../src/htmlTreeAsString', () => ({ - htmlTreeAsString: vi.fn(), -})); - -describe('_sendStandaloneClsSpan', () => { - const mockSpan = { - addEvent: vi.fn(), - end: vi.fn(), - }; - - const mockScope = { - getScopeData: vi.fn().mockReturnValue({ - transactionName: 'test-transaction', - }), - }; - - afterEach(() => { - vi.clearAllMocks(); - }); - - beforeEach(() => { - vi.mocked(SentryCore.getCurrentScope).mockReturnValue(mockScope as any); - vi.mocked(SentryCore.browserPerformanceTimeOrigin).mockReturnValue(1000); - vi.mocked(SentryCore.timestampInSeconds).mockReturnValue(1.5); - vi.mocked(htmlTreeAsString).mockImplementation((node: any) => `<${node?.tagName || 'div'}>`); - vi.spyOn(WebVitalUtils, 'startStandaloneWebVitalSpan').mockReturnValue(mockSpan as any); - }); - - it('sends a standalone CLS span with entry data', () => { - const clsValue = 0.1; - const mockEntry: LayoutShift = { - name: 'layout-shift', - entryType: 'layout-shift', - startTime: 100, - duration: 0, - value: clsValue, - hadRecentInput: false, - sources: [ - // @ts-expect-error - other properties are irrelevant - { - node: { tagName: 'div' } as Element, - }, - ], - toJSON: vi.fn(), - }; - const pageloadSpanId = '123'; - const reportEvent = 'navigation'; - - _sendStandaloneClsSpan(clsValue, mockEntry, pageloadSpanId, reportEvent); - - expect(WebVitalUtils.startStandaloneWebVitalSpan).toHaveBeenCalledWith({ - name: '
', - transaction: 'test-transaction', - attributes: { - 'sentry.origin': 'auto.http.browser.cls', - 'sentry.op': 'ui.webvital.cls', - 'sentry.exclusive_time': 0, - 'sentry.pageload.span_id': '123', - 'sentry.report_event': 'navigation', - 'cls.source.1': '
', - }, - startTime: 1.1, // (1000 + 100) / 1000 - }); - - expect(mockSpan.addEvent).toHaveBeenCalledWith('cls', { - 'sentry.measurement_unit': '', - 'sentry.measurement_value': 0.1, - }); - - expect(mockSpan.end).toHaveBeenCalledWith(1.1); - }); - - it('sends a standalone CLS span without entry data', () => { - const clsValue = 0; - const pageloadSpanId = '456'; - const reportEvent = 'pagehide'; - - _sendStandaloneClsSpan(clsValue, undefined, pageloadSpanId, reportEvent); - - expect(SentryCore.timestampInSeconds).toHaveBeenCalled(); - expect(SentryCore.browserPerformanceTimeOrigin).not.toHaveBeenCalled(); - - expect(WebVitalUtils.startStandaloneWebVitalSpan).toHaveBeenCalledWith({ - name: 'Layout shift', - transaction: 'test-transaction', - attributes: { - 'sentry.origin': 'auto.http.browser.cls', - 'sentry.op': 'ui.webvital.cls', - 'sentry.exclusive_time': 0, - 'sentry.pageload.span_id': pageloadSpanId, - 'sentry.report_event': 'pagehide', - }, - startTime: 1.5, - }); - - expect(mockSpan.end).toHaveBeenCalledWith(1.5); - expect(mockSpan.addEvent).toHaveBeenCalledWith('cls', { - 'sentry.measurement_unit': '', - 'sentry.measurement_value': 0, - }); - }); - - it('handles entry with multiple sources', () => { - const clsValue = 0.15; - const mockEntry: LayoutShift = { - name: 'layout-shift', - entryType: 'layout-shift', - startTime: 200, - duration: 0, - value: clsValue, - hadRecentInput: false, - sources: [ - // @ts-expect-error - other properties are irrelevant - { - node: { tagName: 'div' } as Element, - }, - // @ts-expect-error - other properties are irrelevant - { - node: { tagName: 'span' } as Element, - }, - ], - toJSON: vi.fn(), - }; - const pageloadSpanId = '789'; - - vi.mocked(htmlTreeAsString) - .mockReturnValueOnce('
') // for the name - .mockReturnValueOnce('
') // for source 1 - .mockReturnValueOnce(''); // for source 2 - - _sendStandaloneClsSpan(clsValue, mockEntry, pageloadSpanId, 'navigation'); - - expect(htmlTreeAsString).toHaveBeenCalledTimes(3); - expect(WebVitalUtils.startStandaloneWebVitalSpan).toHaveBeenCalledWith({ - name: '
', - transaction: 'test-transaction', - attributes: { - 'sentry.origin': 'auto.http.browser.cls', - 'sentry.op': 'ui.webvital.cls', - 'sentry.exclusive_time': 0, - 'sentry.pageload.span_id': '789', - 'sentry.report_event': 'navigation', - 'cls.source.1': '
', - 'cls.source.2': '', - }, - startTime: 1.2, // (1000 + 200) / 1000 - }); - }); - - it('handles entry without sources', () => { - const clsValue = 0.05; - const mockEntry: LayoutShift = { - name: 'layout-shift', - entryType: 'layout-shift', - startTime: 50, - duration: 0, - value: clsValue, - hadRecentInput: false, - sources: [], - toJSON: vi.fn(), - }; - const pageloadSpanId = '101'; - - _sendStandaloneClsSpan(clsValue, mockEntry, pageloadSpanId, 'navigation'); - - expect(WebVitalUtils.startStandaloneWebVitalSpan).toHaveBeenCalledWith({ - name: '
', - transaction: 'test-transaction', - attributes: { - 'sentry.origin': 'auto.http.browser.cls', - 'sentry.op': 'ui.webvital.cls', - 'sentry.exclusive_time': 0, - 'sentry.pageload.span_id': '101', - 'sentry.report_event': 'navigation', - }, - startTime: 1.05, // (1000 + 50) / 1000 - }); - }); - - it('handles when startStandaloneWebVitalSpan returns undefined', () => { - vi.spyOn(WebVitalUtils, 'startStandaloneWebVitalSpan').mockReturnValue(undefined); - - const clsValue = 0.1; - const pageloadSpanId = '123'; - - expect(() => { - _sendStandaloneClsSpan(clsValue, undefined, pageloadSpanId, 'navigation'); - }).not.toThrow(); - - expect(mockSpan.addEvent).not.toHaveBeenCalled(); - expect(mockSpan.end).not.toHaveBeenCalled(); - }); - - it('handles when browserPerformanceTimeOrigin returns null', () => { - vi.mocked(SentryCore.browserPerformanceTimeOrigin).mockReturnValue(undefined); - - const clsValue = 0.1; - const mockEntry: LayoutShift = { - name: 'layout-shift', - entryType: 'layout-shift', - startTime: 200, - duration: 0, - value: clsValue, - hadRecentInput: false, - sources: [], - toJSON: vi.fn(), - }; - const pageloadSpanId = '123'; - - _sendStandaloneClsSpan(clsValue, mockEntry, pageloadSpanId, 'navigation'); - - expect(WebVitalUtils.startStandaloneWebVitalSpan).toHaveBeenCalledWith( - expect.objectContaining({ - startTime: 0.2, - }), - ); - }); -}); diff --git a/packages/browser-utils/test/metrics/lcp.test.ts b/packages/browser-utils/test/metrics/lcp.test.ts index baa7cd5de052..9315f7032b59 100644 --- a/packages/browser-utils/test/metrics/lcp.test.ts +++ b/packages/browser-utils/test/metrics/lcp.test.ts @@ -1,21 +1,5 @@ -import * as SentryCore from '@sentry/core'; -import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; -import { htmlTreeAsString } from '../../src/htmlTreeAsString'; -import { _sendStandaloneLcpSpan, isValidLcpMetric, MAX_PLAUSIBLE_LCP_DURATION } from '../../src/metrics/lcp'; -import * as WebVitalUtils from '../../src/metrics/utils'; - -vi.mock('@sentry/core', async () => { - const actual = await vi.importActual('@sentry/core'); - return { - ...actual, - browserPerformanceTimeOrigin: vi.fn(), - getCurrentScope: vi.fn(), - }; -}); - -vi.mock('../../src/htmlTreeAsString', () => ({ - htmlTreeAsString: vi.fn(), -})); +import { describe, expect, it } from 'vitest'; +import { isValidLcpMetric, MAX_PLAUSIBLE_LCP_DURATION } from '../../src/metrics/lcp'; describe('isValidLcpMetric', () => { it('returns true for plausible lcp values', () => { @@ -31,79 +15,3 @@ describe('isValidLcpMetric', () => { expect(isValidLcpMetric(MAX_PLAUSIBLE_LCP_DURATION + 1)).toBe(false); }); }); - -describe('_sendStandaloneLcpSpan', () => { - const mockSpan = { - addEvent: vi.fn(), - end: vi.fn(), - }; - - const mockScope = { - getScopeData: vi.fn().mockReturnValue({ - transactionName: 'test-transaction', - }), - }; - - beforeEach(() => { - vi.mocked(SentryCore.getCurrentScope).mockReturnValue(mockScope as any); - vi.mocked(SentryCore.browserPerformanceTimeOrigin).mockReturnValue(1000); - vi.mocked(htmlTreeAsString).mockImplementation((node: any) => `<${node?.tagName || 'div'}>`); - vi.spyOn(WebVitalUtils, 'startStandaloneWebVitalSpan').mockReturnValue(mockSpan as any); - }); - - afterEach(() => { - vi.clearAllMocks(); - }); - - it('sends a standalone lcp span with entry data', () => { - const lcpValue = 1_234; - const mockEntry: LargestContentfulPaint = { - name: 'largest-contentful-paint', - entryType: 'largest-contentful-paint', - startTime: 100, - duration: 0, - id: 'image', - url: 'https://example.com/image.png', - size: 1234, - loadTime: 95, - renderTime: 100, - element: { tagName: 'img' } as Element, - toJSON: vi.fn(), - }; - - _sendStandaloneLcpSpan(lcpValue, mockEntry, '123', 'navigation'); - - expect(WebVitalUtils.startStandaloneWebVitalSpan).toHaveBeenCalledWith({ - name: '', - transaction: 'test-transaction', - attributes: { - 'sentry.origin': 'auto.http.browser.lcp', - 'sentry.op': 'ui.webvital.lcp', - 'sentry.exclusive_time': 0, - 'sentry.pageload.span_id': '123', - 'sentry.report_event': 'navigation', - 'lcp.element': '', - 'lcp.id': 'image', - 'lcp.url': 'https://example.com/image.png', - 'lcp.loadTime': 95, - 'lcp.renderTime': 100, - 'lcp.size': 1234, - }, - startTime: 1.1, - }); - - expect(mockSpan.addEvent).toHaveBeenCalledWith('lcp', { - 'sentry.measurement_unit': 'millisecond', - 'sentry.measurement_value': lcpValue, - }); - expect(mockSpan.end).toHaveBeenCalledWith(1.1); - }); - - it('does not send a standalone lcp span for implausibly large values', () => { - _sendStandaloneLcpSpan(MAX_PLAUSIBLE_LCP_DURATION + 1, undefined, '123', 'pagehide'); - - expect(WebVitalUtils.startStandaloneWebVitalSpan).not.toHaveBeenCalled(); - expect(mockSpan.addEvent).not.toHaveBeenCalled(); - expect(mockSpan.end).not.toHaveBeenCalled(); - }); -}); diff --git a/packages/browser-utils/test/metrics/webVitalSpans.test.ts b/packages/browser-utils/test/metrics/webVitalSpans.test.ts index 640a2ee95f71..1feb4438c98b 100644 --- a/packages/browser-utils/test/metrics/webVitalSpans.test.ts +++ b/packages/browser-utils/test/metrics/webVitalSpans.test.ts @@ -2,8 +2,7 @@ import * as SentryCore from '@sentry/core'; import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; import { htmlTreeAsString } from '../../src/htmlTreeAsString'; import * as inpModule from '../../src/metrics/inp'; -import { MAX_PLAUSIBLE_LCP_DURATION } from '../../src/metrics/lcp'; -import { _emitWebVitalSpan, _sendClsSpan, _sendInpSpan, _sendLcpSpan } from '../../src/metrics/webVitalSpans'; +import { _emitWebVitalSpan, _sendInpSpan } from '../../src/metrics/webVitalSpans'; vi.mock('@sentry/core', async () => { const actual = await vi.importActual('@sentry/core'); @@ -200,173 +199,6 @@ describe('_emitWebVitalSpan', () => { }); }); -describe('_sendLcpSpan', () => { - const mockSpan = { - end: vi.fn(), - }; - - const mockScope = { - getScopeData: vi.fn().mockReturnValue({ - transactionName: 'test-route', - }), - }; - - beforeEach(() => { - vi.mocked(SentryCore.getCurrentScope).mockReturnValue(mockScope as any); - vi.mocked(SentryCore.browserPerformanceTimeOrigin).mockReturnValue(1000); - vi.mocked(htmlTreeAsString).mockImplementation((node: any) => `<${node?.tagName || 'div'}>`); - vi.mocked(SentryCore.startInactiveSpan).mockReturnValue(mockSpan as any); - vi.mocked(SentryCore.spanToStreamedSpanJSON).mockReturnValue({ - attributes: { 'sentry.op': 'pageload' }, - } as any); - }); - - afterEach(() => { - vi.clearAllMocks(); - }); - - it('sends a streamed LCP span with entry data', () => { - const mockEntry = { - element: { tagName: 'img' } as Element, - id: 'hero', - url: 'https://example.com/hero.jpg', - loadTime: 100, - renderTime: 150, - size: 50000, - startTime: 200, - } as LargestContentfulPaint; - - const mockPageloadSpan = createMockPageloadSpan('pageload-123'); - - _sendLcpSpan(250, mockEntry, mockPageloadSpan as any, 'pagehide'); - - expect(SentryCore.startInactiveSpan).toHaveBeenCalledWith( - expect.objectContaining({ - name: '', - attributes: expect.objectContaining({ - 'sentry.origin': 'auto.http.browser.lcp', - 'sentry.op': 'ui.webvital.lcp', - 'sentry.exclusive_time': 0, - 'sentry.pageload.span_id': 'pageload-123', - 'browser.web_vital.lcp.element': '', - 'browser.web_vital.lcp.id': 'hero', - 'browser.web_vital.lcp.url': 'https://example.com/hero.jpg', - 'browser.web_vital.lcp.load_time': 100, - 'browser.web_vital.lcp.render_time': 150, - 'browser.web_vital.lcp.size': 50000, - 'browser.web_vital.lcp.report_event': 'pagehide', - 'sentry.transaction': 'test-route', - }), - startTime: 1, // timeOrigin: 1000 / 1000 - parentSpan: mockPageloadSpan, - }), - ); - - // endTime = timeOrigin + entry.startTime = (1000 + 200) / 1000 = 1.2 - expect(mockSpan.end).toHaveBeenCalledWith(1.2); - }); - - it('sends a streamed LCP span without entry data', () => { - _sendLcpSpan(250, undefined); - - expect(SentryCore.startInactiveSpan).toHaveBeenCalledWith( - expect.objectContaining({ - name: 'Largest contentful paint', - startTime: 1, // timeOrigin: 1000 / 1000 - }), - ); - }); - - it('drops implausible LCP values', () => { - _sendLcpSpan(0, undefined); - _sendLcpSpan(MAX_PLAUSIBLE_LCP_DURATION + 1, undefined); - - expect(SentryCore.startInactiveSpan).not.toHaveBeenCalled(); - }); -}); - -describe('_sendClsSpan', () => { - const mockSpan = { - end: vi.fn(), - }; - - const mockScope = { - getScopeData: vi.fn().mockReturnValue({ - transactionName: 'test-route', - }), - }; - - beforeEach(() => { - vi.mocked(SentryCore.getCurrentScope).mockReturnValue(mockScope as any); - vi.mocked(SentryCore.browserPerformanceTimeOrigin).mockReturnValue(1000); - vi.mocked(SentryCore.timestampInSeconds).mockReturnValue(1.5); - vi.mocked(htmlTreeAsString).mockImplementation((node: any) => `<${node?.tagName || 'div'}>`); - vi.mocked(SentryCore.startInactiveSpan).mockReturnValue(mockSpan as any); - vi.mocked(SentryCore.spanToStreamedSpanJSON).mockReturnValue({ - attributes: { 'sentry.op': 'pageload' }, - } as any); - }); - - afterEach(() => { - vi.clearAllMocks(); - }); - - it('sends a streamed CLS span with entry data and sources', () => { - const mockEntry: LayoutShift = { - name: 'layout-shift', - entryType: 'layout-shift', - startTime: 100, - duration: 0, - value: 0.1, - hadRecentInput: false, - sources: [ - // @ts-expect-error - other properties are irrelevant - { node: { tagName: 'div' } as Element }, - // @ts-expect-error - other properties are irrelevant - { node: { tagName: 'span' } as Element }, - ], - toJSON: vi.fn(), - }; - - vi.mocked(htmlTreeAsString) - .mockReturnValueOnce('
') // for the name - .mockReturnValueOnce('
') // for source 1 - .mockReturnValueOnce(''); // for source 2 - - const mockPageloadSpan = createMockPageloadSpan('pageload-789'); - - _sendClsSpan(0.1, mockEntry, mockPageloadSpan as any, 'navigation'); - - expect(SentryCore.startInactiveSpan).toHaveBeenCalledWith( - expect.objectContaining({ - name: '
', - attributes: expect.objectContaining({ - 'sentry.origin': 'auto.http.browser.cls', - 'sentry.op': 'ui.webvital.cls', - 'sentry.pageload.span_id': 'pageload-789', - 'browser.web_vital.cls.source.1': '
', - 'browser.web_vital.cls.source.2': '', - 'browser.web_vital.cls.report_event': 'navigation', - 'sentry.transaction': 'test-route', - }), - parentSpan: mockPageloadSpan, - }), - ); - }); - - it('sends a streamed CLS span without entry data', () => { - _sendClsSpan(0, undefined); - - expect(SentryCore.timestampInSeconds).toHaveBeenCalled(); - expect(SentryCore.startInactiveSpan).toHaveBeenCalledWith( - expect.objectContaining({ - name: 'Layout shift', - startTime: 1.5, - }), - ); - }); -}); - describe('_sendInpSpan', () => { const mockSpan = { end: vi.fn(), diff --git a/packages/browser/src/integrations/webVitals.ts b/packages/browser/src/integrations/webVitals.ts index 8332427ef782..ff97ac552589 100644 --- a/packages/browser/src/integrations/webVitals.ts +++ b/packages/browser/src/integrations/webVitals.ts @@ -5,9 +5,7 @@ import { registerInpInteractionListener, startTrackingINP, startTrackingWebVitals, - trackClsAsSpan, trackInpAsSpan, - trackLcpAsSpan, } from '@sentry-internal/browser-utils'; export const WEB_VITALS_INTEGRATION_NAME = 'WebVitals'; @@ -19,14 +17,6 @@ export interface WebVitalsOptions { * Web vitals to skip. */ ignore?: WebVitalName[]; - - /** - * @experimental - */ - _experiments?: Partial<{ - enableStandaloneClsSpans: boolean; - enableStandaloneLcpSpans: boolean; - }>; } /** @@ -43,16 +33,13 @@ export const webVitalsIntegration = defineIntegration((options: WebVitalsOptions name: WEB_VITALS_INTEGRATION_NAME, setup(client) { const spanStreamingEnabled = hasSpanStreamingEnabled(client); - const { enableStandaloneClsSpans, enableStandaloneLcpSpans } = options._experiments ?? {}; - const recordClsStandaloneSpans = - spanStreamingEnabled || ignored.has('cls') ? undefined : enableStandaloneClsSpans || false; - const recordLcpStandaloneSpans = - spanStreamingEnabled || ignored.has('lcp') ? undefined : enableStandaloneLcpSpans || false; + const trackCls = !ignored.has('cls'); + const trackLcp = !ignored.has('lcp'); const finalizeWebVitals = startTrackingWebVitals({ - recordClsStandaloneSpans, - recordLcpStandaloneSpans, + trackCls, + trackLcp, client, }); @@ -69,21 +56,13 @@ export const webVitalsIntegration = defineIntegration((options: WebVitalsOptions finalizeWebVitals(); addWebVitalsToSpan(span, { - // CLS/LCP are recorded as pageload span measurements only when they're neither - // tracked as standalone spans nor handled by span streaming (and not ignored). - recordClsOnPageloadSpan: recordClsStandaloneSpans === false, - recordLcpOnPageloadSpan: recordLcpStandaloneSpans === false, + recordClsOnPageloadSpan: trackCls, + recordLcpOnPageloadSpan: trackLcp, spanStreamingEnabled, }); }); if (spanStreamingEnabled) { - if (!ignored.has('lcp')) { - trackLcpAsSpan(client); - } - if (!ignored.has('cls')) { - trackClsAsSpan(client); - } if (!ignored.has('inp')) { trackInpAsSpan(); } diff --git a/packages/browser/src/tracing/browserTracingIntegration.ts b/packages/browser/src/tracing/browserTracingIntegration.ts index 3f3467538f82..01185dbba917 100644 --- a/packages/browser/src/tracing/browserTracingIntegration.ts +++ b/packages/browser/src/tracing/browserTracingIntegration.ts @@ -299,8 +299,6 @@ export interface BrowserTracingOptions { */ _experiments: Partial<{ enableInteractions: boolean; - enableStandaloneClsSpans: boolean; - enableStandaloneLcpSpans: boolean; }>; /** @@ -382,7 +380,7 @@ export const browserTracingIntegration = ((options: Partial vi.fn()); const mockRegisterInpInteractionListener = vi.hoisted(() => vi.fn()); const mockStartTrackingINP = vi.hoisted(() => vi.fn()); const mockStartTrackingWebVitals = vi.hoisted(() => vi.fn()); -const mockTrackClsAsSpan = vi.hoisted(() => vi.fn()); const mockTrackInpAsSpan = vi.hoisted(() => vi.fn()); -const mockTrackLcpAsSpan = vi.hoisted(() => vi.fn()); vi.mock('@sentry-internal/browser-utils', () => ({ addWebVitalsToSpan: mockAddWebVitalsToSpan, registerInpInteractionListener: mockRegisterInpInteractionListener, startTrackingINP: mockStartTrackingINP, startTrackingWebVitals: mockStartTrackingWebVitals, - trackClsAsSpan: mockTrackClsAsSpan, trackInpAsSpan: mockTrackInpAsSpan, - trackLcpAsSpan: mockTrackLcpAsSpan, })); function getMockClient(options: Record = {}) { @@ -52,7 +48,7 @@ describe('webVitalsIntegration', () => { vi.restoreAllMocks(); }); - it('tracks web vitals with the existing non-streaming behavior by default', () => { + it('tracks web vitals as measurements by default', () => { const client = getMockClient(); const integration = webVitalsIntegration(); @@ -60,36 +56,16 @@ describe('webVitalsIntegration', () => { integration.afterAllSetup?.(client as never); expect(mockStartTrackingWebVitals).toHaveBeenCalledWith({ - recordClsStandaloneSpans: false, - recordLcpStandaloneSpans: false, + trackCls: true, + trackLcp: true, client, }); expect(mockStartTrackingINP).toHaveBeenCalledTimes(1); expect(mockRegisterInpInteractionListener).toHaveBeenCalledTimes(1); - expect(mockTrackLcpAsSpan).not.toHaveBeenCalled(); - expect(mockTrackClsAsSpan).not.toHaveBeenCalled(); expect(mockTrackInpAsSpan).not.toHaveBeenCalled(); }); - it('keeps standalone LCP and CLS experiments working', () => { - const client = getMockClient(); - 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', () => { + it('tracks INP as a streamed span when span streaming is enabled', () => { const client = getMockClient({ traceLifecycle: 'stream' }); const integration = webVitalsIntegration(); @@ -97,18 +73,16 @@ describe('webVitalsIntegration', () => { integration.afterAllSetup?.(client as never); expect(mockStartTrackingWebVitals).toHaveBeenCalledWith({ - recordClsStandaloneSpans: undefined, - recordLcpStandaloneSpans: undefined, + trackCls: true, + trackLcp: true, client, }); - expect(mockTrackLcpAsSpan).toHaveBeenCalledWith(client); - expect(mockTrackClsAsSpan).toHaveBeenCalledWith(client); expect(mockTrackInpAsSpan).toHaveBeenCalledTimes(1); expect(mockStartTrackingINP).not.toHaveBeenCalled(); expect(mockRegisterInpInteractionListener).toHaveBeenCalledTimes(1); }); - it('supports ignoring selected web vitals for browserTracingIntegration compatibility', () => { + it('supports ignoring selected web vitals', () => { const client = getMockClient(); const integration = webVitalsIntegration({ ignore: ['cls', 'inp', 'lcp'] }); @@ -116,8 +90,8 @@ describe('webVitalsIntegration', () => { integration.afterAllSetup?.(client as never); expect(mockStartTrackingWebVitals).toHaveBeenCalledWith({ - recordClsStandaloneSpans: undefined, - recordLcpStandaloneSpans: undefined, + trackCls: false, + trackLcp: false, client, }); expect(mockStartTrackingINP).not.toHaveBeenCalled(); @@ -155,7 +129,7 @@ describe('webVitalsIntegration', () => { expect(mockAddWebVitalsToSpan).not.toHaveBeenCalled(); }); - it('does not record CLS/LCP on the pageload span when span streaming is enabled', () => { + it('records CLS/LCP on the pageload span even when span streaming is enabled', () => { const client = getMockClient({ traceLifecycle: 'stream' }); const span = {}; @@ -164,27 +138,9 @@ describe('webVitalsIntegration', () => { client.emit('spanEnd', span); expect(mockAddWebVitalsToSpan).toHaveBeenCalledWith(span, { - recordClsOnPageloadSpan: false, - recordLcpOnPageloadSpan: false, + recordClsOnPageloadSpan: true, + recordLcpOnPageloadSpan: true, spanStreamingEnabled: true, }); }); - - it('does not record CLS/LCP on the pageload span when standalone spans are enabled', () => { - const client = getMockClient(); - const span = {}; - const integration = webVitalsIntegration({ - _experiments: { enableStandaloneClsSpans: true, enableStandaloneLcpSpans: true }, - }); - - integration.setup?.(client as never); - client.emit('afterStartPageLoadSpan', span); - client.emit('spanEnd', span); - - expect(mockAddWebVitalsToSpan).toHaveBeenCalledWith(span, { - recordClsOnPageloadSpan: false, - recordLcpOnPageloadSpan: false, - spanStreamingEnabled: false, - }); - }); });