From 64ac02d53910c9eade9c8864dc1fb197e80f40dc Mon Sep 17 00:00:00 2001 From: Abdelrahman Awad Date: Thu, 4 Jun 2026 12:19:32 -0400 Subject: [PATCH] ref(browser): Remove experimental CLS/LCP standalone and streamed spans CLS and LCP are now always recorded as measurements (on transaction pageload spans) or attributes (on streamed pageload spans), never as their own separate spans. Removes `_experiments.enableStandaloneClsSpans` and `_experiments.enableStandaloneLcpSpans` options from both `webVitalsIntegration` and `browserTracingIntegration`. Co-Authored-By: Claude Opus 4.6 --- .../web-vitals-cls-standalone-spans/init.js | 16 - .../subject.js | 17 - .../template.html | 10 - .../web-vitals-cls-standalone-spans/test.ts | 516 ------------------ .../web-vitals-cls-streamed-spans/init.js | 11 - .../web-vitals-cls-streamed-spans/subject.js | 17 - .../template.html | 10 - .../web-vitals-cls-streamed-spans/test.ts | 76 --- .../assets/sentry-logo-600x179.png | Bin 16118 -> 0 bytes .../web-vitals-lcp-standalone-spans/init.js | 17 - .../template.html | 10 - .../web-vitals-lcp-standalone-spans/test.ts | 369 ------------- .../assets/sentry-logo-600x179.png | Bin 16118 -> 0 bytes .../web-vitals-lcp-streamed-spans/init.js | 11 - .../template.html | 10 - .../web-vitals-lcp-streamed-spans/test.ts | 65 --- packages/browser-utils/src/index.ts | 2 +- .../src/metrics/browserMetrics.ts | 65 +-- packages/browser-utils/src/metrics/cls.ts | 103 +--- packages/browser-utils/src/metrics/lcp.ts | 114 ---- .../src/metrics/webVitalSpans.ts | 160 +----- .../test/browser/browserMetrics.test.ts | 4 +- .../browser-utils/test/metrics/cls.test.ts | 235 -------- .../browser-utils/test/metrics/lcp.test.ts | 96 +--- .../test/metrics/webVitalSpans.test.ts | 170 +----- .../browser/src/integrations/webVitals.ts | 33 +- .../src/tracing/browserTracingIntegration.ts | 8 +- .../test/integrations/webVitals.test.ts | 68 +-- 28 files changed, 57 insertions(+), 2156 deletions(-) delete mode 100644 dev-packages/browser-integration-tests/suites/tracing/metrics/web-vitals-cls-standalone-spans/init.js delete mode 100644 dev-packages/browser-integration-tests/suites/tracing/metrics/web-vitals-cls-standalone-spans/subject.js delete mode 100644 dev-packages/browser-integration-tests/suites/tracing/metrics/web-vitals-cls-standalone-spans/template.html delete mode 100644 dev-packages/browser-integration-tests/suites/tracing/metrics/web-vitals-cls-standalone-spans/test.ts delete mode 100644 dev-packages/browser-integration-tests/suites/tracing/metrics/web-vitals-cls-streamed-spans/init.js delete mode 100644 dev-packages/browser-integration-tests/suites/tracing/metrics/web-vitals-cls-streamed-spans/subject.js delete mode 100644 dev-packages/browser-integration-tests/suites/tracing/metrics/web-vitals-cls-streamed-spans/template.html delete mode 100644 dev-packages/browser-integration-tests/suites/tracing/metrics/web-vitals-cls-streamed-spans/test.ts delete mode 100644 dev-packages/browser-integration-tests/suites/tracing/metrics/web-vitals-lcp-standalone-spans/assets/sentry-logo-600x179.png delete mode 100644 dev-packages/browser-integration-tests/suites/tracing/metrics/web-vitals-lcp-standalone-spans/init.js delete mode 100644 dev-packages/browser-integration-tests/suites/tracing/metrics/web-vitals-lcp-standalone-spans/template.html delete mode 100644 dev-packages/browser-integration-tests/suites/tracing/metrics/web-vitals-lcp-standalone-spans/test.ts delete mode 100644 dev-packages/browser-integration-tests/suites/tracing/metrics/web-vitals-lcp-streamed-spans/assets/sentry-logo-600x179.png delete mode 100644 dev-packages/browser-integration-tests/suites/tracing/metrics/web-vitals-lcp-streamed-spans/init.js delete mode 100644 dev-packages/browser-integration-tests/suites/tracing/metrics/web-vitals-lcp-streamed-spans/template.html delete mode 100644 dev-packages/browser-integration-tests/suites/tracing/metrics/web-vitals-lcp-streamed-spans/test.ts delete mode 100644 packages/browser-utils/test/metrics/cls.test.ts 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 353b7233d6bfa4f026f9998cacfa4add4bba9274..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 16118 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 Cl9AH@ 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 353b7233d6bfa4f026f9998cacfa4add4bba9274..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 16118 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 Cl9AH@ 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, - }); - }); });