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): PromiseSome 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
-
-
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
-
-
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: '