@@ -155,6 +155,14 @@ export class Tracing implements Integration {
155
155
156
156
private static _heartbeatCounter : number = 0 ;
157
157
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
+
158
166
/**
159
167
* Constructor for Tracing
160
168
*
@@ -163,6 +171,7 @@ export class Tracing implements Integration {
163
171
public constructor ( _options ?: Partial < TracingOptions > ) {
164
172
if ( global . performance ) {
165
173
global . performance . mark ( 'sentry-tracing-init' ) ;
174
+ Tracing . _trackLCP ( ) ;
166
175
}
167
176
const defaults = {
168
177
debug : {
@@ -450,7 +459,7 @@ export class Tracing implements Integration {
450
459
}
451
460
452
461
/**
453
- * Finshes the current active transaction
462
+ * Finishes the current active transaction
454
463
*/
455
464
public static finishIdleTransaction ( endTimestamp : number ) : void {
456
465
const active = Tracing . _activeTransaction ;
@@ -508,6 +517,16 @@ export class Tracing implements Integration {
508
517
509
518
Tracing . _log ( '[Tracing] Adding & adjusting spans using Performance API' ) ;
510
519
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
+
511
530
const timeOrigin = Tracing . _msToSec ( performance . timeOrigin ) ;
512
531
513
532
// tslint:disable-next-line: completed-docs
@@ -632,6 +651,69 @@ export class Tracing implements Integration {
632
651
// tslint:enable: no-unsafe-any
633
652
}
634
653
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
+
635
717
/**
636
718
* Sets the status of the current active transaction (if there is one)
637
719
*/
0 commit comments