@@ -14,6 +14,73 @@ import { Span as SpanClass } from '../span';
14
14
import { SpanStatus } from '../spanstatus' ;
15
15
import { Transaction } from '../transaction' ;
16
16
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
+
17
84
/**
18
85
* Options for Tracing integration
19
86
*/
@@ -447,7 +514,7 @@ export class Tracing implements Integration {
447
514
}
448
515
449
516
/**
450
- * Finshes the current active transaction
517
+ * Finishes the current active transaction
451
518
*/
452
519
public static finishIdleTransaction ( endTimestamp : number ) : void {
453
520
const active = Tracing . _activeTransaction ;
@@ -505,6 +572,16 @@ export class Tracing implements Integration {
505
572
506
573
Tracing . _log ( '[Tracing] Adding & adjusting spans using Performance API' ) ;
507
574
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
+
508
585
const timeOrigin = Tracing . _msToSec ( performance . timeOrigin ) ;
509
586
510
587
// tslint:disable-next-line: completed-docs
0 commit comments