Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(rum): Add measurements support and web vitals #2909

Merged
merged 41 commits into from
Oct 7, 2020
Merged
Show file tree
Hide file tree
Changes from 10 commits
Commits
Show all changes
41 commits
Select commit Hold shift + click to select a range
49db0be
feat(rum): Add measurements support and web vitals
dashed Aug 21, 2020
f26f152
Update packages/tracing/src/browser/metrics.ts
dashed Sep 16, 2020
242a71d
remove some @ts-ignore
dashed Sep 17, 2020
0f06193
Measurements are only available for pageload transactions
dashed Sep 17, 2020
892e0ec
add enableMeasurements option to browser tracing options
dashed Sep 17, 2020
951c8b9
lint fix
dashed Sep 17, 2020
2b8f766
fix test
dashed Sep 17, 2020
d696e62
rename mark.fid_start to mark.fid
dashed Sep 17, 2020
29c4b52
set enableMeasurements to be true by default
dashed Sep 17, 2020
01f1117
remove enableMeasurements option
dashed Sep 17, 2020
4f194a7
Merge branch 'master' into add-web-vitals
dashed Sep 24, 2020
cad03a0
Update packages/tracing/src/browser/metrics.ts
dashed Sep 24, 2020
646ebea
Update packages/tracing/src/browser/metrics.ts
dashed Sep 24, 2020
375e7cc
Update packages/tracing/src/browser/metrics.ts
dashed Sep 24, 2020
bc7e0cc
Update packages/tracing/src/browser/metrics.ts
dashed Sep 24, 2020
2f1821b
save some bytes
dashed Sep 24, 2020
688c985
delete old lcp implementation
dashed Sep 24, 2020
c1b72ae
share firstHiddenTime code
dashed Sep 24, 2020
8c81de5
feature-detect FID and LCP
dashed Sep 24, 2020
8cc49f8
construction transaction event beforehand and add measurements condit…
dashed Sep 24, 2020
9eca498
yarn fix
dashed Sep 24, 2020
7f88469
ref: Restore blank line
rhcarvalho Sep 24, 2020
2318bfb
capture final lcp on hidden
dashed Sep 24, 2020
1dd9e83
Merge branch 'add-web-vitals' of github.com:getsentry/sentry-javascri…
dashed Sep 24, 2020
d5f0a5e
always disconnect the performance observer for FID
dashed Sep 24, 2020
b2671c3
vendor FID and LCP implementations from https://github.com/GoogleChro…
dashed Sep 24, 2020
6564a13
update ts on web-vitals impl
dashed Sep 24, 2020
4928ed3
use getLCP function
dashed Sep 24, 2020
d803955
remove _forceLCP
dashed Sep 24, 2020
18e4060
use getFID function
dashed Sep 24, 2020
7a40c5a
fix import
dashed Sep 24, 2020
6665ab6
rm unused interface
dashed Sep 24, 2020
f13c9ae
add missing file
dashed Sep 24, 2020
0700577
more fixes
dashed Sep 24, 2020
4ef5c48
fix log
dashed Sep 24, 2020
7b71679
add web-vitals README
dashed Sep 24, 2020
b0522af
Merge branch 'master' into add-web-vitals
dashed Sep 28, 2020
d8c89d3
Merge branch 'master' into add-web-vitals
dashed Oct 5, 2020
7006bc8
Merge branch 'master' into add-web-vitals
dashed Oct 6, 2020
3cb996b
note the SHA commit used to vendor web-vitals
dashed Oct 6, 2020
7801463
Merge branch 'master' into add-web-vitals
dashed Oct 7, 2020
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
98 changes: 95 additions & 3 deletions packages/tracing/src/browser/metrics.ts
Original file line number Diff line number Diff line change
@@ -1,17 +1,24 @@
/* eslint-disable max-lines */
/* eslint-disable @typescript-eslint/no-explicit-any */
import { SpanContext } from '@sentry/types';
import { Measurements, SpanContext } from '@sentry/types';
import { getGlobalObject, logger } from '@sentry/utils';

import { Span } from '../span';
import { Transaction } from '../transaction';
import { msToSec } from './utils';

const global = getGlobalObject<Window>();
// https://wicg.github.io/event-timing/#sec-performance-event-timing
interface PerformanceEventTiming extends PerformanceEntry {
processingStart: DOMHighResTimeStamp;
cancelable?: boolean;
target?: Element;
}

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

private _performanceCursor: number = 0;

Expand All @@ -22,6 +29,7 @@ export class MetricsInstrumentation {
}

this._trackLCP();
this._trackFID();
}
}

Expand Down Expand Up @@ -85,6 +93,21 @@ export class MetricsInstrumentation {
if (tracingInitMarkStartTime === undefined && entry.name === 'sentry-tracing-init') {
tracingInitMarkStartTime = startTimestamp;
}

// capture web vitals

if (entry.name === 'first-paint') {
logger.log('[Measurements] Adding FP (First Paint)');
dashed marked this conversation as resolved.
Show resolved Hide resolved
this._measurements['fp'] = { value: entry.startTime };
this._measurements['mark.fp'] = { value: startTimestamp };
}
rhcarvalho marked this conversation as resolved.
Show resolved Hide resolved

if (entry.name === 'first-contentful-paint') {
logger.log('[Measurements] Adding FCP (First Contentful Paint)');
this._measurements['fcp'] = { value: entry.startTime };
this._measurements['mark.fcp'] = { value: startTimestamp };
}

break;
}
case 'resource': {
Expand All @@ -111,6 +134,11 @@ export class MetricsInstrumentation {
}

this._performanceCursor = Math.max(performance.getEntries().length - 1, 0);

// Measurements are only available for pageload transactions
if (transaction.op === 'pageload') {
transaction.setMeasurements(this._measurements);
}
}

private _forceLCP: () => void = () => {
Expand Down Expand Up @@ -154,6 +182,14 @@ export class MetricsInstrumentation {
...(entry.size && { elementSize: entry.size }),
value: entry.startTime,
};
rhcarvalho marked this conversation as resolved.
Show resolved Hide resolved

logger.log('[Measurements] Adding LCP (Largest Contentful Paint)');

this._measurements['lcp'] = { value: entry.startTime };

const timeOrigin = msToSec(performance.timeOrigin);
const startTime = msToSec(entry.startTime as number);
this._measurements['mark.lcp'] = { value: timeOrigin + startTime };
dashed marked this conversation as resolved.
Show resolved Hide resolved
}
};

Expand All @@ -166,7 +202,6 @@ export class MetricsInstrumentation {
// i.e. entries that occurred before calling `observe()` below.
po.observe({
buffered: true,
// @ts-ignore type does not exist on obj
type: 'largest-contentful-paint',
});

Expand All @@ -179,6 +214,63 @@ export class MetricsInstrumentation {
// Do nothing if the browser doesn't support this API.
}
}

/** Starts tracking the First Input Delay on the current page. */
private _trackFID(): void {
// Based on reference implementation from https://web.dev/fid/#measure-fid-in-javascript.
// Use a try/catch instead of feature detecting `first-input`
// support, since some browsers throw when using the new `type` option.
rhcarvalho marked this conversation as resolved.
Show resolved Hide resolved
// https://bugs.webkit.org/show_bug.cgi?id=209216
try {
// Keep track of whether (and when) the page was first hidden, see:
// https://github.com/w3c/page-visibility/issues/29
// NOTE: ideally this check would be performed in the document <head>
// to avoid cases where the visibility state changes before this code runs.
let firstHiddenTime = document.visibilityState === 'hidden' ? 0 : Infinity;
document.addEventListener(
'visibilitychange',
event => {
firstHiddenTime = Math.min(firstHiddenTime, event.timeStamp);
},
{ once: true },
);
rhcarvalho marked this conversation as resolved.
Show resolved Hide resolved

const updateFID = (entry: PerformanceEventTiming, po: PerformanceObserver): void => {
// Only report FID if the page wasn't hidden prior to
// the entry being dispatched. This typically happens when a
// page is loaded in a background tab.
if (entry.startTime < firstHiddenTime) {
const fidValue = entry.processingStart - entry.startTime;

logger.log('[Measurements] Adding FID (First Input Delay)');

// Report the FID value to an analytics endpoint.
this._measurements['fid'] = { value: fidValue };

const timeOrigin = msToSec(performance.timeOrigin);
const startTime = msToSec(entry.startTime as number);
this._measurements['mark.fid'] = { value: timeOrigin + startTime };
dashed marked this conversation as resolved.
Show resolved Hide resolved

// Disconnect the observer.
dashed marked this conversation as resolved.
Show resolved Hide resolved
po.disconnect();
dashed marked this conversation as resolved.
Show resolved Hide resolved
}
};

// Create a PerformanceObserver that calls `updateFID` for each entry.
const po = new PerformanceObserver(entryList => {
entryList.getEntries().forEach(entry => updateFID(entry as PerformanceEventTiming, po));
});

// Observe entries of type `first-input`, including buffered entries,
// i.e. entries that occurred before calling `observe()` below.
po.observe({
buffered: true,
type: 'first-input',
});
} catch (e) {
// Do nothing if the browser doesn't support this API.
}
}
}

/** Instrument navigation entries */
Expand Down
21 changes: 20 additions & 1 deletion packages/tracing/src/transaction.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,13 @@
import { getCurrentHub, Hub } from '@sentry/hub';
import { TransactionContext } from '@sentry/types';
import { Event, Measurements, TransactionContext } from '@sentry/types';
import { isInstanceOf, logger } from '@sentry/utils';

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

/** JSDoc */
export class Transaction extends SpanClass {
public name?: string;
private _measurements: Measurements = {};

/**
* The reference to the current hub.
Expand Down Expand Up @@ -54,6 +55,14 @@ export class Transaction extends SpanClass {
this.spanRecorder.add(this);
}

/**
* Set observed measurements for this transaction.
* @hidden
*/
public setMeasurements(measurements: Measurements): void {
this._measurements = { ...measurements };
}

/**
* @inheritDoc
*/
Expand Down Expand Up @@ -88,6 +97,15 @@ export class Transaction extends SpanClass {
}).endTimestamp;
}

const extra: Partial<Event> = {};
rhcarvalho marked this conversation as resolved.
Show resolved Hide resolved

const hasMeasurements = Object.keys(this._measurements).length > 0;

if (hasMeasurements) {
logger.log('[Measurements] Adding measurements to transaction', JSON.stringify(this._measurements, undefined, 2));
extra.measurements = this._measurements;
}

return this._hub.captureEvent({
contexts: {
trace: this.getTraceContext(),
Expand All @@ -98,6 +116,7 @@ export class Transaction extends SpanClass {
timestamp: this.endTimestamp,
transaction: this.name,
type: 'transaction',
...extra,
});
}
}
2 changes: 2 additions & 0 deletions packages/types/src/event.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import { SdkInfo } from './sdkinfo';
import { Severity } from './severity';
import { Span } from './span';
import { Stacktrace } from './stacktrace';
import { Measurements } from './transaction';
import { User } from './user';

/** JSDoc */
Expand Down Expand Up @@ -39,6 +40,7 @@ export interface Event {
user?: User;
type?: EventType;
spans?: Span[];
measurements?: Measurements;
}

/** JSDoc */
Expand Down
1 change: 1 addition & 0 deletions packages/types/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ export { Stacktrace } from './stacktrace';
export { Status } from './status';
export {
CustomSamplingContext,
Measurements,
SamplingContext,
TraceparentData,
Transaction,
Expand Down
2 changes: 2 additions & 0 deletions packages/types/src/transaction.ts
Original file line number Diff line number Diff line change
Expand Up @@ -98,3 +98,5 @@ export interface SamplingContext extends CustomSamplingContext {
*/
request?: ExtractedNodeRequestData;
}

export type Measurements = Record<string, { value: number }>;