1
1
/* eslint-disable max-lines */
2
2
/* eslint-disable @typescript-eslint/no-explicit-any */
3
- import { SpanContext } from '@sentry/types' ;
3
+ import { Measurements , SpanContext } from '@sentry/types' ;
4
4
import { getGlobalObject , logger } from '@sentry/utils' ;
5
5
6
6
import { Span } from '../span' ;
7
7
import { Transaction } from '../transaction' ;
8
8
import { msToSec } from './utils' ;
9
9
10
- const global = getGlobalObject < Window > ( ) ;
10
+ // https://wicg.github.io/event-timing/#sec-performance-event-timing
11
+ interface PerformanceEventTiming extends PerformanceEntry {
12
+ processingStart : DOMHighResTimeStamp ;
13
+ cancelable ?: boolean ;
14
+ target ?: Element ;
15
+ }
11
16
17
+ const global = getGlobalObject < Window > ( ) ;
12
18
/** Class tracking metrics */
13
19
export class MetricsInstrumentation {
14
20
private _lcp : Record < string , any > = { } ;
21
+ private _measurements : Measurements = { } ;
15
22
16
23
private _performanceCursor : number = 0 ;
17
24
@@ -22,6 +29,7 @@ export class MetricsInstrumentation {
22
29
}
23
30
24
31
this . _trackLCP ( ) ;
32
+ this . _trackFID ( ) ;
25
33
}
26
34
}
27
35
@@ -85,6 +93,21 @@ export class MetricsInstrumentation {
85
93
if ( tracingInitMarkStartTime === undefined && entry . name === 'sentry-tracing-init' ) {
86
94
tracingInitMarkStartTime = startTimestamp ;
87
95
}
96
+
97
+ // capture web vitals
98
+
99
+ if ( entry . name === 'first-paint' ) {
100
+ logger . log ( '[Measurements] Adding FP (First Paint)' ) ;
101
+ this . _measurements [ 'fp' ] = { value : entry . startTime } ;
102
+ this . _measurements [ 'mark.fp' ] = { value : startTimestamp } ;
103
+ }
104
+
105
+ if ( entry . name === 'first-contentful-paint' ) {
106
+ logger . log ( '[Measurements] Adding FCP (First Contentful Paint)' ) ;
107
+ this . _measurements [ 'fcp' ] = { value : entry . startTime } ;
108
+ this . _measurements [ 'mark.fcp' ] = { value : startTimestamp } ;
109
+ }
110
+
88
111
break ;
89
112
}
90
113
case 'resource' : {
@@ -111,6 +134,8 @@ export class MetricsInstrumentation {
111
134
}
112
135
113
136
this . _performanceCursor = Math . max ( performance . getEntries ( ) . length - 1 , 0 ) ;
137
+
138
+ transaction . setMeasurements ( this . _measurements ) ;
114
139
}
115
140
116
141
private _forceLCP : ( ) => void = ( ) => {
@@ -154,6 +179,14 @@ export class MetricsInstrumentation {
154
179
...( entry . size && { elementSize : entry . size } ) ,
155
180
value : entry . startTime ,
156
181
} ;
182
+
183
+ logger . log ( '[Measurements] Adding LCP (Largest Contentful Paint)' ) ;
184
+
185
+ this . _measurements [ 'lcp' ] = { value : entry . startTime } ;
186
+
187
+ const timeOrigin = msToSec ( performance . timeOrigin ) ;
188
+ const startTime = msToSec ( entry . startTime as number ) ;
189
+ this . _measurements [ 'mark.lcp' ] = { value : timeOrigin + startTime } ;
157
190
}
158
191
} ;
159
192
@@ -179,6 +212,64 @@ export class MetricsInstrumentation {
179
212
// Do nothing if the browser doesn't support this API.
180
213
}
181
214
}
215
+
216
+ /** Starts tracking the First Input Delay on the current page. */
217
+ private _trackFID ( ) : void {
218
+ // Based on reference implementation from https://web.dev/fid/#measure-fid-in-javascript.
219
+ // Use a try/catch instead of feature detecting `first-input`
220
+ // support, since some browsers throw when using the new `type` option.
221
+ // https://bugs.webkit.org/show_bug.cgi?id=209216
222
+ try {
223
+ // Keep track of whether (and when) the page was first hidden, see:
224
+ // https://github.com/w3c/page-visibility/issues/29
225
+ // NOTE: ideally this check would be performed in the document <head>
226
+ // to avoid cases where the visibility state changes before this code runs.
227
+ let firstHiddenTime = document . visibilityState === 'hidden' ? 0 : Infinity ;
228
+ document . addEventListener (
229
+ 'visibilitychange' ,
230
+ event => {
231
+ firstHiddenTime = Math . min ( firstHiddenTime , event . timeStamp ) ;
232
+ } ,
233
+ { once : true } ,
234
+ ) ;
235
+
236
+ const updateFID = ( entry : PerformanceEventTiming , po : PerformanceObserver ) : void => {
237
+ // Only report FID if the page wasn't hidden prior to
238
+ // the entry being dispatched. This typically happens when a
239
+ // page is loaded in a background tab.
240
+ if ( entry . startTime < firstHiddenTime ) {
241
+ const fidValue = entry . processingStart - entry . startTime ;
242
+
243
+ logger . log ( '[Measurements] Adding FID (First Input Delay)' ) ;
244
+
245
+ // Report the FID value to an analytics endpoint.
246
+ this . _measurements [ 'fid' ] = { value : fidValue } ;
247
+
248
+ const timeOrigin = msToSec ( performance . timeOrigin ) ;
249
+ const startTime = msToSec ( entry . startTime as number ) ;
250
+ this . _measurements [ 'mark.fid_start' ] = { value : timeOrigin + startTime } ;
251
+
252
+ // Disconnect the observer.
253
+ po . disconnect ( ) ;
254
+ }
255
+ } ;
256
+
257
+ // Create a PerformanceObserver that calls `updateFID` for each entry.
258
+ const po = new PerformanceObserver ( entryList => {
259
+ entryList . getEntries ( ) . forEach ( entry => updateFID ( entry as PerformanceEventTiming , po ) ) ;
260
+ } ) ;
261
+
262
+ // Observe entries of type `largest-contentful-paint`, including buffered entries,
263
+ // i.e. entries that occurred before calling `observe()` below.
264
+ po . observe ( {
265
+ buffered : true ,
266
+ // @ts -ignore type does not exist on obj
267
+ type : 'first-input' ,
268
+ } ) ;
269
+ } catch ( e ) {
270
+ // Do nothing if the browser doesn't support this API.
271
+ }
272
+ }
182
273
}
183
274
184
275
/** Instrument navigation entries */
0 commit comments