Skip to content

Commit 1d6d216

Browse files
authored
fix(tracing): Record LCP and CLS on transaction finish (#7386)
1 parent 361c5a4 commit 1d6d216

File tree

7 files changed

+63
-17
lines changed

7 files changed

+63
-17
lines changed

packages/integration-tests/suites/tracing/metrics/web-vitals-lcp/test.ts

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,8 +17,6 @@ sentryTest('should capture a LCP vital with element details.', async ({ browserN
1717
const url = await getLocalTestPath({ testDir: __dirname });
1818
await page.goto(url);
1919

20-
// Force closure of LCP listener.
21-
await page.click('body');
2220
const eventData = await getFirstSentryEnvelopeRequest<Event>(page);
2321

2422
expect(eventData.measurements).toBeDefined();

packages/tracing/src/browser/browsertracing.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -169,6 +169,8 @@ export class BrowserTracing implements Integration {
169169
private _latestRouteName?: string;
170170
private _latestRouteSource?: TransactionSource;
171171

172+
private _collectWebVitals: () => void;
173+
172174
public constructor(_options?: Partial<BrowserTracingOptions>) {
173175
this.options = {
174176
...DEFAULT_BROWSER_TRACING_OPTIONS,
@@ -190,7 +192,7 @@ export class BrowserTracing implements Integration {
190192
this.options.tracePropagationTargets = _options.tracingOrigins;
191193
}
192194

193-
startTrackingWebVitals();
195+
this._collectWebVitals = startTrackingWebVitals();
194196
if (this.options.enableLongTask) {
195197
startTrackingLongTasks();
196198
}
@@ -311,6 +313,7 @@ export class BrowserTracing implements Integration {
311313
heartbeatInterval,
312314
);
313315
idleTransaction.registerBeforeFinishCallback(transaction => {
316+
this._collectWebVitals();
314317
addPerformanceEntries(transaction);
315318
});
316319

packages/tracing/src/browser/metrics/index.ts

Lines changed: 20 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -33,17 +33,30 @@ let _clsEntry: LayoutShift | undefined;
3333

3434
/**
3535
* Start tracking web vitals
36+
*
37+
* @returns A function that forces web vitals collection
3638
*/
37-
export function startTrackingWebVitals(): void {
39+
export function startTrackingWebVitals(): () => void {
3840
const performance = getBrowserPerformanceAPI();
3941
if (performance && browserPerformanceTimeOrigin) {
4042
if (performance.mark) {
4143
WINDOW.performance.mark('sentry-tracing-init');
4244
}
43-
_trackCLS();
44-
_trackLCP();
4545
_trackFID();
46+
const clsCallback = _trackCLS();
47+
const lcpCallback = _trackLCP();
48+
49+
return (): void => {
50+
if (clsCallback) {
51+
clsCallback();
52+
}
53+
if (lcpCallback) {
54+
lcpCallback();
55+
}
56+
};
4657
}
58+
59+
return () => undefined;
4760
}
4861

4962
/**
@@ -100,11 +113,11 @@ export function startTrackingInteractions(): void {
100113
}
101114

102115
/** Starts tracking the Cumulative Layout Shift on the current page. */
103-
function _trackCLS(): void {
116+
function _trackCLS(): ReturnType<typeof onCLS> {
104117
// See:
105118
// https://web.dev/evolving-cls/
106119
// https://web.dev/cls-web-tooling/
107-
onCLS(metric => {
120+
return onCLS(metric => {
108121
const entry = metric.entries.pop();
109122
if (!entry) {
110123
return;
@@ -117,8 +130,8 @@ function _trackCLS(): void {
117130
}
118131

119132
/** Starts tracking the Largest Contentful Paint on the current page. */
120-
function _trackLCP(): void {
121-
onLCP(metric => {
133+
function _trackLCP(): ReturnType<typeof onLCP> {
134+
return onLCP(metric => {
122135
const entry = metric.entries.pop();
123136
if (!entry) {
124137
return;

packages/tracing/src/browser/web-vitals/getCLS.ts

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ import { bindReporter } from './lib/bindReporter';
1818
import { initMetric } from './lib/initMetric';
1919
import { observe } from './lib/observe';
2020
import { onHidden } from './lib/onHidden';
21-
import type { CLSMetric, ReportCallback } from './types';
21+
import type { CLSMetric, ReportCallback, StopListening } from './types';
2222

2323
/**
2424
* Calculates the [CLS](https://web.dev/cls/) value for the current page and
@@ -41,7 +41,7 @@ import type { CLSMetric, ReportCallback } from './types';
4141
* hidden. As a result, the `callback` function might be called multiple times
4242
* during the same page load._
4343
*/
44-
export const onCLS = (onReport: ReportCallback): void => {
44+
export const onCLS = (onReport: ReportCallback): StopListening | undefined => {
4545
const metric = initMetric('CLS', 0);
4646
let report: ReturnType<typeof bindReporter>;
4747

@@ -89,9 +89,15 @@ export const onCLS = (onReport: ReportCallback): void => {
8989
if (po) {
9090
report = bindReporter(onReport, metric);
9191

92-
onHidden(() => {
92+
const stopListening = (): void => {
9393
handleEntries(po.takeRecords() as CLSMetric['entries']);
9494
report(true);
95-
});
95+
};
96+
97+
onHidden(stopListening);
98+
99+
return stopListening;
96100
}
101+
102+
return;
97103
};

packages/tracing/src/browser/web-vitals/getLCP.ts

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ import { getVisibilityWatcher } from './lib/getVisibilityWatcher';
2020
import { initMetric } from './lib/initMetric';
2121
import { observe } from './lib/observe';
2222
import { onHidden } from './lib/onHidden';
23-
import type { LCPMetric, ReportCallback } from './types';
23+
import type { LCPMetric, ReportCallback, StopListening } from './types';
2424

2525
const reportedMetricIDs: Record<string, boolean> = {};
2626

@@ -30,7 +30,7 @@ const reportedMetricIDs: Record<string, boolean> = {};
3030
* relevant `largest-contentful-paint` performance entry used to determine the
3131
* value). The reported value is a `DOMHighResTimeStamp`.
3232
*/
33-
export const onLCP = (onReport: ReportCallback): void => {
33+
export const onLCP = (onReport: ReportCallback): StopListening | undefined => {
3434
const visibilityWatcher = getVisibilityWatcher();
3535
const metric = initMetric('LCP');
3636
let report: ReturnType<typeof bindReporter>;
@@ -75,5 +75,9 @@ export const onLCP = (onReport: ReportCallback): void => {
7575
});
7676

7777
onHidden(stopListening, true);
78+
79+
return stopListening;
7880
}
81+
82+
return;
7983
};

packages/tracing/src/browser/web-vitals/types/base.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -104,3 +104,5 @@ export interface ReportOpts {
104104
* loading. This is equivalent to the corresponding `readyState` value.
105105
*/
106106
export type LoadState = 'loading' | 'dom-interactive' | 'dom-content-loaded' | 'complete';
107+
108+
export type StopListening = () => void;

packages/tracing/test/browser/browsertracing.test.ts

Lines changed: 21 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,14 @@ jest.mock('@sentry/utils', () => {
2828
};
2929
});
3030

31-
jest.mock('../../src/browser/metrics');
31+
const mockStartTrackingWebVitals = jest.fn().mockReturnValue(() => () => {});
32+
33+
jest.mock('../../src/browser/metrics', () => ({
34+
addPerformanceEntries: jest.fn(),
35+
startTrackingInteractions: jest.fn(),
36+
startTrackingLongTasks: jest.fn(),
37+
startTrackingWebVitals: () => mockStartTrackingWebVitals(),
38+
}));
3239

3340
const instrumentOutgoingRequestsMock = jest.fn();
3441
jest.mock('./../../src/browser/request', () => {
@@ -57,6 +64,8 @@ describe('BrowserTracing', () => {
5764
hub = new Hub(new BrowserClient(options));
5865
makeMain(hub);
5966
document.head.innerHTML = '';
67+
68+
mockStartTrackingWebVitals.mockClear();
6069
});
6170

6271
afterEach(() => {
@@ -371,6 +380,17 @@ describe('BrowserTracing', () => {
371380
jest.advanceTimersByTime(2000);
372381
expect(mockFinish).toHaveBeenCalledTimes(1);
373382
});
383+
384+
it('calls `_collectWebVitals` if enabled', () => {
385+
createBrowserTracing(true, { routingInstrumentation: customInstrumentRouting });
386+
const transaction = getActiveTransaction(hub) as IdleTransaction;
387+
388+
const span = transaction.startChild(); // activities = 1
389+
span.finish(); // activities = 0
390+
391+
jest.advanceTimersByTime(TRACING_DEFAULTS.idleTimeout);
392+
expect(mockStartTrackingWebVitals).toHaveBeenCalledTimes(1);
393+
});
374394
});
375395

376396
describe('heartbeatInterval', () => {

0 commit comments

Comments
 (0)