Skip to content

Commit a8d733f

Browse files
committed
add new cls file, oops
1 parent fb0086d commit a8d733f

File tree

1 file changed

+114
-0
lines changed
  • packages/browser-utils/src/metrics

1 file changed

+114
-0
lines changed
+114
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,114 @@
1+
import {
2+
SEMANTIC_ATTRIBUTE_EXCLUSIVE_TIME,
3+
SEMANTIC_ATTRIBUTE_SENTRY_MEASUREMENT_UNIT,
4+
SEMANTIC_ATTRIBUTE_SENTRY_MEASUREMENT_VALUE,
5+
SEMANTIC_ATTRIBUTE_SENTRY_OP,
6+
SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN,
7+
getClient,
8+
getCurrentScope,
9+
} from '@sentry/core';
10+
import type { SpanAttributes } from '@sentry/types';
11+
import { browserPerformanceTimeOrigin, dropUndefinedKeys, htmlTreeAsString, logger } from '@sentry/utils';
12+
import { DEBUG_BUILD } from '../debug-build';
13+
import { addClsInstrumentationHandler } from './instrument';
14+
import { msToSec, startStandaloneWebVitalSpan } from './utils';
15+
import { onHidden } from './web-vitals/lib/onHidden';
16+
17+
/**
18+
* Starts tracking the Cumulative Layout Shift on the current page and collects the value once
19+
*
20+
* - the page visibility is hidden
21+
* - a navigation span is started (to stop CLS measurement for SPA soft navigations)
22+
*
23+
* Once either of these events triggers, the CLS value is sent as a standalone span and we stop
24+
* measuring CLS.
25+
*/
26+
export function trackClsAsStandaloneSpan(): () => void {
27+
let standaloneCLsValue = 0;
28+
let standaloneClsEntry: LayoutShift | undefined;
29+
30+
// Cleanup for standalone span mode is handled in this function.
31+
// Returning a no-op for API compatibility with `_trackCLS` measurement mode (saves some bytes)
32+
const cleanupNoop = () => undefined;
33+
34+
if (!supportsLayoutShift()) {
35+
return cleanupNoop;
36+
}
37+
38+
let sentSpan = false;
39+
function _collectClsOnce() {
40+
if (sentSpan) {
41+
return;
42+
}
43+
sendStandaloneClsSpan(standaloneCLsValue, standaloneClsEntry);
44+
cleanupClsHandler();
45+
sentSpan = true;
46+
}
47+
48+
const cleanupClsHandler = addClsInstrumentationHandler(({ metric }) => {
49+
const entry = metric.entries[metric.entries.length - 1] as LayoutShift | undefined;
50+
if (!entry) {
51+
return;
52+
}
53+
standaloneCLsValue = metric.value;
54+
standaloneClsEntry = entry;
55+
}, true);
56+
57+
// use pagehide event from web-vitals
58+
onHidden(() => {
59+
_collectClsOnce();
60+
});
61+
62+
// Since the call chain of this function is synchronous and evaluates before the SDK client is created,
63+
// we need to wait with subscribing to a client hook until the client is created. Therefore, we defer
64+
// to the next tick after the SDK setup.
65+
setTimeout(() => {
66+
const unsubscribe = getClient()?.on('startNavigationSpan', () => {
67+
_collectClsOnce();
68+
typeof unsubscribe === 'function' && unsubscribe();
69+
});
70+
}, 0);
71+
72+
return cleanupNoop;
73+
}
74+
75+
function sendStandaloneClsSpan(clsValue: number, entry: LayoutShift | undefined) {
76+
DEBUG_BUILD && logger.log(`Sending CLS span (${clsValue})`);
77+
78+
const startTime = msToSec(browserPerformanceTimeOrigin as number) + (entry?.startTime || 0);
79+
const duration = msToSec(entry?.duration || 0);
80+
const routeName = getCurrentScope().getScopeData().transactionName;
81+
82+
// TODO: Is this fine / does it provide any value? Alternatively, we can
83+
// - send the CLS source node as an attribute
84+
// - do nothing at all and ignore the source node
85+
const name = entry ? htmlTreeAsString(entry.sources[0]?.node) : 'Layout shift';
86+
87+
const attributes: SpanAttributes = dropUndefinedKeys({
88+
[SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.http.browser.cls',
89+
[SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'ui.webvital.cls',
90+
[SEMANTIC_ATTRIBUTE_EXCLUSIVE_TIME]: entry?.duration || 0,
91+
});
92+
93+
const span = startStandaloneWebVitalSpan({
94+
name,
95+
transaction: routeName,
96+
attributes,
97+
startTime,
98+
});
99+
100+
span?.addEvent('cls', {
101+
[SEMANTIC_ATTRIBUTE_SENTRY_MEASUREMENT_UNIT]: '',
102+
[SEMANTIC_ATTRIBUTE_SENTRY_MEASUREMENT_VALUE]: clsValue,
103+
});
104+
105+
span?.end(startTime + duration);
106+
}
107+
108+
function supportsLayoutShift(): boolean {
109+
try {
110+
return PerformanceObserver.supportedEntryTypes?.includes('layout-shift');
111+
} catch {
112+
return false;
113+
}
114+
}

0 commit comments

Comments
 (0)