Skip to content

Commit e9e0eb2

Browse files
committed
ref(browser): Add CLS/LCP report event attribute to standalone spans
1 parent affebb4 commit e9e0eb2

File tree

3 files changed

+39
-26
lines changed

3 files changed

+39
-26
lines changed

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

Lines changed: 20 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -16,8 +16,10 @@ import {
1616
} from '@sentry/core';
1717
import { DEBUG_BUILD } from '../debug-build';
1818
import { addClsInstrumentationHandler } from './instrument';
19+
import type { WebVitalReportEvent } from './utils';
1920
import { msToSec, startStandaloneWebVitalSpan } from './utils';
2021
import { onHidden } from './web-vitals/lib/onHidden';
22+
import { runOnce } from './web-vitals/lib/runOnce';
2123

2224
/**
2325
* Starts tracking the Cumulative Layout Shift on the current page and collects the value once
@@ -37,16 +39,13 @@ export function trackClsAsStandaloneSpan(): void {
3739
return;
3840
}
3941

40-
let sentSpan = false;
41-
function _collectClsOnce() {
42-
if (sentSpan) {
43-
return;
44-
}
45-
sentSpan = true;
46-
if (pageloadSpanId) {
47-
sendStandaloneClsSpan(standaloneCLsValue, standaloneClsEntry, pageloadSpanId);
48-
}
49-
cleanupClsHandler();
42+
function _collectClsOnce(reportEvent: WebVitalReportEvent) {
43+
runOnce(() => {
44+
if (pageloadSpanId) {
45+
sendStandaloneClsSpan(standaloneCLsValue, standaloneClsEntry, pageloadSpanId, reportEvent);
46+
}
47+
cleanupClsHandler();
48+
});
5049
}
5150

5251
const cleanupClsHandler = addClsInstrumentationHandler(({ metric }) => {
@@ -59,7 +58,7 @@ export function trackClsAsStandaloneSpan(): void {
5958
}, true);
6059

6160
onHidden(() => {
62-
_collectClsOnce();
61+
_collectClsOnce('pagehide');
6362
});
6463

6564
// Since the call chain of this function is synchronous and evaluates before the SDK client is created,
@@ -73,9 +72,9 @@ export function trackClsAsStandaloneSpan(): void {
7372
}
7473

7574
const unsubscribeStartNavigation = client.on('beforeStartNavigationSpan', (_, options) => {
76-
// we only want to collect LCP if we actually navigate. Redirects should be ignored.
75+
// we only want to collect CLS if we actually navigate. Redirects should be ignored.
7776
if (!options?.isRedirect) {
78-
_collectClsOnce();
77+
_collectClsOnce('navigation');
7978
unsubscribeStartNavigation?.();
8079
}
8180
});
@@ -91,7 +90,12 @@ export function trackClsAsStandaloneSpan(): void {
9190
}, 0);
9291
}
9392

94-
function sendStandaloneClsSpan(clsValue: number, entry: LayoutShift | undefined, pageloadSpanId: string) {
93+
function sendStandaloneClsSpan(
94+
clsValue: number,
95+
entry: LayoutShift | undefined,
96+
pageloadSpanId: string,
97+
reportEvent: WebVitalReportEvent,
98+
) {
9599
DEBUG_BUILD && logger.log(`Sending CLS span (${clsValue})`);
96100

97101
const startTime = msToSec((browserPerformanceTimeOrigin() || 0) + (entry?.startTime || 0));
@@ -105,6 +109,8 @@ function sendStandaloneClsSpan(clsValue: number, entry: LayoutShift | undefined,
105109
[SEMANTIC_ATTRIBUTE_EXCLUSIVE_TIME]: entry?.duration || 0,
106110
// attach the pageload span id to the CLS span so that we can link them in the UI
107111
'sentry.pageload.span_id': pageloadSpanId,
112+
// describes what triggered the web vital to be reported
113+
'sentry.report_event': reportEvent,
108114
};
109115

110116
// Add CLS sources as span attributes to help with debugging layout shifts

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

Lines changed: 14 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -16,8 +16,10 @@ import {
1616
} from '@sentry/core';
1717
import { DEBUG_BUILD } from '../debug-build';
1818
import { addLcpInstrumentationHandler } from './instrument';
19+
import type { WebVitalReportEvent } from './utils';
1920
import { msToSec, startStandaloneWebVitalSpan } from './utils';
2021
import { onHidden } from './web-vitals/lib/onHidden';
22+
import { runOnce } from './web-vitals/lib/runOnce';
2123

2224
/**
2325
* Starts tracking the Largest Contentful Paint on the current page and collects the value once
@@ -37,16 +39,13 @@ export function trackLcpAsStandaloneSpan(): void {
3739
return;
3840
}
3941

40-
let sentSpan = false;
41-
function _collectLcpOnce() {
42-
if (sentSpan) {
43-
return;
44-
}
45-
sentSpan = true;
46-
if (pageloadSpanId) {
47-
_sendStandaloneLcpSpan(standaloneLcpValue, standaloneLcpEntry, pageloadSpanId);
48-
}
49-
cleanupLcpHandler();
42+
function _collectLcpOnce(reportEvent: WebVitalReportEvent) {
43+
runOnce(() => {
44+
if (pageloadSpanId) {
45+
_sendStandaloneLcpSpan(standaloneLcpValue, standaloneLcpEntry, pageloadSpanId, reportEvent);
46+
}
47+
cleanupLcpHandler();
48+
});
5049
}
5150

5251
const cleanupLcpHandler = addLcpInstrumentationHandler(({ metric }) => {
@@ -59,7 +58,7 @@ export function trackLcpAsStandaloneSpan(): void {
5958
}, true);
6059

6160
onHidden(() => {
62-
_collectLcpOnce();
61+
_collectLcpOnce('pagehide');
6362
});
6463

6564
// Since the call chain of this function is synchronous and evaluates before the SDK client is created,
@@ -75,7 +74,7 @@ export function trackLcpAsStandaloneSpan(): void {
7574
const unsubscribeStartNavigation = client.on('beforeStartNavigationSpan', (_, options) => {
7675
// we only want to collect LCP if we actually navigate. Redirects should be ignored.
7776
if (!options?.isRedirect) {
78-
_collectLcpOnce();
77+
_collectLcpOnce('navigation');
7978
unsubscribeStartNavigation?.();
8079
}
8180
});
@@ -98,6 +97,7 @@ export function _sendStandaloneLcpSpan(
9897
lcpValue: number,
9998
entry: LargestContentfulPaint | undefined,
10099
pageloadSpanId: string,
100+
reportEvent: WebVitalReportEvent,
101101
) {
102102
DEBUG_BUILD && logger.log(`Sending LCP span (${lcpValue})`);
103103

@@ -112,6 +112,8 @@ export function _sendStandaloneLcpSpan(
112112
[SEMANTIC_ATTRIBUTE_EXCLUSIVE_TIME]: 0, // LCP is a point-in-time metric
113113
// attach the pageload span id to the LCP span so that we can link them in the UI
114114
'sentry.pageload.span_id': pageloadSpanId,
115+
// describes what triggered the web vital to be reported
116+
'sentry.report_event': reportEvent,
115117
};
116118

117119
if (entry) {

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

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@ import type { Integration, SentrySpan, Span, SpanAttributes, SpanTimeInput, Star
22
import { getClient, getCurrentScope, spanToJSON, startInactiveSpan, withActiveSpan } from '@sentry/core';
33
import { WINDOW } from '../types';
44

5+
export type WebVitalReportEvent = 'pagehide' | 'navigation';
6+
57
/**
68
* Checks if a given value is a valid measurement value.
79
*/
@@ -168,3 +170,6 @@ export function extractNetworkProtocol(nextHopProtocol: string): { name: string;
168170
}
169171
return { name, version };
170172
}
173+
174+
type ReportEvent = 'pagehide' | 'navigation';
175+
function createReportOnceHandler();

0 commit comments

Comments
 (0)