Skip to content

feat(browser): Bump web-vitals to 3.5.2 #11391

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 3 commits into from
Apr 4, 2024
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
Original file line number Diff line number Diff line change
Expand Up @@ -4,15 +4,22 @@ import type { Event } from '@sentry/types';
import { sentryTest } from '../../../../utils/fixtures';
import { getFirstSentryEnvelopeRequest, shouldSkipTracingTest } from '../../../../utils/helpers';

sentryTest('should capture TTFB vital.', async ({ getLocalTestPath, page }) => {
sentryTest('should capture TTFB vital.', async ({ getLocalTestUrl, page }) => {
if (shouldSkipTracingTest()) {
sentryTest.skip();
}

const url = await getLocalTestPath({ testDir: __dirname });
const url = await getLocalTestUrl({ testDir: __dirname });
const eventData = await getFirstSentryEnvelopeRequest<Event>(page, url);

expect(eventData.measurements).toBeDefined();
expect(eventData.measurements?.ttfb?.value).toBeDefined();

// If responseStart === 0, ttfb is not reported
// This seems to happen somewhat randomly, so we just ignore this in that case
const responseStart = await page.evaluate("performance.getEntriesByType('navigation')[0].responseStart;");
if (responseStart !== 0) {
expect(eventData.measurements?.ttfb?.value).toBeDefined();
}

expect(eventData.measurements?.['ttfb.requestTime']?.value).toBeDefined();
});
6 changes: 6 additions & 0 deletions packages/tracing-internal/.eslintrc.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,5 +7,11 @@ module.exports = {
'@sentry-internal/sdk/no-optional-chaining': 'off',
},
},
{
files: ['src/browser/web-vitals/**'],
rules: {
'@typescript-eslint/explicit-function-return-type': 'off',
},
},
],
};
2 changes: 1 addition & 1 deletion packages/tracing-internal/src/browser/instrument.ts
Original file line number Diff line number Diff line change
Expand Up @@ -69,7 +69,7 @@ interface Metric {
* support that API). For pages that are restored from the bfcache, this
* value will be 'back-forward-cache'.
*/
navigationType: 'navigate' | 'reload' | 'back-forward' | 'back-forward-cache' | 'prerender';
navigationType: 'navigate' | 'reload' | 'back-forward' | 'back-forward-cache' | 'prerender' | 'restore';
}

type InstrumentHandlerType = InstrumentHandlerTypeMetric | InstrumentHandlerTypePerformanceObserver;
Expand Down
39 changes: 38 additions & 1 deletion packages/tracing-internal/src/browser/metrics/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,9 +16,46 @@ import {
import { WINDOW } from '../types';
import { getNavigationEntry } from '../web-vitals/lib/getNavigationEntry';
import { getVisibilityWatcher } from '../web-vitals/lib/getVisibilityWatcher';
import type { NavigatorDeviceMemory, NavigatorNetworkInformation } from '../web-vitals/types';
import { isMeasurementValue, startAndEndSpan } from './utils';

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
interface NavigatorDeviceMemory {
readonly deviceMemory?: number;
}

const MAX_INT_AS_BYTES = 2147483647;

/**
Expand Down
4 changes: 2 additions & 2 deletions packages/tracing-internal/src/browser/web-vitals/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,10 @@

> 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: v3.0.4
This was vendored from: https://github.com/GoogleChrome/web-vitals: v3.5.2

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

Current vendored web vitals are:

Expand Down
105 changes: 56 additions & 49 deletions packages/tracing-internal/src/browser/web-vitals/getCLS.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,14 +18,19 @@ import { bindReporter } from './lib/bindReporter';
import { initMetric } from './lib/initMetric';
import { observe } from './lib/observe';
import { onHidden } from './lib/onHidden';
import type { CLSMetric, ReportCallback, StopListening } from './types';
import { runOnce } from './lib/runOnce';
import { onFCP } from './onFCP';
import type { CLSMetric, CLSReportCallback, MetricRatingThresholds, ReportOpts } from './types';

/** Thresholds for CLS. See https://web.dev/articles/cls#what_is_a_good_cls_score */
export const CLSThresholds: MetricRatingThresholds = [0.1, 0.25];

/**
* Calculates the [CLS](https://web.dev/cls/) value for the current page and
* Calculates the [CLS](https://web.dev/articles/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)).
* [layout shift score](https://web.dev/articles/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
Expand All @@ -41,63 +46,65 @@ import type { CLSMetric, ReportCallback, StopListening } from './types';
* hidden. As a result, the `callback` function might be called multiple times
* during the same page load._
*/
export const onCLS = (onReport: ReportCallback): StopListening | undefined => {
const metric = initMetric('CLS', 0);
let report: ReturnType<typeof bindReporter>;
export const onCLS = (onReport: CLSReportCallback, opts: ReportOpts = {}): void => {
// Start monitoring FCP so we can only report CLS if FCP is also reported.
// Note: this is done to match the current behavior of CrUX.
onFCP(
runOnce(() => {
const metric = initMetric('CLS', 0);
let report: ReturnType<typeof bindReporter>;

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

// 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];
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) {
report();
}
report();
}
}
});
};

const po = observe('layout-shift', handleEntries);
if (po) {
report = bindReporter(onReport, metric);
};

const stopListening = (): void => {
handleEntries(po.takeRecords() as CLSMetric['entries']);
report(true);
};
const po = observe('layout-shift', handleEntries);
if (po) {
report = bindReporter(onReport, metric, CLSThresholds, opts.reportAllChanges);

onHidden(stopListening);
onHidden(() => {
handleEntries(po.takeRecords() as CLSMetric['entries']);
report(true);
});

return stopListening;
}

return;
// Queue a task to report (if nothing else triggers a report first).
// This allows CLS to be reported as soon as FCP fires when
// `reportAllChanges` is true.
setTimeout(report, 0);
}
}),
);
};
61 changes: 35 additions & 26 deletions packages/tracing-internal/src/browser/web-vitals/getFID.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,43 +19,52 @@ import { getVisibilityWatcher } from './lib/getVisibilityWatcher';
import { initMetric } from './lib/initMetric';
import { observe } from './lib/observe';
import { onHidden } from './lib/onHidden';
import type { FIDMetric, PerformanceEventTiming, ReportCallback } from './types';
import { runOnce } from './lib/runOnce';
import { whenActivated } from './lib/whenActivated';
import type { FIDMetric, FIDReportCallback, MetricRatingThresholds, ReportOpts } from './types';

/** Thresholds for FID. See https://web.dev/articles/fid#what_is_a_good_fid_score */
export const FIDThresholds: MetricRatingThresholds = [100, 300];

/**
* Calculates the [FID](https://web.dev/fid/) value for the current page and
* Calculates the [FID](https://web.dev/articles/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): void => {
const visibilityWatcher = getVisibilityWatcher();
const metric = initMetric('FID');
// eslint-disable-next-line prefer-const
let report: ReturnType<typeof bindReporter>;
export const onFID = (onReport: FIDReportCallback, opts: ReportOpts = {}): void => {
whenActivated(() => {
const visibilityWatcher = getVisibilityWatcher();
const metric = initMetric('FID');
// eslint-disable-next-line prefer-const
let report: ReturnType<typeof bindReporter>;

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

const handleEntries = (entries: FIDMetric['entries']): void => {
(entries as PerformanceEventTiming[]).forEach(handleEntry);
};
const handleEntries = (entries: FIDMetric['entries']) => {
(entries as PerformanceEventTiming[]).forEach(handleEntry);
};

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

if (po) {
onHidden(() => {
handleEntries(po.takeRecords() as FIDMetric['entries']);
po.disconnect();
}, true);
}
if (po) {
onHidden(
runOnce(() => {
handleEntries(po.takeRecords() as FIDMetric['entries']);
po.disconnect();
}),
);
}
});
};
Loading