Skip to content

Commit abe04f2

Browse files
committed
feat(rum): Add measurements support and web vitals
1 parent daae471 commit abe04f2

File tree

5 files changed

+118
-3
lines changed

5 files changed

+118
-3
lines changed

packages/tracing/src/browser/metrics.ts

Lines changed: 93 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,24 @@
11
/* eslint-disable max-lines */
22
/* eslint-disable @typescript-eslint/no-explicit-any */
3-
import { SpanContext } from '@sentry/types';
3+
import { Measurements, SpanContext } from '@sentry/types';
44
import { getGlobalObject, logger } from '@sentry/utils';
55

66
import { Span } from '../span';
77
import { Transaction } from '../transaction';
88
import { msToSec } from './utils';
99

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+
}
1116

17+
const global = getGlobalObject<Window>();
1218
/** Class tracking metrics */
1319
export class MetricsInstrumentation {
1420
private _lcp: Record<string, any> = {};
21+
private _measurements: Measurements = {};
1522

1623
private _performanceCursor: number = 0;
1724

@@ -22,6 +29,7 @@ export class MetricsInstrumentation {
2229
}
2330

2431
this._trackLCP();
32+
this._trackFID();
2533
}
2634
}
2735

@@ -85,6 +93,21 @@ export class MetricsInstrumentation {
8593
if (tracingInitMarkStartTime === undefined && entry.name === 'sentry-tracing-init') {
8694
tracingInitMarkStartTime = startTimestamp;
8795
}
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+
88111
break;
89112
}
90113
case 'resource': {
@@ -111,6 +134,8 @@ export class MetricsInstrumentation {
111134
}
112135

113136
this._performanceCursor = Math.max(performance.getEntries().length - 1, 0);
137+
138+
transaction.setMeasurements(this._measurements);
114139
}
115140

116141
private _forceLCP: () => void = () => {
@@ -154,6 +179,14 @@ export class MetricsInstrumentation {
154179
...(entry.size && { elementSize: entry.size }),
155180
value: entry.startTime,
156181
};
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 };
157190
}
158191
};
159192

@@ -179,6 +212,64 @@ export class MetricsInstrumentation {
179212
// Do nothing if the browser doesn't support this API.
180213
}
181214
}
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+
}
182273
}
183274

184275
/** Instrument navigation entries */

packages/tracing/src/transaction.ts

Lines changed: 20 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,13 @@
11
import { getCurrentHub, Hub } from '@sentry/hub';
2-
import { TransactionContext } from '@sentry/types';
2+
import { Event, Measurements, TransactionContext } from '@sentry/types';
33
import { isInstanceOf, logger } from '@sentry/utils';
44

55
import { Span as SpanClass, SpanRecorder } from './span';
66

77
/** JSDoc */
88
export class Transaction extends SpanClass {
99
public name?: string;
10+
private _measurements: Measurements = {};
1011

1112
/**
1213
* The reference to the current hub.
@@ -54,6 +55,14 @@ export class Transaction extends SpanClass {
5455
this.spanRecorder.add(this);
5556
}
5657

58+
/**
59+
* Set observed measurements for this transaction.
60+
* @hidden
61+
*/
62+
public setMeasurements(measurements: Measurements): void {
63+
this._measurements = { ...measurements };
64+
}
65+
5766
/**
5867
* @inheritDoc
5968
*/
@@ -88,6 +97,15 @@ export class Transaction extends SpanClass {
8897
}).endTimestamp;
8998
}
9099

100+
const extra: Partial<Event> = {};
101+
102+
const hasMeasurements = Object.keys(this._measurements).length > 0;
103+
104+
if (hasMeasurements) {
105+
logger.log('[Measurements] Adding measurements to transaction', JSON.stringify(this._measurements, undefined, 2));
106+
extra.measurements = this._measurements;
107+
}
108+
91109
return this._hub.captureEvent({
92110
contexts: {
93111
trace: this.getTraceContext(),
@@ -98,6 +116,7 @@ export class Transaction extends SpanClass {
98116
timestamp: this.endTimestamp,
99117
transaction: this.name,
100118
type: 'transaction',
119+
...extra,
101120
});
102121
}
103122
}

packages/types/src/event.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import { SdkInfo } from './sdkinfo';
66
import { Severity } from './severity';
77
import { Span } from './span';
88
import { Stacktrace } from './stacktrace';
9+
import { Measurements } from './transaction';
910
import { User } from './user';
1011

1112
/** JSDoc */
@@ -39,6 +40,7 @@ export interface Event {
3940
user?: User;
4041
type?: EventType;
4142
spans?: Span[];
43+
measurements?: Measurements;
4244
}
4345

4446
/** JSDoc */

packages/types/src/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ export { Stacktrace } from './stacktrace';
2525
export { Status } from './status';
2626
export {
2727
CustomSamplingContext,
28+
Measurements,
2829
SamplingContext,
2930
TraceparentData,
3031
Transaction,

packages/types/src/transaction.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -98,3 +98,5 @@ export interface SamplingContext extends CustomSamplingContext {
9898
*/
9999
request?: ExtractedNodeRequestData;
100100
}
101+
102+
export type Measurements = Record<string, { value: number }>;

0 commit comments

Comments
 (0)