Skip to content

feat(tracing): Upgrade to web-vitals 2.1.0 #3781

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

Merged
merged 6 commits into from
Jul 16, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
40 changes: 33 additions & 7 deletions packages/tracing/src/browser/metrics.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,8 @@ import { msToSec } from '../utils';
import { getCLS, LayoutShift } from './web-vitals/getCLS';
import { getFID } from './web-vitals/getFID';
import { getLCP, LargestContentfulPaint } from './web-vitals/getLCP';
import { getFirstHidden } from './web-vitals/lib/getFirstHidden';
import { getUpdatedCLS } from './web-vitals/getUpdatedCLS';
import { getVisibilityWatcher } from './web-vitals/lib/getVisibilityWatcher';
import { NavigatorDeviceMemory, NavigatorNetworkInformation } from './web-vitals/types';

const global = getGlobalObject<Window>();
Expand All @@ -21,6 +22,7 @@ export class MetricsInstrumentation {
private _performanceCursor: number = 0;
private _lcpEntry: LargestContentfulPaint | undefined;
private _clsEntry: LayoutShift | undefined;
private _updatedClsEntry: LayoutShift | undefined;

public constructor() {
if (!isNodeEnv() && global?.performance) {
Expand Down Expand Up @@ -92,9 +94,9 @@ export class MetricsInstrumentation {

// capture web vitals

const firstHidden = getFirstHidden();
const firstHidden = getVisibilityWatcher();
// Only report if the page wasn't hidden prior to the web vital.
const shouldRecord = entry.startTime < firstHidden.timeStamp;
const shouldRecord = entry.startTime < firstHidden.firstHiddenTime;

if (entry.name === 'first-paint' && shouldRecord) {
logger.log('[Measurements] Adding FP');
Expand Down Expand Up @@ -187,6 +189,12 @@ export class MetricsInstrumentation {
});
}

// If FCP is not recorded we should not record the updated cls value
// according to the new definition of CLS.
if (!('fcp' in this._measurements)) {
delete this._measurements['updated-cls'];
}

transaction.setMeasurements(this._measurements);
this._tagMetricInfo(transaction);
}
Expand Down Expand Up @@ -217,17 +225,23 @@ export class MetricsInstrumentation {
// See: https://developer.mozilla.org/en-US/docs/Web/API/LayoutShift
if (this._clsEntry && this._clsEntry.sources) {
logger.log('[Measurements] Adding CLS Data');
this._clsEntry.sources.map((source, index) =>
this._clsEntry.sources.forEach((source, index) =>
transaction.setTag(`cls.source.${index + 1}`, htmlTreeAsString(source.node)),
);
}

if (this._updatedClsEntry && this._updatedClsEntry.sources) {
logger.log('[Measurements] Adding Updated CLS Data');
this._updatedClsEntry.sources.forEach((source, index) =>
transaction.setTag(`updated-cls.source.${index + 1}`, htmlTreeAsString(source.node)),
);
}
}

/** Starts tracking the Cumulative Layout Shift on the current page. */
private _trackCLS(): void {
getCLS(metric => {
const entry = metric.entries.pop();

if (!entry) {
return;
}
Expand All @@ -236,20 +250,32 @@ export class MetricsInstrumentation {
this._measurements['cls'] = { value: metric.value };
this._clsEntry = entry as LayoutShift;
});

// See:
// https://web.dev/evolving-cls/
// https://web.dev/cls-web-tooling/
getUpdatedCLS(metric => {
const entry = metric.entries.pop();
if (!entry) {
return;
}

logger.log('[Measurements] Adding Updated CLS');
this._measurements['updated-cls'] = { value: metric.value };
this._updatedClsEntry = entry as LayoutShift;
});
}

/**
* Capture the information of the user agent.
*/
private _trackNavigator(transaction: Transaction): void {
const navigator = global.navigator as null | (Navigator & NavigatorNetworkInformation & NavigatorDeviceMemory);

if (!navigator) {
return;
}

// track network connectivity

const connection = navigator.connection;
if (connection) {
if (connection.effectiveType) {
Expand Down
26 changes: 23 additions & 3 deletions packages/tracing/src/browser/web-vitals/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,15 +2,35 @@

> A modular library for measuring the [Web Vitals](https://web.dev/vitals/) metrics on real users.

This was vendored from: https://github.com/GoogleChrome/web-vitals
This was vendored from: https://github.com/GoogleChrome/web-vitals: v2.1.0

The commit SHA used is: [56c736b7c4e80f295bc8a98017671c95231fa225](https://github.com/GoogleChrome/web-vitals/tree/56c736b7c4e80f295bc8a98017671c95231fa225)
The commit SHA used is: [3f3338d994f182172d5b97b22a0fcce0c2846908](https://github.com/GoogleChrome/web-vitals/tree/3f3338d994f182172d5b97b22a0fcce0c2846908)

Current vendored web vitals are:

- LCP (Largest Contentful Paint)
- FID (First Input Delay)
- CLS (Cumulative Layout Shift)

# License
## Notable Changes from web-vitals library

This vendored web-vitals library is meant to be used in conjunction with the `@sentry/tracing` `BrowserTracing` integration.
As such, logic around `BFCache` and multiple reports were removed from the library as our web-vitals only report once per pageload.

## License

[Apache 2.0](https://github.com/GoogleChrome/web-vitals/blob/master/LICENSE)

## CHANGELOG

https://github.com/getsentry/sentry-javascript/pull/3781
- Bumped from Web Vitals v0.2.4 to v2.1.0

https://github.com/getsentry/sentry-javascript/pull/3515
- Remove support for Time to First Byte (TTFB)

https://github.com/getsentry/sentry-javascript/pull/2964
- Added support for Cumulative Layout Shift (CLS) and Time to First Byte (TTFB)

https://github.com/getsentry/sentry-javascript/pull/2909
- Added support for FID (First Input Delay) and LCP (Largest Contentful Paint)
18 changes: 7 additions & 11 deletions packages/tracing/src/browser/web-vitals/getCLS.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,31 +34,27 @@ export interface LayoutShiftAttribution {
currentRect: DOMRectReadOnly;
}

export const getCLS = (onReport: ReportHandler, reportAllChanges = false): void => {
export const getCLS = (onReport: ReportHandler, reportAllChanges?: boolean): void => {
const metric = initMetric('CLS', 0);

let report: ReturnType<typeof bindReporter>;

const entryHandler = (entry: LayoutShift): void => {
// Only count layout shifts without recent user input.
if (!entry.hadRecentInput) {
(metric.value as number) += entry.value;
metric.entries.push(entry);
report();
if (report) {
report();
}
}
};

const po = observe('layout-shift', entryHandler as PerformanceEntryHandler);
if (po) {
report = bindReporter(onReport, metric, po, reportAllChanges);
report = bindReporter(onReport, metric, reportAllChanges);

onHidden(({ isUnloading }) => {
onHidden(() => {
po.takeRecords().map(entryHandler as PerformanceEntryHandler);

if (isUnloading) {
metric.isFinal = true;
}
report();
report(true);
});
}
};
59 changes: 8 additions & 51 deletions packages/tracing/src/browser/web-vitals/getFID.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,75 +15,32 @@
*/

import { bindReporter } from './lib/bindReporter';
import { getFirstHidden } from './lib/getFirstHidden';
import { getVisibilityWatcher } from './lib/getVisibilityWatcher';
import { initMetric } from './lib/initMetric';
import { observe, PerformanceEntryHandler } from './lib/observe';
import { onHidden } from './lib/onHidden';
import { ReportHandler } from './types';
import { PerformanceEventTiming, ReportHandler } from './types';

interface FIDPolyfillCallback {
(value: number, event: Event): void;
}

interface FIDPolyfill {
onFirstInputDelay: (onReport: FIDPolyfillCallback) => void;
}

declare global {
interface Window {
perfMetrics: FIDPolyfill;
}
}

// https://wicg.github.io/event-timing/#sec-performance-event-timing
interface PerformanceEventTiming extends PerformanceEntry {
processingStart: DOMHighResTimeStamp;
cancelable?: boolean;
target?: Element;
}

export const getFID = (onReport: ReportHandler): void => {
export const getFID = (onReport: ReportHandler, reportAllChanges?: boolean): void => {
const visibilityWatcher = getVisibilityWatcher();
const metric = initMetric('FID');
const firstHidden = getFirstHidden();
let report: ReturnType<typeof bindReporter>;

const entryHandler = (entry: PerformanceEventTiming): void => {
// Only report if the page wasn't hidden prior to the first input.
if (entry.startTime < firstHidden.timeStamp) {
if (report && entry.startTime < visibilityWatcher.firstHiddenTime) {
metric.value = entry.processingStart - entry.startTime;
metric.entries.push(entry);
metric.isFinal = true;
report();
report(true);
}
};

const po = observe('first-input', entryHandler as PerformanceEntryHandler);
const report = bindReporter(onReport, metric, po);

if (po) {
report = bindReporter(onReport, metric, reportAllChanges);
onHidden(() => {
po.takeRecords().map(entryHandler as PerformanceEntryHandler);
po.disconnect();
}, true);
} else {
if (window.perfMetrics && window.perfMetrics.onFirstInputDelay) {
window.perfMetrics.onFirstInputDelay((value: number, event: Event) => {
// Only report if the page wasn't hidden prior to the first input.
if (event.timeStamp < firstHidden.timeStamp) {
metric.value = value;
metric.isFinal = true;
metric.entries = [
{
entryType: 'first-input',
name: event.type,
target: event.target,
cancelable: event.cancelable,
startTime: event.timeStamp,
processingStart: event.timeStamp + value,
} as PerformanceEventTiming,
];
report();
}
});
}
}
};
39 changes: 23 additions & 16 deletions packages/tracing/src/browser/web-vitals/getLCP.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,11 +15,10 @@
*/

import { bindReporter } from './lib/bindReporter';
import { getFirstHidden } from './lib/getFirstHidden';
import { getVisibilityWatcher } from './lib/getVisibilityWatcher';
import { initMetric } from './lib/initMetric';
import { observe, PerformanceEntryHandler } from './lib/observe';
import { onHidden } from './lib/onHidden';
import { whenInput } from './lib/whenInput';
import { ReportHandler } from './types';

// https://wicg.github.io/largest-contentful-paint/#sec-largest-contentful-paint-interface
Expand All @@ -33,10 +32,11 @@ export interface LargestContentfulPaint extends PerformanceEntry {
toJSON(): Record<string, string>;
}

export const getLCP = (onReport: ReportHandler, reportAllChanges = false): void => {
const metric = initMetric('LCP');
const firstHidden = getFirstHidden();
const reportedMetricIDs: Record<string, boolean> = {};

export const getLCP = (onReport: ReportHandler, reportAllChanges?: boolean): void => {
const visibilityWatcher = getVisibilityWatcher();
const metric = initMetric('LCP');
let report: ReturnType<typeof bindReporter>;

const entryHandler = (entry: PerformanceEntry): void => {
Expand All @@ -46,30 +46,37 @@ export const getLCP = (onReport: ReportHandler, reportAllChanges = false): void

// If the page was hidden prior to paint time of the entry,
// ignore it and mark the metric as final, otherwise add the entry.
if (value < firstHidden.timeStamp) {
if (value < visibilityWatcher.firstHiddenTime) {
metric.value = value;
metric.entries.push(entry);
} else {
metric.isFinal = true;
}

report();
if (report) {
report();
}
};

const po = observe('largest-contentful-paint', entryHandler);

if (po) {
report = bindReporter(onReport, metric, po, reportAllChanges);
report = bindReporter(onReport, metric, reportAllChanges);

const onFinal = (): void => {
if (!metric.isFinal) {
const stopListening = (): void => {
if (!reportedMetricIDs[metric.id]) {
po.takeRecords().map(entryHandler as PerformanceEntryHandler);
metric.isFinal = true;
report();
po.disconnect();
reportedMetricIDs[metric.id] = true;
report(true);
}
};

void whenInput().then(onFinal);
onHidden(onFinal, true);
// Stop listening after input. Note: while scrolling is an input that
// stop LCP observation, it's unreliable since it can be programmatically
// generated. See: https://github.com/GoogleChrome/web-vitals/issues/75
['keydown', 'click'].forEach(type => {
addEventListener(type, stopListening, { once: true, capture: true });
});

onHidden(stopListening, true);
}
};
Loading