Skip to content

Commit

Permalink
Merge v3 changes
Browse files Browse the repository at this point in the history
  • Loading branch information
timfish committed Oct 18, 2022
1 parent 9c66c39 commit 1746cf2
Show file tree
Hide file tree
Showing 17 changed files with 744 additions and 157 deletions.
31 changes: 17 additions & 14 deletions packages/tracing/src/browser/metrics/index.ts
Expand Up @@ -5,9 +5,9 @@ import { browserPerformanceTimeOrigin, htmlTreeAsString, logger, WINDOW } from '
import { IdleTransaction } from '../../idletransaction';
import { Transaction } from '../../transaction';
import { getActiveTransaction, msToSec } from '../../utils';
import { getCLS, LayoutShift } from '../web-vitals/getCLS';
import { getFID } from '../web-vitals/getFID';
import { getLCP, LargestContentfulPaint } from '../web-vitals/getLCP';
import { onCLS } from '../web-vitals/getCLS';
import { onFID } from '../web-vitals/getFID';
import { onLCP } from '../web-vitals/getLCP';
import { getVisibilityWatcher } from '../web-vitals/lib/getVisibilityWatcher';
import { observe, PerformanceEntryHandler } from '../web-vitals/lib/observe';
import { NavigatorDeviceMemory, NavigatorNetworkInformation } from '../web-vitals/types';
Expand Down Expand Up @@ -65,7 +65,7 @@ function _trackCLS(): void {
// See:
// https://web.dev/evolving-cls/
// https://web.dev/cls-web-tooling/
getCLS(metric => {
onCLS(metric => {
const entry = metric.entries.pop();
if (!entry) {
return;
Expand All @@ -79,21 +79,24 @@ function _trackCLS(): void {

/** Starts tracking the Largest Contentful Paint on the current page. */
function _trackLCP(reportAllChanges: boolean): void {
getLCP(metric => {
const entry = metric.entries.pop();
if (!entry) {
return;
}
onLCP(
metric => {
const entry = metric.entries.pop();
if (!entry) {
return;
}

__DEBUG_BUILD__ && logger.log('[Measurements] Adding LCP');
_measurements['lcp'] = { value: metric.value, unit: 'millisecond' };
_lcpEntry = entry as LargestContentfulPaint;
}, reportAllChanges);
__DEBUG_BUILD__ && logger.log('[Measurements] Adding LCP');
_measurements['lcp'] = { value: metric.value, unit: 'millisecond' };
_lcpEntry = entry as LargestContentfulPaint;
},
{ reportAllChanges },
);
}

/** Starts tracking the First Input Delay on the current page. */
function _trackFID(): void {
getFID(metric => {
onFID(metric => {
const entry = metric.entries.pop();
if (!entry) {
return;
Expand Down
102 changes: 54 additions & 48 deletions packages/tracing/src/browser/web-vitals/getCLS.ts
Expand Up @@ -16,72 +16,78 @@

import { bindReporter } from './lib/bindReporter';
import { initMetric } from './lib/initMetric';
import { observe, PerformanceEntryHandler } from './lib/observe';
import { observe } from './lib/observe';
import { onHidden } from './lib/onHidden';
import { ReportHandler } from './types';
import { CLSMetric, ReportCallback, ReportOpts } from './types';

// https://wicg.github.io/layout-instability/#sec-layout-shift
export interface LayoutShift extends PerformanceEntry {
value: number;
hadRecentInput: boolean;
sources: Array<LayoutShiftAttribution>;
toJSON(): Record<string, unknown>;
}

export interface LayoutShiftAttribution {
node?: Node;
previousRect: DOMRectReadOnly;
currentRect: DOMRectReadOnly;
}

export const getCLS = (onReport: ReportHandler, reportAllChanges?: boolean): void => {
/**
* Calculates the [CLS](https://web.dev/cls/) value for the current page and
* calls the `callback` function once the value is ready to be reported, along
* with all `layout-shift` performance entries that were used in the metric
* value calculation. The reported value is a `double` (corresponding to a
* [layout shift score](https://web.dev/cls/#layout-shift-score)).
*
* If the `reportAllChanges` configuration option is set to `true`, the
* `callback` function will be called as soon as the value is initially
* determined as well as any time the value changes throughout the page
* lifespan.
*
* _**Important:** CLS should be continually monitored for changes throughout
* the entire lifespan of a page—including if the user returns to the page after
* it's been hidden/backgrounded. However, since browsers often [will not fire
* additional callbacks once the user has backgrounded a
* page](https://developer.chrome.com/blog/page-lifecycle-api/#advice-hidden),
* `callback` is always called when the page's visibility state changes to
* hidden. As a result, the `callback` function might be called multiple times
* during the same page load._
*/
export const onCLS = (onReport: ReportCallback, opts: ReportOpts = {}): void => {
const metric = initMetric('CLS', 0);
let report: ReturnType<typeof bindReporter>;

let sessionValue = 0;
let sessionEntries: PerformanceEntry[] = [];

const entryHandler = (entry: LayoutShift): void => {
// Only count layout shifts without recent user input.
// TODO: Figure out why entry can be undefined
if (entry && !entry.hadRecentInput) {
const firstSessionEntry = sessionEntries[0];
const lastSessionEntry = sessionEntries[sessionEntries.length - 1];
// const handleEntries = (entries: Metric['entries']) => {
const handleEntries = (entries: LayoutShift[]): void => {
entries.forEach(entry => {
// Only count layout shifts without recent user input.
if (!entry.hadRecentInput) {
const firstSessionEntry = sessionEntries[0];
const lastSessionEntry = sessionEntries[sessionEntries.length - 1];

// If the entry occurred less than 1 second after the previous entry and
// less than 5 seconds after the first entry in the session, include the
// entry in the current session. Otherwise, start a new session.
if (
sessionValue &&
sessionEntries.length !== 0 &&
entry.startTime - lastSessionEntry.startTime < 1000 &&
entry.startTime - firstSessionEntry.startTime < 5000
) {
sessionValue += entry.value;
sessionEntries.push(entry);
} else {
sessionValue = entry.value;
sessionEntries = [entry];
}
// If the entry occurred less than 1 second after the previous entry and
// less than 5 seconds after the first entry in the session, include the
// entry in the current session. Otherwise, start a new session.
if (
sessionValue &&
entry.startTime - lastSessionEntry.startTime < 1000 &&
entry.startTime - firstSessionEntry.startTime < 5000
) {
sessionValue += entry.value;
sessionEntries.push(entry);
} else {
sessionValue = entry.value;
sessionEntries = [entry];
}

// If the current session value is larger than the current CLS value,
// update CLS and the entries contributing to it.
if (sessionValue > metric.value) {
metric.value = sessionValue;
metric.entries = sessionEntries;
if (report) {
// If the current session value is larger than the current CLS value,
// update CLS and the entries contributing to it.
if (sessionValue > metric.value) {
metric.value = sessionValue;
metric.entries = sessionEntries;
report();
}
}
}
});
};

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

onHidden(() => {
po.takeRecords().map(entryHandler as PerformanceEntryHandler);
handleEntries(po.takeRecords() as CLSMetric['entries']);
report(true);
});
}
Expand Down
31 changes: 23 additions & 8 deletions packages/tracing/src/browser/web-vitals/getFID.ts
Expand Up @@ -17,29 +17,44 @@
import { bindReporter } from './lib/bindReporter';
import { getVisibilityWatcher } from './lib/getVisibilityWatcher';
import { initMetric } from './lib/initMetric';
import { observe, PerformanceEntryHandler } from './lib/observe';
import { observe } from './lib/observe';
import { onHidden } from './lib/onHidden';
import { PerformanceEventTiming, ReportHandler } from './types';
import { FIDMetric, PerformanceEventTiming, ReportCallback, ReportOpts } from './types';

export const getFID = (onReport: ReportHandler, reportAllChanges?: boolean): void => {
/**
* Calculates the [FID](https://web.dev/fid/) value for the current page and
* calls the `callback` function once the value is ready, along with the
* relevant `first-input` performance entry used to determine the value. The
* reported value is a `DOMHighResTimeStamp`.
*
* _**Important:** since FID is only reported after the user interacts with the
* page, it's possible that it will not be reported for some page loads._
*/
export const onFID = (onReport: ReportCallback, opts: ReportOpts = {}): void => {
const visibilityWatcher = getVisibilityWatcher();
const metric = initMetric('FID');
// eslint-disable-next-line prefer-const
let report: ReturnType<typeof bindReporter>;

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

const po = observe('first-input', entryHandler as PerformanceEntryHandler);
const handleEntries = (entries: FIDMetric['entries']): void => {
(entries as PerformanceEventTiming[]).forEach(handleEntry);
};

const po = observe('first-input', handleEntries);
report = bindReporter(onReport, metric, opts.reportAllChanges);

if (po) {
report = bindReporter(onReport, metric, reportAllChanges);
onHidden(() => {
po.takeRecords().map(entryHandler as PerformanceEntryHandler);
handleEntries(po.takeRecords() as FIDMetric['entries']);
po.disconnect();
}, true);
}
Expand Down
62 changes: 32 additions & 30 deletions packages/tracing/src/browser/web-vitals/getLCP.ts
Expand Up @@ -15,55 +15,57 @@
*/

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

// https://wicg.github.io/largest-contentful-paint/#sec-largest-contentful-paint-interface
export interface LargestContentfulPaint extends PerformanceEntry {
renderTime: DOMHighResTimeStamp;
loadTime: DOMHighResTimeStamp;
size: number;
id: string;
url: string;
element?: Element;
toJSON(): Record<string, string>;
}
import { LCPMetric, ReportCallback, ReportOpts } from './types';

const reportedMetricIDs: Record<string, boolean> = {};

export const getLCP = (onReport: ReportHandler, reportAllChanges?: boolean): void => {
/**
* Calculates the [LCP](https://web.dev/lcp/) value for the current page and
* calls the `callback` function once the value is ready (along with the
* relevant `largest-contentful-paint` performance entry used to determine the
* value). The reported value is a `DOMHighResTimeStamp`.
*
* If the `reportAllChanges` configuration option is set to `true`, the
* `callback` function will be called any time a new `largest-contentful-paint`
* performance entry is dispatched, or once the final value of the metric has
* been determined.
*/
export const onLCP = (onReport: ReportCallback, opts: ReportOpts = {}): void => {
const visibilityWatcher = getVisibilityWatcher();
const metric = initMetric('LCP');
let report: ReturnType<typeof bindReporter>;

const entryHandler = (entry: PerformanceEntry): void => {
// The startTime attribute returns the value of the renderTime if it is not 0,
// and the value of the loadTime otherwise.
const value = entry.startTime;
const handleEntries = (entries: LCPMetric['entries']): void => {
const lastEntry = entries[entries.length - 1] as LargestContentfulPaint;
if (lastEntry) {
// The startTime attribute returns the value of the renderTime if it is
// not 0, and the value of the loadTime otherwise. The activationStart
// reference is used because LCP should be relative to page activation
// rather than navigation start if the page was prerendered.
const value = lastEntry.startTime - getActivationStart();

// 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 < visibilityWatcher.firstHiddenTime) {
metric.value = value;
metric.entries.push(entry);
}

if (report) {
report();
// Only report if the page wasn't hidden prior to LCP.
if (value < visibilityWatcher.firstHiddenTime) {
metric.value = value;
metric.entries = [lastEntry];
report();
}
}
};

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

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

const stopListening = (): void => {
if (!reportedMetricIDs[metric.id]) {
po.takeRecords().map(entryHandler as PerformanceEntryHandler);
handleEntries(po.takeRecords() as LCPMetric['entries']);
po.disconnect();
reportedMetricIDs[metric.id] = true;
report(true);
Expand Down
10 changes: 6 additions & 4 deletions packages/tracing/src/browser/web-vitals/lib/bindReporter.ts
Expand Up @@ -14,25 +14,27 @@
* limitations under the License.
*/

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

export const bindReporter = (
callback: ReportHandler,
callback: ReportCallback,
metric: Metric,
reportAllChanges?: boolean,
): ((forceReport?: boolean) => void) => {
let prevValue: number;
let delta: number;
return (forceReport?: boolean) => {
if (metric.value >= 0) {
if (forceReport || reportAllChanges) {
metric.delta = metric.value - (prevValue || 0);
delta = metric.value - (prevValue || 0);

// Report the metric if there's a non-zero delta 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 || prevValue === undefined) {
if (delta || prevValue === undefined) {
prevValue = metric.value;
metric.delta = delta;
callback(metric);
}
}
Expand Down
Expand Up @@ -20,5 +20,5 @@
* @return {string}
*/
export const generateUniqueID = (): string => {
return `v2-${Date.now()}-${Math.floor(Math.random() * (9e12 - 1)) + 1e12}`;
return `v3-${Date.now()}-${Math.floor(Math.random() * (9e12 - 1)) + 1e12}`;
};
22 changes: 22 additions & 0 deletions packages/tracing/src/browser/web-vitals/lib/getActivationStart.ts
@@ -0,0 +1,22 @@
/*
* Copyright 2022 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 { getNavigationEntry } from './getNavigationEntry';

export const getActivationStart = (): number => {
const navEntry = getNavigationEntry();
return (navEntry && navEntry.activationStart) || 0;
};

0 comments on commit 1746cf2

Please sign in to comment.