Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 3 additions & 2 deletions packages/tracing/src/browser/metrics.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,11 +6,11 @@ import { browserPerformanceTimeOrigin, getGlobalObject, htmlTreeAsString, isNode
import { Span } from '../span';
import { Transaction } from '../transaction';
import { msToSec } from '../utils';
import { NavigatorDeviceMemory, NavigatorNetworkInformation } from './types';
import { getCLS } 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 { NavigatorDeviceMemory, NavigatorNetworkInformation } from './web-vitals/types';

const global = getGlobalObject<Window>();

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

/** Add performance related spans to a transaction */
// eslint-disable-next-line complexity
public addPerformanceEntries(transaction: Transaction): void {
if (!global || !global.performance || !global.performance.getEntries || !browserPerformanceTimeOrigin) {
// Gatekeeper if performance API not available
Expand Down Expand Up @@ -188,7 +189,7 @@ export class MetricsInstrumentation {

transaction.setMeasurements(this._measurements);

if (this._lcpEntry) {
if (this._lcpEntry && this._lcpEntry !== undefined) {
logger.log('[Measurements] Adding LCP Data');
// Capture Properties of the LCP element that contributes to the LCP.

Expand Down
49 changes: 49 additions & 0 deletions packages/tracing/src/browser/types.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
// http://wicg.github.io/netinfo/#navigatornetworkinformation-interface
export interface NavigatorNetworkInformation {
readonly connection?: NetworkInformation;
}

// http://wicg.github.io/netinfo/#connection-types
type ConnectionType = 'bluetooth' | 'cellular' | 'ethernet' | 'mixed' | 'none' | 'other' | 'unknown' | 'wifi' | 'wimax';

// http://wicg.github.io/netinfo/#effectiveconnectiontype-enum
type EffectiveConnectionType = '2g' | '3g' | '4g' | 'slow-2g';

// http://wicg.github.io/netinfo/#dom-megabit
type Megabit = number;
// http://wicg.github.io/netinfo/#dom-millisecond
type Millisecond = number;

// http://wicg.github.io/netinfo/#networkinformation-interface
interface NetworkInformation extends EventTarget {
// http://wicg.github.io/netinfo/#type-attribute
readonly type?: ConnectionType;
// http://wicg.github.io/netinfo/#effectivetype-attribute
readonly effectiveType?: EffectiveConnectionType;
// http://wicg.github.io/netinfo/#downlinkmax-attribute
readonly downlinkMax?: Megabit;
// http://wicg.github.io/netinfo/#downlink-attribute
readonly downlink?: Megabit;
// http://wicg.github.io/netinfo/#rtt-attribute
readonly rtt?: Millisecond;
// http://wicg.github.io/netinfo/#savedata-attribute
readonly saveData?: boolean;
// http://wicg.github.io/netinfo/#handling-changes-to-the-underlying-connection
onchange?: EventListener;
}

// https://w3c.github.io/device-memory/#sec-device-memory-js-api
export interface NavigatorDeviceMemory {
readonly deviceMemory?: number;
}

export type NavigationTimingPolyfillEntry = Omit<
PerformanceNavigationTiming,
| 'initiatorType'
| 'nextHopProtocol'
| 'redirectCount'
| 'transferSize'
| 'encodedBodySize'
| 'decodedBodySize'
| 'toJSON'
>;
23 changes: 21 additions & 2 deletions packages/tracing/src/browser/web-vitals/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,13 +4,32 @@

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

The commit SHA used is: [56c736b7c4e80f295bc8a98017671c95231fa225](https://github.com/GoogleChrome/web-vitals/tree/56c736b7c4e80f295bc8a98017671c95231fa225)
Current vendored version: 1.2.2

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

Current vendored web vitals are:

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

# License
## License

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

## Notable differences from `web-vitals` library

<!---
TODO(abhi): Uhh I gotta figure this out lol, it's def something with polyfilled func
-->
## CHANGELOG

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)
19 changes: 10 additions & 9 deletions packages/tracing/src/browser/web-vitals/getCLS.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
import { bindReporter } from './lib/bindReporter';
import { initMetric } from './lib/initMetric';
import { observe, PerformanceEntryHandler } from './lib/observe';
import { onBFCacheRestore } from './lib/onBFCacheRestore';
import { onHidden } from './lib/onHidden';
import { ReportHandler } from './types';

Expand All @@ -26,9 +27,8 @@ interface LayoutShift extends PerformanceEntry {
hadRecentInput: boolean;
}

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

export const getCLS = (onReport: ReportHandler, reportAllChanges?: boolean): void => {
let metric = initMetric('CLS', 0);
let report: ReturnType<typeof bindReporter>;

const entryHandler = (entry: LayoutShift): void => {
Expand All @@ -42,15 +42,16 @@ export const getCLS = (onReport: ReportHandler, reportAllChanges = false): void

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();
});

onBFCacheRestore(() => {
metric = initMetric('CLS', 0);
report = bindReporter(onReport, metric, reportAllChanges);
});
}
};
51 changes: 20 additions & 31 deletions packages/tracing/src/browser/web-vitals/getFID.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,11 +15,14 @@
*/

import { bindReporter } from './lib/bindReporter';
import { finalMetrics } from './lib/finalMetrics';
import { getFirstHidden } from './lib/getFirstHidden';
import { initMetric } from './lib/initMetric';
import { observe, PerformanceEntryHandler } from './lib/observe';
import { onBFCacheRestore } from './lib/onBFCacheRestore';
import { onHidden } from './lib/onHidden';
import { ReportHandler } from './types';
import { firstInputPolyfill, resetFirstInputPolyfill } from './lib/polyfills/firstInputPolyfill';
import { FirstInputPolyfillCallback, PerformanceEventTiming, ReportHandler } from './types';

interface FIDPolyfillCallback {
(value: number, event: Event): void;
Expand All @@ -35,55 +38,41 @@ declare global {
}
}

// 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 => {
const metric = initMetric('FID');
export const getFID = (onReport: ReportHandler, reportAllChanges?: boolean): void => {
let 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) {
metric.value = entry.processingStart - entry.startTime;
metric.entries.push(entry);
metric.isFinal = true;
finalMetrics.add(metric);
report();
}
};

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

if (po) {
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();
}

if (po) {
onBFCacheRestore(() => {
metric = initMetric('FID');
report = bindReporter(onReport, metric, reportAllChanges);
resetFirstInputPolyfill();
firstInputPolyfill(entryHandler as FirstInputPolyfillCallback);
});
}
} else {
resetFirstInputPolyfill();
firstInputPolyfill(entryHandler as FirstInputPolyfillCallback);
}
};
41 changes: 29 additions & 12 deletions packages/tracing/src/browser/web-vitals/getLCP.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,11 +15,12 @@
*/

import { bindReporter } from './lib/bindReporter';
import { finalMetrics } from './lib/finalMetrics';
import { getFirstHidden } from './lib/getFirstHidden';
import { initMetric } from './lib/initMetric';
import { observe, PerformanceEntryHandler } from './lib/observe';
import { onBFCacheRestore } from './lib/onBFCacheRestore';
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,8 +34,8 @@ export interface LargestContentfulPaint extends PerformanceEntry {
toJSON(): Record<string, string>;
}

export const getLCP = (onReport: ReportHandler, reportAllChanges = false): void => {
const metric = initMetric('LCP');
export const getLCP = (onReport: ReportHandler, reportAllChanges?: boolean): void => {
let metric = initMetric('LCP');
const firstHidden = getFirstHidden();

let report: ReturnType<typeof bindReporter>;
Expand All @@ -49,8 +50,6 @@ export const getLCP = (onReport: ReportHandler, reportAllChanges = false): void
if (value < firstHidden.timeStamp) {
metric.value = value;
metric.entries.push(entry);
} else {
metric.isFinal = true;
}

report();
Expand All @@ -59,17 +58,35 @@ export const getLCP = (onReport: ReportHandler, reportAllChanges = false): void
const po = observe('largest-contentful-paint', entryHandler);

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

const onFinal = (): void => {
if (!metric.isFinal) {
report = bindReporter(onReport, metric, reportAllChanges);
const stopListening = (): void => {
if (!finalMetrics.has(metric)) {
po.takeRecords().map(entryHandler as PerformanceEntryHandler);
metric.isFinal = true;
po.disconnect();
finalMetrics.add(metric);
report();
}
};

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);

onBFCacheRestore(event => {
metric = initMetric('LCP');
report = bindReporter(onReport, metric, reportAllChanges);
requestAnimationFrame(() => {
requestAnimationFrame(() => {
metric.value = performance.now() - event.timeStamp;
finalMetrics.add(metric);
report();
});
});
});
}
};
17 changes: 5 additions & 12 deletions packages/tracing/src/browser/web-vitals/lib/bindReporter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,29 +15,22 @@
*/

import { Metric, ReportHandler } from '../types';
import { finalMetrics } from './finalMetrics';

export const bindReporter = (
callback: ReportHandler,
metric: Metric,
po: PerformanceObserver | undefined,
observeAllUpdates?: boolean,
): (() => void) => {
export const bindReporter = (callback: ReportHandler, metric: Metric, reportAllChanges?: boolean): (() => void) => {
let prevValue: number;
return () => {
if (po && metric.isFinal) {
po.disconnect();
}
if (metric.value >= 0) {
if (observeAllUpdates || metric.isFinal || document.visibilityState === 'hidden') {
if (reportAllChanges || finalMetrics.has(metric) || document.visibilityState === 'hidden') {
metric.delta = metric.value - (prevValue || 0);

// Report the metric if there's a non-zero delta, if the metric is
// final, or if no previous value exists (which can happen in the case
// of the document becoming hidden when the metric value is 0).
// See: https://github.com/GoogleChrome/web-vitals/issues/14
if (metric.delta || metric.isFinal || prevValue === undefined) {
callback(metric);
if (metric.delta || prevValue === undefined) {
prevValue = metric.value;
callback(metric);
}
}
}
Expand Down
19 changes: 19 additions & 0 deletions packages/tracing/src/browser/web-vitals/lib/finalMetrics.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
/*
* Copyright 2020 Google LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

import { Metric } from '../types';

export const finalMetrics: WeakSet<Metric> | Set<Metric> = typeof WeakSet === 'function' ? new WeakSet() : new Set();
Original file line number Diff line number Diff line change
Expand Up @@ -15,10 +15,10 @@
*/

/**
* Performantly generate a unique, 27-char string by combining the current
* timestamp with a 13-digit random number.
* Performantly generate a unique, 30-char string by combining a version
* number, the current timestamp with a 13-digit number integer.
* @return {string}
*/
export const generateUniqueID = (): string => {
return `${Date.now()}-${Math.floor(Math.random() * (9e12 - 1)) + 1e12}`;
return `v1-${Date.now()}-${Math.floor(Math.random() * (9e12 - 1)) + 1e12}`;
};
Loading