@@ -155,6 +155,14 @@ export class Tracing implements Integration {
155155
156156 private static _heartbeatCounter : number = 0 ;
157157
158+ /** Holds the latest LargestContentfulPaint value (it changes during page load). */
159+ private static _lcp ?: { [ key : string ] : any } ;
160+
161+ /** Force any pending LargestContentfulPaint records to be dispatched. */
162+ private static _forceLCP = ( ) => {
163+ /* No-op, replaced later if LCP API is available. */
164+ } ;
165+
158166 /**
159167 * Constructor for Tracing
160168 *
@@ -163,6 +171,7 @@ export class Tracing implements Integration {
163171 public constructor ( _options ?: Partial < TracingOptions > ) {
164172 if ( global . performance ) {
165173 global . performance . mark ( 'sentry-tracing-init' ) ;
174+ Tracing . _trackLCP ( ) ;
166175 }
167176 const defaults = {
168177 debug : {
@@ -450,7 +459,7 @@ export class Tracing implements Integration {
450459 }
451460
452461 /**
453- * Finshes the current active transaction
462+ * Finishes the current active transaction
454463 */
455464 public static finishIdleTransaction ( endTimestamp : number ) : void {
456465 const active = Tracing . _activeTransaction ;
@@ -508,6 +517,16 @@ export class Tracing implements Integration {
508517
509518 Tracing . _log ( '[Tracing] Adding & adjusting spans using Performance API' ) ;
510519
520+ // FIXME: depending on the 'op' directly is brittle.
521+ if ( transactionSpan . op === 'pageload' ) {
522+ // Force any pending records to be dispatched.
523+ Tracing . _forceLCP ( ) ;
524+ if ( Tracing . _lcp ) {
525+ // Set the last observed LCP score.
526+ transactionSpan . setData ( '_sentry_web_vitals' , { LCP : Tracing . _lcp } ) ;
527+ }
528+ }
529+
511530 const timeOrigin = Tracing . _msToSec ( performance . timeOrigin ) ;
512531
513532 // tslint:disable-next-line: completed-docs
@@ -632,6 +651,69 @@ export class Tracing implements Integration {
632651 // tslint:enable: no-unsafe-any
633652 }
634653
654+ /**
655+ * Starts tracking the Largest Contentful Paint on the current page.
656+ */
657+ private static _trackLCP ( ) : void {
658+ // Based on reference implementation from https://web.dev/lcp/#measure-lcp-in-javascript.
659+
660+ // Use a try/catch instead of feature detecting `largest-contentful-paint`
661+ // support, since some browsers throw when using the new `type` option.
662+ // https://bugs.webkit.org/show_bug.cgi?id=209216
663+ try {
664+ // Keep track of whether (and when) the page was first hidden, see:
665+ // https://github.com/w3c/page-visibility/issues/29
666+ // NOTE: ideally this check would be performed in the document <head>
667+ // to avoid cases where the visibility state changes before this code runs.
668+ let firstHiddenTime = document . visibilityState === 'hidden' ? 0 : Infinity ;
669+ document . addEventListener (
670+ 'visibilitychange' ,
671+ event => {
672+ firstHiddenTime = Math . min ( firstHiddenTime , event . timeStamp ) ;
673+ } ,
674+ { once : true } ,
675+ ) ;
676+
677+ const updateLCP = ( entry : PerformanceEntry ) => {
678+ // Only include an LCP entry if the page wasn't hidden prior to
679+ // the entry being dispatched. This typically happens when a page is
680+ // loaded in a background tab.
681+ if ( entry . startTime < firstHiddenTime ) {
682+ // NOTE: the `startTime` value is a getter that returns the entry's
683+ // `renderTime` value, if available, or its `loadTime` value otherwise.
684+ // The `renderTime` value may not be available if the element is an image
685+ // that's loaded cross-origin without the `Timing-Allow-Origin` header.
686+ Tracing . _lcp = {
687+ // @ts -ignore
688+ ...( entry . id && { elementId : entry . id } ) ,
689+ // @ts -ignore
690+ ...( entry . size && { elementSize : entry . size } ) ,
691+ value : entry . startTime ,
692+ } ;
693+ }
694+ } ;
695+
696+ // Create a PerformanceObserver that calls `updateLCP` for each entry.
697+ const po = new PerformanceObserver ( entryList => {
698+ entryList . getEntries ( ) . forEach ( updateLCP ) ;
699+ } ) ;
700+
701+ // Observe entries of type `largest-contentful-paint`, including buffered entries,
702+ // i.e. entries that occurred before calling `observe()` below.
703+ po . observe ( {
704+ buffered : true ,
705+ // @ts -ignore
706+ type : 'largest-contentful-paint' ,
707+ } ) ;
708+
709+ Tracing . _forceLCP = ( ) => {
710+ po . takeRecords ( ) . forEach ( updateLCP ) ;
711+ } ;
712+ } catch ( e ) {
713+ // Do nothing if the browser doesn't support this API.
714+ }
715+ }
716+
635717 /**
636718 * Sets the status of the current active transaction (if there is one)
637719 */
0 commit comments