Skip to content

Commit 2912474

Browse files
authored
feat(browser): Add afterStartPageloadSpan hook to improve spanId assignment on web vital spans (#16893)
Our standalone web vital spans need to include the span id of the `pageload` span s.t. we can correctly assign the web vital value to the respective pageload span in the Sentry backend. In the previous implementation, we'd simply wait for a tick after tracking the web vitals, get the active root span and take its spanId if it was a pageload span. However, this relies on the assumption that the pageload span is indeed started immediately. By default this happens but users can always deactivate default behaviour and call `startBrowserTracingPageloadSpan` whenever they want (for example, in a custom `browserTracingIntegration`). Furthermore, this "wait for a tick" logic always bugged me and was only done because `getClient()` would not yet return the already initialized client. This change now makes the pageload spanId retrieval more robust by - adding and listening to a new SDK lifecycle hook: `afterStartPageloadSpan`. This callback fires as soon as the pageload span is actually started and available. - passing the `client` from the `browserTracingIntegration`'s `setup` hook into the web vital listening function so that we can remove the `setTimeout(_, 0)` logic.
1 parent 101b4f2 commit 2912474

File tree

6 files changed

+52
-43
lines changed

6 files changed

+52
-43
lines changed

packages/browser-utils/src/metrics/browserMetrics.ts

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/* eslint-disable max-lines */
2-
import type { Measurements, Span, SpanAttributes, SpanAttributeValue, StartSpanOptions } from '@sentry/core';
2+
import type { Client, Measurements, Span, SpanAttributes, SpanAttributeValue, StartSpanOptions } from '@sentry/core';
33
import {
44
browserPerformanceTimeOrigin,
55
getActiveSpan,
@@ -83,6 +83,7 @@ let _clsEntry: LayoutShift | undefined;
8383
interface StartTrackingWebVitalsOptions {
8484
recordClsStandaloneSpans: boolean;
8585
recordLcpStandaloneSpans: boolean;
86+
client: Client;
8687
}
8788

8889
/**
@@ -94,6 +95,7 @@ interface StartTrackingWebVitalsOptions {
9495
export function startTrackingWebVitals({
9596
recordClsStandaloneSpans,
9697
recordLcpStandaloneSpans,
98+
client,
9799
}: StartTrackingWebVitalsOptions): () => void {
98100
const performance = getBrowserPerformanceAPI();
99101
if (performance && browserPerformanceTimeOrigin()) {
@@ -102,9 +104,9 @@ export function startTrackingWebVitals({
102104
WINDOW.performance.mark('sentry-tracing-init');
103105
}
104106
const fidCleanupCallback = _trackFID();
105-
const lcpCleanupCallback = recordLcpStandaloneSpans ? trackLcpAsStandaloneSpan() : _trackLCP();
107+
const lcpCleanupCallback = recordLcpStandaloneSpans ? trackLcpAsStandaloneSpan(client) : _trackLCP();
106108
const ttfbCleanupCallback = _trackTtfb();
107-
const clsCleanupCallback = recordClsStandaloneSpans ? trackClsAsStandaloneSpan() : _trackCLS();
109+
const clsCleanupCallback = recordClsStandaloneSpans ? trackClsAsStandaloneSpan(client) : _trackCLS();
108110

109111
return (): void => {
110112
fidCleanupCallback();

packages/browser-utils/src/metrics/cls.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import type { SpanAttributes } from '@sentry/core';
1+
import type { Client, SpanAttributes } from '@sentry/core';
22
import {
33
browserPerformanceTimeOrigin,
44
getCurrentScope,
@@ -24,7 +24,7 @@ import { listenForWebVitalReportEvents, msToSec, startStandaloneWebVitalSpan, su
2424
* Once either of these events triggers, the CLS value is sent as a standalone span and we stop
2525
* measuring CLS.
2626
*/
27-
export function trackClsAsStandaloneSpan(): void {
27+
export function trackClsAsStandaloneSpan(client: Client): void {
2828
let standaloneCLsValue = 0;
2929
let standaloneClsEntry: LayoutShift | undefined;
3030

@@ -41,7 +41,7 @@ export function trackClsAsStandaloneSpan(): void {
4141
standaloneClsEntry = entry;
4242
}, true);
4343

44-
listenForWebVitalReportEvents((reportEvent, pageloadSpanId) => {
44+
listenForWebVitalReportEvents(client, (reportEvent, pageloadSpanId) => {
4545
sendStandaloneClsSpan(standaloneCLsValue, standaloneClsEntry, pageloadSpanId, reportEvent);
4646
cleanupClsHandler();
4747
});

packages/browser-utils/src/metrics/lcp.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import type { SpanAttributes } from '@sentry/core';
1+
import type { Client, SpanAttributes } from '@sentry/core';
22
import {
33
browserPerformanceTimeOrigin,
44
getCurrentScope,
@@ -24,7 +24,7 @@ import { listenForWebVitalReportEvents, msToSec, startStandaloneWebVitalSpan, su
2424
* Once either of these events triggers, the LCP value is sent as a standalone span and we stop
2525
* measuring LCP for subsequent routes.
2626
*/
27-
export function trackLcpAsStandaloneSpan(): void {
27+
export function trackLcpAsStandaloneSpan(client: Client): void {
2828
let standaloneLcpValue = 0;
2929
let standaloneLcpEntry: LargestContentfulPaint | undefined;
3030

@@ -41,7 +41,7 @@ export function trackLcpAsStandaloneSpan(): void {
4141
standaloneLcpEntry = entry;
4242
}, true);
4343

44-
listenForWebVitalReportEvents((reportEvent, pageloadSpanId) => {
44+
listenForWebVitalReportEvents(client, (reportEvent, pageloadSpanId) => {
4545
_sendStandaloneLcpSpan(standaloneLcpValue, standaloneLcpEntry, pageloadSpanId, reportEvent);
4646
cleanupLcpHandler();
4747
});

packages/browser-utils/src/metrics/utils.ts

Lines changed: 22 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,13 @@
1-
import type { Integration, SentrySpan, Span, SpanAttributes, SpanTimeInput, StartSpanOptions } from '@sentry/core';
2-
import {
3-
getActiveSpan,
4-
getClient,
5-
getCurrentScope,
6-
getRootSpan,
7-
spanToJSON,
8-
startInactiveSpan,
9-
withActiveSpan,
1+
import type {
2+
Client,
3+
Integration,
4+
SentrySpan,
5+
Span,
6+
SpanAttributes,
7+
SpanTimeInput,
8+
StartSpanOptions,
109
} from '@sentry/core';
10+
import { getClient, getCurrentScope, spanToJSON, startInactiveSpan, withActiveSpan } from '@sentry/core';
1111
import { WINDOW } from '../types';
1212
import { onHidden } from './web-vitals/lib/onHidden';
1313

@@ -205,6 +205,7 @@ export function supportsWebVital(entryType: 'layout-shift' | 'largest-contentful
205205
* - pageloadSpanId: the span id of the pageload span. This is used to link the web vital span to the pageload span.
206206
*/
207207
export function listenForWebVitalReportEvents(
208+
client: Client,
208209
collectorCallback: (event: WebVitalReportEvent, pageloadSpanId: string) => void,
209210
) {
210211
let pageloadSpanId: string | undefined;
@@ -218,32 +219,20 @@ export function listenForWebVitalReportEvents(
218219
}
219220

220221
onHidden(() => {
221-
if (!collected) {
222-
_runCollectorCallbackOnce('pagehide');
223-
}
222+
_runCollectorCallbackOnce('pagehide');
224223
});
225224

226-
setTimeout(() => {
227-
const client = getClient();
228-
if (!client) {
229-
return;
225+
const unsubscribeStartNavigation = client.on('beforeStartNavigationSpan', (_, options) => {
226+
// we only want to collect LCP if we actually navigate. Redirects should be ignored.
227+
if (!options?.isRedirect) {
228+
_runCollectorCallbackOnce('navigation');
229+
unsubscribeStartNavigation?.();
230+
unsubscribeAfterStartPageLoadSpan?.();
230231
}
232+
});
231233

232-
const unsubscribeStartNavigation = client.on('beforeStartNavigationSpan', (_, options) => {
233-
// we only want to collect LCP if we actually navigate. Redirects should be ignored.
234-
if (!options?.isRedirect) {
235-
_runCollectorCallbackOnce('navigation');
236-
unsubscribeStartNavigation?.();
237-
}
238-
});
239-
240-
const activeSpan = getActiveSpan();
241-
if (activeSpan) {
242-
const rootSpan = getRootSpan(activeSpan);
243-
const spanJSON = spanToJSON(rootSpan);
244-
if (spanJSON.op === 'pageload') {
245-
pageloadSpanId = rootSpan.spanContext().spanId;
246-
}
247-
}
248-
}, 0);
234+
const unsubscribeAfterStartPageLoadSpan = client.on('afterStartPageLoadSpan', span => {
235+
pageloadSpanId = span.spanContext().spanId;
236+
unsubscribeAfterStartPageLoadSpan?.();
237+
});
249238
}

packages/browser/src/tracing/browserTracingIntegration.ts

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -440,6 +440,7 @@ export const browserTracingIntegration = ((_options: Partial<BrowserTracingOptio
440440
_collectWebVitals = startTrackingWebVitals({
441441
recordClsStandaloneSpans: enableStandaloneClsSpans || false,
442442
recordLcpStandaloneSpans: enableStandaloneLcpSpans || false,
443+
client,
443444
});
444445

445446
if (enableInp) {
@@ -644,7 +645,13 @@ export function startBrowserTracingPageLoadSpan(
644645
client.emit('startPageLoadSpan', spanOptions, traceOptions);
645646
getCurrentScope().setTransactionName(spanOptions.name);
646647

647-
return getActiveIdleSpan(client);
648+
const pageloadSpan = getActiveIdleSpan(client);
649+
650+
if (pageloadSpan) {
651+
client.emit('afterStartPageLoadSpan', pageloadSpan);
652+
}
653+
654+
return pageloadSpan;
648655
}
649656

650657
/**

packages/core/src/client.ts

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -603,6 +603,12 @@ export abstract class Client<O extends ClientOptions = ClientOptions> {
603603
) => void,
604604
): () => void;
605605

606+
/**
607+
* A hook for the browser tracing integrations to trigger after the pageload span was started.
608+
* @returns {() => void} A function that, when executed, removes the registered callback.
609+
*/
610+
public on(hook: 'afterStartPageLoadSpan', callback: (span: Span) => void): () => void;
611+
606612
/**
607613
* A hook for triggering right before a navigation span is started.
608614
* @returns {() => void} A function that, when executed, removes the registered callback.
@@ -791,6 +797,11 @@ export abstract class Client<O extends ClientOptions = ClientOptions> {
791797
traceOptions?: { sentryTrace?: string | undefined; baggage?: string | undefined },
792798
): void;
793799

800+
/**
801+
* Emit a hook event for browser tracing integrations to trigger aafter the pageload span was started.
802+
*/
803+
public emit(hook: 'afterStartPageLoadSpan', span: Span): void;
804+
794805
/**
795806
* Emit a hook event for triggering right before a navigation span is started.
796807
*/

0 commit comments

Comments
 (0)