Skip to content

Commit 26f9db8

Browse files
authored
feat(browser): Send additional LCP timing info (#14372)
Adds `loadTime` and `renderTime` attributes from the [LCP performance entry](https://developer.mozilla.org/en-US/docs/Web/API/LargestContentfulPaint#instance_properties) to the `pageload` span. Besides being valuable information for users, this also helps us detect if LCP is potentially unreliable: If the LCP element was from a 3rd party origin and the `Timing-Allow-Origin` header was not sent in the resource response, the `renderTime` will be `0`.
1 parent b3dad24 commit 26f9db8

File tree

4 files changed

+62
-9
lines changed

4 files changed

+62
-9
lines changed

dev-packages/browser-integration-tests/suites/tracing/metrics/web-vitals-lcp/template.html

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,5 @@
66
<body>
77
<div id="content"></div>
88
<img src="https://example.com/my/image.png" />
9-
<button type="button">Test button</button>
109
</body>
1110
</html>

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

Lines changed: 48 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,14 @@ import type { Event } from '@sentry/types';
55
import { sentryTest } from '../../../../utils/fixtures';
66
import { getFirstSentryEnvelopeRequest, shouldSkipTracingTest } from '../../../../utils/helpers';
77

8-
sentryTest('should capture a LCP vital with element details.', async ({ browserName, getLocalTestUrl, page }) => {
8+
/*
9+
Because we "serve" the html test page as a static file, all requests for the image
10+
are considered 3rd party requests. So the LCP value we obtain for the image is also
11+
considered a 3rd party LCP value, meaning `renderTime` is only set if we also
12+
return the `Timing-Allow-Origin` header.
13+
*/
14+
15+
sentryTest('captures LCP vitals with element details.', async ({ browserName, getLocalTestUrl, page }) => {
916
if (shouldSkipTracingTest() || browserName !== 'chromium') {
1017
sentryTest.skip();
1118
}
@@ -16,17 +23,50 @@ sentryTest('should capture a LCP vital with element details.', async ({ browserN
1623
});
1724

1825
const url = await getLocalTestUrl({ testDir: __dirname });
19-
const [eventData] = await Promise.all([
20-
getFirstSentryEnvelopeRequest<Event>(page),
21-
page.goto(url),
22-
page.locator('button').click(),
23-
]);
26+
const [eventData] = await Promise.all([getFirstSentryEnvelopeRequest<Event>(page), page.goto(url)]);
2427

2528
expect(eventData.measurements).toBeDefined();
2629
expect(eventData.measurements?.lcp?.value).toBeDefined();
2730

28-
// XXX: This should be body > img, but it can be flakey as sometimes it will report
29-
// the button as LCP.
3031
expect(eventData.contexts?.trace?.data?.['lcp.element'].startsWith('body >')).toBe(true);
3132
expect(eventData.contexts?.trace?.data?.['lcp.size']).toBeGreaterThan(0);
33+
expect(eventData.contexts?.trace?.data?.['lcp.loadTime']).toBeGreaterThan(0);
34+
35+
// renderTime is 0 because we don't return the `Timing-Allow-Origin` header
36+
// and the image is loaded from a 3rd party origin
37+
expect(eventData.contexts?.trace?.data?.['lcp.renderTime']).toBe(0);
38+
39+
// The LCP value should be the loadTime because the renderTime is not set
40+
expect(eventData.measurements?.lcp?.value).toBeCloseTo(eventData.contexts?.trace?.data?.['lcp.loadTime']);
3241
});
42+
43+
sentryTest(
44+
'captures LCP renderTime when returning Timing-Allow-Origin header.',
45+
async ({ browserName, getLocalTestUrl, page }) => {
46+
if (shouldSkipTracingTest() || browserName !== 'chromium') {
47+
sentryTest.skip();
48+
}
49+
50+
page.route('**', route => route.continue());
51+
page.route('**/my/image.png', async (route: Route) => {
52+
return route.fulfill({
53+
path: `${__dirname}/assets/sentry-logo-600x179.png`,
54+
headers: { 'Timing-Allow-Origin': '*' },
55+
});
56+
});
57+
58+
const url = await getLocalTestUrl({ testDir: __dirname });
59+
const [eventData] = await Promise.all([getFirstSentryEnvelopeRequest<Event>(page), page.goto(url)]);
60+
61+
expect(eventData.measurements).toBeDefined();
62+
expect(eventData.measurements?.lcp?.value).toBeDefined();
63+
64+
expect(eventData.contexts?.trace?.data?.['lcp.element'].startsWith('body >')).toBe(true);
65+
expect(eventData.contexts?.trace?.data?.['lcp.size']).toBeGreaterThan(0);
66+
expect(eventData.contexts?.trace?.data?.['lcp.loadTime']).toBeGreaterThan(0);
67+
expect(eventData.contexts?.trace?.data?.['lcp.renderTime']).toBeGreaterThan(0);
68+
69+
// The LCP value should be the renderTime because the renderTime is set
70+
expect(eventData.measurements?.lcp?.value).toBeCloseTo(eventData.contexts?.trace?.data?.['lcp.renderTime']);
71+
},
72+
);

dev-packages/e2e-tests/test-applications/react-create-hash-router/tests/transactions.test.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,8 @@ test('Captures a pageload transaction', async ({ page }) => {
2424
'sentry.source': 'route',
2525
'performance.timeOrigin': expect.any(Number),
2626
'performance.activationStart': expect.any(Number),
27+
'lcp.renderTime': expect.any(Number),
28+
'lcp.loadTime': expect.any(Number),
2729
},
2830
op: 'pageload',
2931
span_id: expect.any(String),

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

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -635,6 +635,18 @@ function _setWebVitalAttributes(span: Span): void {
635635
span.setAttribute('lcp.url', _lcpEntry.url.trim().slice(0, 200));
636636
}
637637

638+
if (_lcpEntry.loadTime != null) {
639+
// loadTime is the time of LCP that's related to receiving the LCP element response..
640+
span.setAttribute('lcp.loadTime', _lcpEntry.loadTime);
641+
}
642+
643+
if (_lcpEntry.renderTime != null) {
644+
// renderTime is loadTime + rendering time
645+
// it's 0 if the LCP element is loaded from a 3rd party origin that doesn't send the
646+
// `Timing-Allow-Origin` header.
647+
span.setAttribute('lcp.renderTime', _lcpEntry.renderTime);
648+
}
649+
638650
span.setAttribute('lcp.size', _lcpEntry.size);
639651
}
640652

0 commit comments

Comments
 (0)