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 { browserPerformanceTimeOrigin , 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
+ import { getFID } from './web-vitals/getFID' ;
10
+ import { getLCP } from './web-vitals/getLCP' ;
9
11
10
12
const global = getGlobalObject < Window > ( ) ;
11
13
12
14
/** Class tracking metrics */
13
15
export class MetricsInstrumentation {
14
- private _lcp : Record < string , any > = { } ;
16
+ private _measurements : Measurements = { } ;
15
17
16
18
private _performanceCursor : number = 0 ;
17
19
@@ -22,6 +24,7 @@ export class MetricsInstrumentation {
22
24
}
23
25
24
26
this . _trackLCP ( ) ;
27
+ this . _trackFID ( ) ;
25
28
}
26
29
}
27
30
@@ -34,16 +37,6 @@ export class MetricsInstrumentation {
34
37
35
38
logger . log ( '[Tracing] Adding & adjusting spans using Performance API' ) ;
36
39
37
- // TODO(fixme): depending on the 'op' directly is brittle.
38
- if ( transaction . op === 'pageload' ) {
39
- // Force any pending records to be dispatched.
40
- this . _forceLCP ( ) ;
41
- if ( this . _lcp ) {
42
- // Set the last observed LCP score.
43
- transaction . setData ( '_sentry_web_vitals' , { LCP : this . _lcp } ) ;
44
- }
45
- }
46
-
47
40
const timeOrigin = msToSec ( browserPerformanceTimeOrigin ) ;
48
41
let entryScriptSrc : string | undefined ;
49
42
@@ -85,6 +78,21 @@ export class MetricsInstrumentation {
85
78
if ( tracingInitMarkStartTime === undefined && entry . name === 'sentry-tracing-init' ) {
86
79
tracingInitMarkStartTime = startTimestamp ;
87
80
}
81
+
82
+ // capture web vitals
83
+
84
+ if ( entry . name === 'first-paint' ) {
85
+ logger . log ( '[Measurements] Adding FP' ) ;
86
+ this . _measurements [ 'fp' ] = { value : entry . startTime } ;
87
+ this . _measurements [ 'mark.fp' ] = { value : startTimestamp } ;
88
+ }
89
+
90
+ if ( entry . name === 'first-contentful-paint' ) {
91
+ logger . log ( '[Measurements] Adding FCP' ) ;
92
+ this . _measurements [ 'fcp' ] = { value : entry . startTime } ;
93
+ this . _measurements [ 'mark.fcp' ] = { value : startTimestamp } ;
94
+ }
95
+
88
96
break ;
89
97
}
90
98
case 'resource' : {
@@ -111,73 +119,45 @@ export class MetricsInstrumentation {
111
119
}
112
120
113
121
this . _performanceCursor = Math . max ( performance . getEntries ( ) . length - 1 , 0 ) ;
114
- }
115
122
116
- private _forceLCP : ( ) => void = ( ) => {
117
- /* No-op, replaced later if LCP API is available. */
118
- return ;
119
- } ;
123
+ // Measurements are only available for pageload transactions
124
+ if ( transaction . op === 'pageload' ) {
125
+ transaction . setMeasurements ( this . _measurements ) ;
126
+ }
127
+ }
120
128
121
129
/** Starts tracking the Largest Contentful Paint on the current page. */
122
130
private _trackLCP ( ) : void {
123
- // Based on reference implementation from https://web.dev/lcp/#measure-lcp-in-javascript.
124
- // Use a try/catch instead of feature detecting `largest-contentful-paint`
125
- // support, since some browsers throw when using the new `type` option.
126
- // https://bugs.webkit.org/show_bug.cgi?id=209216
127
- try {
128
- // Keep track of whether (and when) the page was first hidden, see:
129
- // https://github.com/w3c/page-visibility/issues/29
130
- // NOTE: ideally this check would be performed in the document <head>
131
- // to avoid cases where the visibility state changes before this code runs.
132
- let firstHiddenTime = document . visibilityState === 'hidden' ? 0 : Infinity ;
133
- document . addEventListener (
134
- 'visibilitychange' ,
135
- event => {
136
- firstHiddenTime = Math . min ( firstHiddenTime , event . timeStamp ) ;
137
- } ,
138
- { once : true } ,
139
- ) ;
140
-
141
- const updateLCP = ( entry : PerformanceEntry ) : void => {
142
- // Only include an LCP entry if the page wasn't hidden prior to
143
- // the entry being dispatched. This typically happens when a page is
144
- // loaded in a background tab.
145
- if ( entry . startTime < firstHiddenTime ) {
146
- // NOTE: the `startTime` value is a getter that returns the entry's
147
- // `renderTime` value, if available, or its `loadTime` value otherwise.
148
- // The `renderTime` value may not be available if the element is an image
149
- // that's loaded cross-origin without the `Timing-Allow-Origin` header.
150
- this . _lcp = {
151
- // @ts -ignore can't access id on entry
152
- ...( entry . id && { elementId : entry . id } ) ,
153
- // @ts -ignore can't access id on entry
154
- ...( entry . size && { elementSize : entry . size } ) ,
155
- value : entry . startTime ,
156
- } ;
157
- }
158
- } ;
131
+ getLCP ( metric => {
132
+ const entry = metric . entries . pop ( ) ;
159
133
160
- // Create a PerformanceObserver that calls `updateLCP` for each entry.
161
- const po = new PerformanceObserver ( entryList => {
162
- entryList . getEntries ( ) . forEach ( updateLCP ) ;
163
- } ) ;
134
+ if ( ! entry ) {
135
+ return ;
136
+ }
164
137
165
- // Observe entries of type `largest-contentful-paint`, including buffered entries,
166
- // i.e. entries that occurred before calling `observe()` below.
167
- po . observe ( {
168
- buffered : true ,
169
- // @ts -ignore type does not exist on obj
170
- type : 'largest-contentful-paint' ,
171
- } ) ;
138
+ const timeOrigin = msToSec ( performance . timeOrigin ) ;
139
+ const startTime = msToSec ( entry . startTime as number ) ;
140
+ logger . log ( '[Measurements] Adding LCP' ) ;
141
+ this . _measurements [ 'lcp' ] = { value : metric . value } ;
142
+ this . _measurements [ 'mark.lcp' ] = { value : timeOrigin + startTime } ;
143
+ } ) ;
144
+ }
172
145
173
- this . _forceLCP = ( ) => {
174
- if ( po . takeRecords ) {
175
- po . takeRecords ( ) . forEach ( updateLCP ) ;
176
- }
177
- } ;
178
- } catch ( e ) {
179
- // Do nothing if the browser doesn't support this API.
180
- }
146
+ /** Starts tracking the First Input Delay on the current page. */
147
+ private _trackFID ( ) : void {
148
+ getFID ( metric => {
149
+ const entry = metric . entries . pop ( ) ;
150
+
151
+ if ( ! entry ) {
152
+ return ;
153
+ }
154
+
155
+ const timeOrigin = msToSec ( performance . timeOrigin ) ;
156
+ const startTime = msToSec ( entry . startTime as number ) ;
157
+ logger . log ( '[Measurements] Adding FID' ) ;
158
+ this . _measurements [ 'fid' ] = { value : metric . value } ;
159
+ this . _measurements [ 'mark.fid' ] = { value : timeOrigin + startTime } ;
160
+ } ) ;
181
161
}
182
162
}
183
163
0 commit comments