Skip to content

Commit 8b32154

Browse files
committed
feat: Report LCP metric on pageload transactions
Largest Contentful Paint (LCP) is an user-centric metric for measuring perceived load speed. It marks the point in the page load timeline when the page's main content has likely loaded. Reference: https://web.dev/lcp/#measure-lcp-in-javascript
1 parent 4e552ec commit 8b32154

File tree

1 file changed

+78
-1
lines changed

1 file changed

+78
-1
lines changed

packages/apm/src/integrations/tracing.ts

Lines changed: 78 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,73 @@ import { Span as SpanClass } from '../span';
1414
import { SpanStatus } from '../spanstatus';
1515
import { Transaction } from '../transaction';
1616

17+
/** Holds the latest LargestContentfulPaint value (it changes during page load). */
18+
let lcp: { [key: string]: any };
19+
20+
/** Force any pending LargestContentfulPaint records to be dispatched. */
21+
let forceLCP = () => {
22+
/* No-op, replaced later if LCP API is available. */
23+
};
24+
25+
// Based on reference implementation from https://web.dev/lcp/#measure-lcp-in-javascript.
26+
{
27+
// Use a try/catch instead of feature detecting `largest-contentful-paint`
28+
// support, since some browsers throw when using the new `type` option.
29+
// https://bugs.webkit.org/show_bug.cgi?id=209216
30+
try {
31+
// Keep track of whether (and when) the page was first hidden, see:
32+
// https://github.com/w3c/page-visibility/issues/29
33+
// NOTE: ideally this check would be performed in the document <head>
34+
// to avoid cases where the visibility state changes before this code runs.
35+
let firstHiddenTime = document.visibilityState === 'hidden' ? 0 : Infinity;
36+
document.addEventListener(
37+
'visibilitychange',
38+
event => {
39+
firstHiddenTime = Math.min(firstHiddenTime, event.timeStamp);
40+
},
41+
{ once: true },
42+
);
43+
44+
const updateLCP = (entry: PerformanceEntry) => {
45+
// Only include an LCP entry if the page wasn't hidden prior to
46+
// the entry being dispatched. This typically happens when a page is
47+
// loaded in a background tab.
48+
if (entry.startTime < firstHiddenTime) {
49+
// NOTE: the `startTime` value is a getter that returns the entry's
50+
// `renderTime` value, if available, or its `loadTime` value otherwise.
51+
// The `renderTime` value may not be available if the element is an image
52+
// that's loaded cross-origin without the `Timing-Allow-Origin` header.
53+
lcp = {
54+
// @ts-ignore
55+
elementId: entry.id,
56+
// @ts-ignore
57+
elementSize: entry.size,
58+
largestContentfulPaint: entry.startTime,
59+
};
60+
}
61+
};
62+
63+
// Create a PerformanceObserver that calls `updateLCP` for each entry.
64+
const po = new PerformanceObserver(entryList => {
65+
entryList.getEntries().forEach(updateLCP);
66+
});
67+
68+
// Observe entries of type `largest-contentful-paint`, including buffered entries,
69+
// i.e. entries that occurred before calling `observe()` below.
70+
po.observe({
71+
buffered: true,
72+
// @ts-ignore
73+
type: 'largest-contentful-paint',
74+
});
75+
76+
forceLCP = () => {
77+
po.takeRecords().forEach(updateLCP);
78+
};
79+
} catch (e) {
80+
// Do nothing if the browser doesn't support this API.
81+
}
82+
}
83+
1784
/**
1885
* Options for Tracing integration
1986
*/
@@ -447,7 +514,7 @@ export class Tracing implements Integration {
447514
}
448515

449516
/**
450-
* Finshes the current active transaction
517+
* Finishes the current active transaction
451518
*/
452519
public static finishIdleTransaction(endTimestamp: number): void {
453520
const active = Tracing._activeTransaction;
@@ -505,6 +572,16 @@ export class Tracing implements Integration {
505572

506573
Tracing._log('[Tracing] Adding & adjusting spans using Performance API');
507574

575+
// FIXME: depending on the 'op' directly is brittle.
576+
if (transactionSpan.op === 'pageload') {
577+
// Force any pending records to be dispatched.
578+
forceLCP();
579+
if (lcp) {
580+
// Set the last observed LCP score.
581+
transactionSpan.setData('_sentry_extra_metrics', JSON.stringify({ lcp }));
582+
}
583+
}
584+
508585
const timeOrigin = Tracing._msToSec(performance.timeOrigin);
509586

510587
// tslint:disable-next-line: completed-docs

0 commit comments

Comments
 (0)