Skip to content

Commit bb45dd2

Browse files
authored
fix(browser): Remove faulty LCP, FCP and FP normalization logic (#13502)
Remove incorrect normalization logic we applied to LCP, FCP and FP web vital measurements. With this change, we no longer alter the three web vital values but report directly what we received from the web-vitals library. Add a span attribute, `performance.timeOrigin` (feel free to suggest better names) to the pageload root span. This attribute contains the `timeOrigin` value we determine in the SDK. This value [should be used](https://developer.mozilla.org/en-US/docs/Web/API/Performance/timeOrigin) to base performance measurements on.
1 parent 09622d6 commit bb45dd2

File tree

8 files changed

+111
-23
lines changed

8 files changed

+111
-23
lines changed

CHANGELOG.md

+11
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,17 @@
1010

1111
- "You miss 100 percent of the chances you don't take. — Wayne Gretzky" — Michael Scott
1212

13+
### Important Changes
14+
15+
- **fix(browser): Remove faulty LCP, FCP and FP normalization logic (#13502)**
16+
17+
This release fixes a bug in the `@sentry/browser` package and all SDKs depending on this package (e.g. `@sentry/react`
18+
or `@sentry/nextjs`) that caused the SDK to send incorrect web vital values for the LCP, FCP and FP vitals. The SDK
19+
previously incorrectly processed the original values as they were reported from the browser. When updating your SDK to
20+
this version, you might experience an increase in LCP, FCP and FP values, which potentially leads to a decrease in your
21+
performance score in the Web Vitals Insights module in Sentry. This is because the previously reported values were
22+
smaller than the actually measured values. We apologize for the inconvenience!
23+
1324
Work in this release was contributed by @leopoldkristjansson and @filips123. Thank you for your contributions!
1425

1526
## 8.27.0

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

+1-1
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
</head>
66
<body>
77
<div id="content"></div>
8-
<img src="https://example.com/path/to/image.png" />
8+
<img src="https://example.com/my/image.png" />
99
<button type="button">Test button</button>
1010
</body>
1111
</html>

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

+1-1
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ sentryTest('should capture a LCP vital with element details.', async ({ browserN
1111
}
1212

1313
page.route('**', route => route.continue());
14-
page.route('**/path/to/image.png', async (route: Route) => {
14+
page.route('**/my/image.png', async (route: Route) => {
1515
return route.fulfill({ path: `${__dirname}/assets/sentry-logo-600x179.png` });
1616
});
1717

Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
<!DOCTYPE html>
2+
<html>
3+
<head>
4+
<meta charset="utf-8" />
5+
</head>
6+
<body>
7+
<div id="content"></div>
8+
<img src="https://example.com/library/image.png" />
9+
<button type="button">Test button</button>
10+
</body>
11+
</html>
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
1+
import type { Route } from '@playwright/test';
2+
import { expect } from '@playwright/test';
3+
import type { Event } from '@sentry/types';
4+
5+
import { sentryTest } from '../../../../utils/fixtures';
6+
import { getFirstSentryEnvelopeRequest, shouldSkipTracingTest } from '../../../../utils/helpers';
7+
8+
/**
9+
* Bit of an odd test but we previously ran into cases where we would report TTFB > (LCP, FCP, FP)
10+
* This should never happen and this test serves as a regression test for that.
11+
*
12+
* The problem is: We don't always get valid TTFB from the web-vitals library, so we skip the test if that's the case.
13+
* Note: There is another test that covers that we actually report TTFB if it is valid (@see ../web-vitals-lcp/test.ts).
14+
*/
15+
sentryTest('paint web vitals values are greater than TTFB', async ({ browserName, getLocalTestPath, page }) => {
16+
// Only run in chromium to ensure all vitals are present
17+
if (shouldSkipTracingTest() || browserName !== 'chromium') {
18+
sentryTest.skip();
19+
}
20+
21+
page.route('**', route => route.continue());
22+
page.route('**/library/image.png', async (route: Route) => {
23+
return route.fulfill({ path: `${__dirname}/assets/sentry-logo-600x179.png` });
24+
});
25+
26+
const url = await getLocalTestPath({ testDir: __dirname });
27+
const [eventData] = await Promise.all([
28+
getFirstSentryEnvelopeRequest<Event>(page),
29+
page.goto(url),
30+
page.locator('button').click(),
31+
]);
32+
33+
expect(eventData.measurements).toBeDefined();
34+
35+
const ttfbValue = eventData.measurements?.ttfb?.value;
36+
37+
if (!ttfbValue) {
38+
// TTFB is unfortunately quite flaky. Sometimes, the web-vitals library doesn't report TTFB because
39+
// responseStart is 0. This seems to happen somewhat randomly, so we just ignore this in that case.
40+
// @see packages/browser-utils/src/metrics/web-vitals/onTTFB
41+
42+
// logging the skip reason so that we at least can check for that in CI logs
43+
// eslint-disable-next-line no-console
44+
console.log('SKIPPING: TTFB is not reported');
45+
sentryTest.skip();
46+
}
47+
48+
const lcpValue = eventData.measurements?.lcp?.value;
49+
const fcpValue = eventData.measurements?.fcp?.value;
50+
const fpValue = eventData.measurements?.fp?.value;
51+
52+
expect(lcpValue).toBeDefined();
53+
expect(fcpValue).toBeDefined();
54+
expect(fpValue).toBeDefined();
55+
56+
// (LCP, FCP, FP) >= TTFB
57+
expect(lcpValue).toBeGreaterThanOrEqual(ttfbValue!);
58+
expect(fcpValue).toBeGreaterThanOrEqual(ttfbValue!);
59+
expect(fpValue).toBeGreaterThanOrEqual(ttfbValue!);
60+
});
61+
62+
sentryTest('captures time origin as span attribute', async ({ getLocalTestPath, page }) => {
63+
// Only run in chromium to ensure all vitals are present
64+
if (shouldSkipTracingTest()) {
65+
sentryTest.skip();
66+
}
67+
68+
const url = await getLocalTestPath({ testDir: __dirname });
69+
const [eventData] = await Promise.all([getFirstSentryEnvelopeRequest<Event>(page), page.goto(url)]);
70+
71+
const timeOriginAttribute = eventData.contexts?.trace?.data?.['performance.timeOrigin'];
72+
const transactionStartTimestamp = eventData.start_timestamp;
73+
74+
expect(timeOriginAttribute).toBeDefined();
75+
expect(transactionStartTimestamp).toBeDefined();
76+
77+
const delta = Math.abs(transactionStartTimestamp! - timeOriginAttribute);
78+
79+
// The delta should be less than 1ms if this flakes, we should increase the threshold
80+
expect(delta).toBeLessThanOrEqual(1);
81+
});

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

+1
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ test('Captures a pageload transaction', async ({ page }) => {
2222
'sentry.origin': 'auto.pageload.react.reactrouter_v6',
2323
'sentry.sample_rate': 1,
2424
'sentry.source': 'route',
25+
'performance.timeOrigin': expect.any(Number),
2526
},
2627
op: 'pageload',
2728
span_id: expect.any(String),

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

+5-21
Original file line numberDiff line numberDiff line change
@@ -354,25 +354,6 @@ export function addPerformanceEntries(span: Span, options: AddPerformanceEntries
354354
if (op === 'pageload') {
355355
_addTtfbRequestTimeToMeasurements(_measurements);
356356

357-
['fcp', 'fp', 'lcp'].forEach(name => {
358-
const measurement = _measurements[name];
359-
if (!measurement || !transactionStartTime || timeOrigin >= transactionStartTime) {
360-
return;
361-
}
362-
// The web vitals, fcp, fp, lcp, and ttfb, all measure relative to timeOrigin.
363-
// Unfortunately, timeOrigin is not captured within the span span data, so these web vitals will need
364-
// to be adjusted to be relative to span.startTimestamp.
365-
const oldValue = measurement.value;
366-
const measurementTimestamp = timeOrigin + msToSec(oldValue);
367-
368-
// normalizedValue should be in milliseconds
369-
const normalizedValue = Math.abs((measurementTimestamp - transactionStartTime) * 1000);
370-
const delta = normalizedValue - oldValue;
371-
372-
DEBUG_BUILD && logger.log(`[Measurements] Normalized ${name} from ${oldValue} to ${normalizedValue} (${delta})`);
373-
measurement.value = normalizedValue;
374-
});
375-
376357
const fidMark = _measurements['mark.fid'];
377358
if (fidMark && _measurements['fid']) {
378359
// create span for FID
@@ -399,7 +380,10 @@ export function addPerformanceEntries(span: Span, options: AddPerformanceEntries
399380
setMeasurement(measurementName, measurement.value, measurement.unit);
400381
});
401382

402-
_tagMetricInfo(span);
383+
// Set timeOrigin which denotes the timestamp which to base the LCP/FCP/FP/TTFB measurements on
384+
span.setAttribute('performance.timeOrigin', timeOrigin);
385+
386+
_setWebVitalAttributes(span);
403387
}
404388

405389
_lcpEntry = undefined;
@@ -604,7 +588,7 @@ function _trackNavigator(span: Span): void {
604588
}
605589

606590
/** Add LCP / CLS data to span to allow debugging */
607-
function _tagMetricInfo(span: Span): void {
591+
function _setWebVitalAttributes(span: Span): void {
608592
if (_lcpEntry) {
609593
DEBUG_BUILD && logger.log('[Measurements] Adding LCP Data');
610594

0 commit comments

Comments
 (0)