diff --git a/packages/browser-utils/src/metrics/browserMetrics.ts b/packages/browser-utils/src/metrics/browserMetrics.ts index 18b274b855e0..e9fa822a431e 100644 --- a/packages/browser-utils/src/metrics/browserMetrics.ts +++ b/packages/browser-utils/src/metrics/browserMetrics.ts @@ -1,5 +1,5 @@ /* eslint-disable max-lines */ -import type { Measurements, Span, SpanAttributes, SpanAttributeValue, StartSpanOptions } from '@sentry/core'; +import type { Client, Measurements, Span, SpanAttributes, SpanAttributeValue, StartSpanOptions } from '@sentry/core'; import { browserPerformanceTimeOrigin, getActiveSpan, @@ -83,6 +83,7 @@ let _clsEntry: LayoutShift | undefined; interface StartTrackingWebVitalsOptions { recordClsStandaloneSpans: boolean; recordLcpStandaloneSpans: boolean; + client: Client; } /** @@ -94,6 +95,7 @@ interface StartTrackingWebVitalsOptions { export function startTrackingWebVitals({ recordClsStandaloneSpans, recordLcpStandaloneSpans, + client, }: StartTrackingWebVitalsOptions): () => void { const performance = getBrowserPerformanceAPI(); if (performance && browserPerformanceTimeOrigin()) { @@ -102,9 +104,9 @@ export function startTrackingWebVitals({ WINDOW.performance.mark('sentry-tracing-init'); } const fidCleanupCallback = _trackFID(); - const lcpCleanupCallback = recordLcpStandaloneSpans ? trackLcpAsStandaloneSpan() : _trackLCP(); + const lcpCleanupCallback = recordLcpStandaloneSpans ? trackLcpAsStandaloneSpan(client) : _trackLCP(); const ttfbCleanupCallback = _trackTtfb(); - const clsCleanupCallback = recordClsStandaloneSpans ? trackClsAsStandaloneSpan() : _trackCLS(); + const clsCleanupCallback = recordClsStandaloneSpans ? trackClsAsStandaloneSpan(client) : _trackCLS(); return (): void => { fidCleanupCallback(); diff --git a/packages/browser-utils/src/metrics/cls.ts b/packages/browser-utils/src/metrics/cls.ts index 8839f038fb49..e300fd28d18b 100644 --- a/packages/browser-utils/src/metrics/cls.ts +++ b/packages/browser-utils/src/metrics/cls.ts @@ -1,4 +1,4 @@ -import type { SpanAttributes } from '@sentry/core'; +import type { Client, SpanAttributes } from '@sentry/core'; import { browserPerformanceTimeOrigin, getCurrentScope, @@ -24,7 +24,7 @@ import { listenForWebVitalReportEvents, msToSec, startStandaloneWebVitalSpan, su * Once either of these events triggers, the CLS value is sent as a standalone span and we stop * measuring CLS. */ -export function trackClsAsStandaloneSpan(): void { +export function trackClsAsStandaloneSpan(client: Client): void { let standaloneCLsValue = 0; let standaloneClsEntry: LayoutShift | undefined; @@ -41,7 +41,7 @@ export function trackClsAsStandaloneSpan(): void { standaloneClsEntry = entry; }, true); - listenForWebVitalReportEvents((reportEvent, pageloadSpanId) => { + listenForWebVitalReportEvents(client, (reportEvent, pageloadSpanId) => { sendStandaloneClsSpan(standaloneCLsValue, standaloneClsEntry, pageloadSpanId, reportEvent); cleanupClsHandler(); }); diff --git a/packages/browser-utils/src/metrics/lcp.ts b/packages/browser-utils/src/metrics/lcp.ts index dc98b2f8f2b1..6864a233466c 100644 --- a/packages/browser-utils/src/metrics/lcp.ts +++ b/packages/browser-utils/src/metrics/lcp.ts @@ -1,4 +1,4 @@ -import type { SpanAttributes } from '@sentry/core'; +import type { Client, SpanAttributes } from '@sentry/core'; import { browserPerformanceTimeOrigin, getCurrentScope, @@ -24,7 +24,7 @@ import { listenForWebVitalReportEvents, msToSec, startStandaloneWebVitalSpan, su * 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(): void { +export function trackLcpAsStandaloneSpan(client: Client): void { let standaloneLcpValue = 0; let standaloneLcpEntry: LargestContentfulPaint | undefined; @@ -41,7 +41,7 @@ export function trackLcpAsStandaloneSpan(): void { standaloneLcpEntry = entry; }, true); - listenForWebVitalReportEvents((reportEvent, pageloadSpanId) => { + listenForWebVitalReportEvents(client, (reportEvent, pageloadSpanId) => { _sendStandaloneLcpSpan(standaloneLcpValue, standaloneLcpEntry, pageloadSpanId, reportEvent); cleanupLcpHandler(); }); diff --git a/packages/browser-utils/src/metrics/utils.ts b/packages/browser-utils/src/metrics/utils.ts index ce3da0d4f16d..e56d0ee98d42 100644 --- a/packages/browser-utils/src/metrics/utils.ts +++ b/packages/browser-utils/src/metrics/utils.ts @@ -1,13 +1,13 @@ -import type { Integration, SentrySpan, Span, SpanAttributes, SpanTimeInput, StartSpanOptions } from '@sentry/core'; -import { - getActiveSpan, - getClient, - getCurrentScope, - getRootSpan, - spanToJSON, - startInactiveSpan, - withActiveSpan, +import type { + Client, + Integration, + SentrySpan, + Span, + SpanAttributes, + SpanTimeInput, + StartSpanOptions, } from '@sentry/core'; +import { getClient, getCurrentScope, spanToJSON, startInactiveSpan, withActiveSpan } from '@sentry/core'; import { WINDOW } from '../types'; import { onHidden } from './web-vitals/lib/onHidden'; @@ -205,6 +205,7 @@ export function supportsWebVital(entryType: 'layout-shift' | 'largest-contentful * - pageloadSpanId: the span id of the pageload span. This is used to link the web vital span to the pageload span. */ export function listenForWebVitalReportEvents( + client: Client, collectorCallback: (event: WebVitalReportEvent, pageloadSpanId: string) => void, ) { let pageloadSpanId: string | undefined; @@ -218,32 +219,20 @@ export function listenForWebVitalReportEvents( } onHidden(() => { - if (!collected) { - _runCollectorCallbackOnce('pagehide'); - } + _runCollectorCallbackOnce('pagehide'); }); - setTimeout(() => { - const client = getClient(); - if (!client) { - return; + const unsubscribeStartNavigation = client.on('beforeStartNavigationSpan', (_, options) => { + // we only want to collect LCP if we actually navigate. Redirects should be ignored. + if (!options?.isRedirect) { + _runCollectorCallbackOnce('navigation'); + unsubscribeStartNavigation?.(); + unsubscribeAfterStartPageLoadSpan?.(); } + }); - const unsubscribeStartNavigation = client.on('beforeStartNavigationSpan', (_, options) => { - // we only want to collect LCP if we actually navigate. Redirects should be ignored. - if (!options?.isRedirect) { - _runCollectorCallbackOnce('navigation'); - unsubscribeStartNavigation?.(); - } - }); - - const activeSpan = getActiveSpan(); - if (activeSpan) { - const rootSpan = getRootSpan(activeSpan); - const spanJSON = spanToJSON(rootSpan); - if (spanJSON.op === 'pageload') { - pageloadSpanId = rootSpan.spanContext().spanId; - } - } - }, 0); + const unsubscribeAfterStartPageLoadSpan = client.on('afterStartPageLoadSpan', span => { + pageloadSpanId = span.spanContext().spanId; + unsubscribeAfterStartPageLoadSpan?.(); + }); } diff --git a/packages/browser/src/tracing/browserTracingIntegration.ts b/packages/browser/src/tracing/browserTracingIntegration.ts index a1eb186d5c1e..f010030c47c3 100644 --- a/packages/browser/src/tracing/browserTracingIntegration.ts +++ b/packages/browser/src/tracing/browserTracingIntegration.ts @@ -440,6 +440,7 @@ export const browserTracingIntegration = ((_options: Partial { ) => void, ): () => void; + /** + * A hook for the browser tracing integrations to trigger after the pageload span was started. + * @returns {() => void} A function that, when executed, removes the registered callback. + */ + public on(hook: 'afterStartPageLoadSpan', callback: (span: Span) => void): () => void; + /** * A hook for triggering right before a navigation span is started. * @returns {() => void} A function that, when executed, removes the registered callback. @@ -791,6 +797,11 @@ export abstract class Client { traceOptions?: { sentryTrace?: string | undefined; baggage?: string | undefined }, ): void; + /** + * Emit a hook event for browser tracing integrations to trigger aafter the pageload span was started. + */ + public emit(hook: 'afterStartPageLoadSpan', span: Span): void; + /** * Emit a hook event for triggering right before a navigation span is started. */