Skip to content

Commit 452dd23

Browse files
committed
more extraction
1 parent e6e300b commit 452dd23

File tree

3 files changed

+86
-95
lines changed

3 files changed

+86
-95
lines changed

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

Lines changed: 4 additions & 47 deletions
Original file line numberDiff line numberDiff line change
@@ -1,25 +1,19 @@
11
import type { SpanAttributes } from '@sentry/core';
22
import {
33
browserPerformanceTimeOrigin,
4-
getActiveSpan,
5-
getClient,
64
getCurrentScope,
7-
getRootSpan,
85
htmlTreeAsString,
96
logger,
107
SEMANTIC_ATTRIBUTE_EXCLUSIVE_TIME,
118
SEMANTIC_ATTRIBUTE_SENTRY_MEASUREMENT_UNIT,
129
SEMANTIC_ATTRIBUTE_SENTRY_MEASUREMENT_VALUE,
1310
SEMANTIC_ATTRIBUTE_SENTRY_OP,
1411
SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN,
15-
spanToJSON,
1612
} from '@sentry/core';
1713
import { DEBUG_BUILD } from '../debug-build';
1814
import { addClsInstrumentationHandler } from './instrument';
1915
import type { WebVitalReportEvent } from './utils';
20-
import { msToSec, startStandaloneWebVitalSpan, supportsWebVital } from './utils';
21-
import { onHidden } from './web-vitals/lib/onHidden';
22-
import { runOnce } from './web-vitals/lib/runOnce';
16+
import { listenForWebVitalReportEvents, msToSec, startStandaloneWebVitalSpan, supportsWebVital } from './utils';
2317

2418
/**
2519
* Starts tracking the Cumulative Layout Shift on the current page and collects the value once
@@ -33,21 +27,11 @@ import { runOnce } from './web-vitals/lib/runOnce';
3327
export function trackClsAsStandaloneSpan(): void {
3428
let standaloneCLsValue = 0;
3529
let standaloneClsEntry: LayoutShift | undefined;
36-
let pageloadSpanId: string | undefined;
3730

3831
if (!supportsWebVital('layout-shift')) {
3932
return;
4033
}
4134

42-
function _collectClsOnce(reportEvent: WebVitalReportEvent) {
43-
runOnce(() => {
44-
if (pageloadSpanId) {
45-
sendStandaloneClsSpan(standaloneCLsValue, standaloneClsEntry, pageloadSpanId, reportEvent);
46-
}
47-
cleanupClsHandler();
48-
});
49-
}
50-
5135
const cleanupClsHandler = addClsInstrumentationHandler(({ metric }) => {
5236
const entry = metric.entries[metric.entries.length - 1] as LayoutShift | undefined;
5337
if (!entry) {
@@ -57,37 +41,10 @@ export function trackClsAsStandaloneSpan(): void {
5741
standaloneClsEntry = entry;
5842
}, true);
5943

60-
onHidden(() => {
61-
_collectClsOnce('pagehide');
44+
listenForWebVitalReportEvents((reportEvent, pageloadSpanId) => {
45+
sendStandaloneClsSpan(standaloneCLsValue, standaloneClsEntry, pageloadSpanId, reportEvent);
46+
cleanupClsHandler();
6247
});
63-
64-
// Since the call chain of this function is synchronous and evaluates before the SDK client is created,
65-
// we need to wait with subscribing to a client hook until the client is created. Therefore, we defer
66-
// to the next tick after the SDK setup.
67-
setTimeout(() => {
68-
const client = getClient();
69-
70-
if (!client) {
71-
return;
72-
}
73-
74-
const unsubscribeStartNavigation = client.on('beforeStartNavigationSpan', (_, options) => {
75-
// we only want to collect CLS if we actually navigate. Redirects should be ignored.
76-
if (!options?.isRedirect) {
77-
_collectClsOnce('navigation');
78-
unsubscribeStartNavigation?.();
79-
}
80-
});
81-
82-
const activeSpan = getActiveSpan();
83-
if (activeSpan) {
84-
const rootSpan = getRootSpan(activeSpan);
85-
const spanJSON = spanToJSON(rootSpan);
86-
if (spanJSON.op === 'pageload') {
87-
pageloadSpanId = rootSpan.spanContext().spanId;
88-
}
89-
}
90-
}, 0);
9148
}
9249

9350
function sendStandaloneClsSpan(

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

Lines changed: 4 additions & 47 deletions
Original file line numberDiff line numberDiff line change
@@ -1,25 +1,19 @@
11
import type { SpanAttributes } from '@sentry/core';
22
import {
33
browserPerformanceTimeOrigin,
4-
getActiveSpan,
5-
getClient,
64
getCurrentScope,
7-
getRootSpan,
85
htmlTreeAsString,
96
logger,
107
SEMANTIC_ATTRIBUTE_EXCLUSIVE_TIME,
118
SEMANTIC_ATTRIBUTE_SENTRY_MEASUREMENT_UNIT,
129
SEMANTIC_ATTRIBUTE_SENTRY_MEASUREMENT_VALUE,
1310
SEMANTIC_ATTRIBUTE_SENTRY_OP,
1411
SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN,
15-
spanToJSON,
1612
} from '@sentry/core';
1713
import { DEBUG_BUILD } from '../debug-build';
1814
import { addLcpInstrumentationHandler } from './instrument';
1915
import type { WebVitalReportEvent } from './utils';
20-
import { msToSec, startStandaloneWebVitalSpan, supportsWebVital } from './utils';
21-
import { onHidden } from './web-vitals/lib/onHidden';
22-
import { runOnce } from './web-vitals/lib/runOnce';
16+
import { listenForWebVitalReportEvents, msToSec, startStandaloneWebVitalSpan, supportsWebVital } from './utils';
2317

2418
/**
2519
* Starts tracking the Largest Contentful Paint on the current page and collects the value once
@@ -33,21 +27,11 @@ import { runOnce } from './web-vitals/lib/runOnce';
3327
export function trackLcpAsStandaloneSpan(): void {
3428
let standaloneLcpValue = 0;
3529
let standaloneLcpEntry: LargestContentfulPaint | undefined;
36-
let pageloadSpanId: string | undefined;
3730

3831
if (!supportsWebVital('largest-contentful-paint')) {
3932
return;
4033
}
4134

42-
function _collectLcpOnce(reportEvent: WebVitalReportEvent) {
43-
runOnce(() => {
44-
if (pageloadSpanId) {
45-
_sendStandaloneLcpSpan(standaloneLcpValue, standaloneLcpEntry, pageloadSpanId, reportEvent);
46-
}
47-
cleanupLcpHandler();
48-
});
49-
}
50-
5135
const cleanupLcpHandler = addLcpInstrumentationHandler(({ metric }) => {
5236
const entry = metric.entries[metric.entries.length - 1] as LargestContentfulPaint | undefined;
5337
if (!entry) {
@@ -57,37 +41,10 @@ export function trackLcpAsStandaloneSpan(): void {
5741
standaloneLcpEntry = entry;
5842
}, true);
5943

60-
onHidden(() => {
61-
_collectLcpOnce('pagehide');
44+
listenForWebVitalReportEvents((reportEvent, pageloadSpanId) => {
45+
_sendStandaloneLcpSpan(standaloneLcpValue, standaloneLcpEntry, pageloadSpanId, reportEvent);
46+
cleanupLcpHandler();
6247
});
63-
64-
// Since the call chain of this function is synchronous and evaluates before the SDK client is created,
65-
// we need to wait with subscribing to a client hook until the client is created. Therefore, we defer
66-
// to the next tick after the SDK setup.
67-
setTimeout(() => {
68-
const client = getClient();
69-
70-
if (!client) {
71-
return;
72-
}
73-
74-
const unsubscribeStartNavigation = client.on('beforeStartNavigationSpan', (_, options) => {
75-
// we only want to collect LCP if we actually navigate. Redirects should be ignored.
76-
if (!options?.isRedirect) {
77-
_collectLcpOnce('navigation');
78-
unsubscribeStartNavigation?.();
79-
}
80-
});
81-
82-
const activeSpan = getActiveSpan();
83-
if (activeSpan) {
84-
const rootSpan = getRootSpan(activeSpan);
85-
const spanJSON = spanToJSON(rootSpan);
86-
if (spanJSON.op === 'pageload') {
87-
pageloadSpanId = rootSpan.spanContext().spanId;
88-
}
89-
}
90-
}, 0);
9148
}
9249

9350
/**

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

Lines changed: 78 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,16 @@
11
import type { Integration, SentrySpan, Span, SpanAttributes, SpanTimeInput, StartSpanOptions } from '@sentry/core';
2-
import { getClient, getCurrentScope, spanToJSON, startInactiveSpan, withActiveSpan } from '@sentry/core';
2+
import {
3+
getActiveSpan,
4+
getClient,
5+
getCurrentScope,
6+
getRootSpan,
7+
spanToJSON,
8+
startInactiveSpan,
9+
withActiveSpan,
10+
} from '@sentry/core';
311
import { WINDOW } from '../types';
12+
import { onHidden } from './web-vitals/lib/onHidden';
13+
import { runOnce } from './web-vitals/lib/runOnce';
414

515
export type WebVitalReportEvent = 'pagehide' | 'navigation';
616

@@ -181,3 +191,70 @@ export function supportsWebVital(entryType: 'layout-shift' | 'largest-contentful
181191
return false;
182192
}
183193
}
194+
195+
/**
196+
* Listens for events on which we want to collect a previously accumulated web vital value.
197+
* Currently, this includes:
198+
*
199+
* - pagehide (i.e. user minimizes browser window, hides tab, etc)
200+
* - soft navigation (we only care about the vital of the initially loaded route)
201+
*
202+
* As a "side-effect", this function will also collect the span id of the pageload span.
203+
*
204+
* @param collectorCallback the callback to be called when the first of these events is triggered. Parameters:
205+
* - event: the event that triggered the reporting of the web vital value.
206+
* - pageloadSpanId: the span id of the pageload span. This is used to link the web vital span to the pageload span.
207+
*/
208+
export function listenForWebVitalReportEvents(
209+
collectorCallback: (event: WebVitalReportEvent, pageloadSpanId: string) => void,
210+
) {
211+
let pageloadSpanId: string | undefined;
212+
let triggeredReportEvent: WebVitalReportEvent | undefined;
213+
let collected = false;
214+
215+
function _runCollectorCallbackOnce() {
216+
if (!collected && triggeredReportEvent) {
217+
collectorCallback(triggeredReportEvent, pageloadSpanId ?? 'unknown');
218+
collected = true;
219+
}
220+
}
221+
222+
onHidden(() => {
223+
if (!collected) {
224+
triggeredReportEvent = 'pagehide';
225+
_runCollectorCallbackOnce();
226+
}
227+
});
228+
229+
setTimeout(() => {
230+
const client = getClient();
231+
if (!client) {
232+
return;
233+
}
234+
235+
const unsubscribeStartNavigation = client.on('beforeStartNavigationSpan', (_, options) => {
236+
// we only want to collect LCP if we actually navigate. Redirects should be ignored.
237+
if (!options?.isRedirect) {
238+
triggeredReportEvent = 'navigation';
239+
_runCollectorCallbackOnce();
240+
unsubscribeStartNavigation?.();
241+
}
242+
});
243+
244+
client.on('spanEnd', span => {
245+
const spanJSON = spanToJSON(span);
246+
if (spanJSON.op === 'pageload') {
247+
pageloadSpanId = span.spanContext().spanId;
248+
}
249+
});
250+
251+
const activeSpan = getActiveSpan();
252+
if (activeSpan) {
253+
const rootSpan = getRootSpan(activeSpan);
254+
const spanJSON = spanToJSON(rootSpan);
255+
if (spanJSON.op === 'pageload') {
256+
pageloadSpanId = rootSpan.spanContext().spanId;
257+
}
258+
}
259+
}, 0);
260+
}

0 commit comments

Comments
 (0)