Skip to content

Commit

Permalink
fix(tracing): [v7] use web-vitals ttfb calculation (#11231)
Browse files Browse the repository at this point in the history
Backport of #11185
  • Loading branch information
AbhiPrasad authored Mar 21, 2024
1 parent add5d5f commit bf4ea76
Show file tree
Hide file tree
Showing 7 changed files with 240 additions and 69 deletions.
2 changes: 2 additions & 0 deletions packages/tracing-internal/src/browser/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,4 +20,6 @@ export {
addClsInstrumentationHandler,
addFidInstrumentationHandler,
addLcpInstrumentationHandler,
addTtfbInstrumentationHandler,
addInpInstrumentationHandler,
} from './instrument';
20 changes: 19 additions & 1 deletion packages/tracing-internal/src/browser/instrument.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import { onFID } from './web-vitals/getFID';
import { onINP } from './web-vitals/getINP';
import { onLCP } from './web-vitals/getLCP';
import { observe } from './web-vitals/lib/observe';
import { onTTFB } from './web-vitals/onTTFB';

type InstrumentHandlerTypePerformanceObserver =
| 'longtask'
Expand All @@ -15,7 +16,7 @@ type InstrumentHandlerTypePerformanceObserver =
| 'resource'
| 'first-input';

type InstrumentHandlerTypeMetric = 'cls' | 'lcp' | 'fid' | 'inp';
type InstrumentHandlerTypeMetric = 'cls' | 'lcp' | 'fid' | 'ttfb' | 'inp';

// We provide this here manually instead of relying on a global, as this is not available in non-browser environements
// And we do not want to expose such types
Expand Down Expand Up @@ -101,6 +102,7 @@ const instrumented: { [key in InstrumentHandlerType]?: boolean } = {};
let _previousCls: Metric | undefined;
let _previousFid: Metric | undefined;
let _previousLcp: Metric | undefined;
let _previousTtfb: Metric | undefined;
let _previousInp: Metric | undefined;

/**
Expand Down Expand Up @@ -131,6 +133,13 @@ export function addLcpInstrumentationHandler(
return addMetricObserver('lcp', callback, instrumentLcp, _previousLcp, stopOnCallback);
}

/**
* Add a callback that will be triggered when a FID metric is available.
*/
export function addTtfbInstrumentationHandler(callback: (data: { metric: Metric }) => void): CleanupHandlerCallback {
return addMetricObserver('ttfb', callback, instrumentTtfb, _previousTtfb);
}

/**
* Add a callback that will be triggered when a FID metric is available.
* Returns a cleanup callback which can be called to remove the instrumentation handler.
Expand Down Expand Up @@ -225,6 +234,15 @@ function instrumentLcp(): StopListening {
});
}

function instrumentTtfb(): StopListening {
return onTTFB(metric => {
triggerHandlers('ttfb', {
metric,
});
_previousTtfb = metric;
});
}

function instrumentInp(): void {
return onINP(metric => {
triggerHandlers('inp', {
Expand Down
64 changes: 28 additions & 36 deletions packages/tracing-internal/src/browser/metrics/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ import {
addInpInstrumentationHandler,
addLcpInstrumentationHandler,
addPerformanceInstrumentationHandler,
addTtfbInstrumentationHandler,
} from '../instrument';
import { WINDOW } from '../types';
import { getVisibilityWatcher } from '../web-vitals/lib/getVisibilityWatcher';
Expand All @@ -30,6 +31,8 @@ import type {
import { _startChild, isMeasurementValue } from './utils';

import { createSpanEnvelope } from '@sentry/core';
import { getNavigationEntry } from '../web-vitals/lib/getNavigationEntry';
import type { TTFBMetric } from '../web-vitals/types/ttfb';

const MAX_INT_AS_BYTES = 2147483647;

Expand Down Expand Up @@ -68,11 +71,13 @@ export function startTrackingWebVitals(): () => void {
const fidCallback = _trackFID();
const clsCallback = _trackCLS();
const lcpCallback = _trackLCP();
const ttfbCallback = _trackTtfb();

return (): void => {
fidCallback();
clsCallback();
lcpCallback();
ttfbCallback();
};
}

Expand Down Expand Up @@ -201,6 +206,18 @@ function _trackFID(): () => void {
});
}

function _trackTtfb(): () => void {
return addTtfbInstrumentationHandler(({ metric }) => {
const entry = metric.entries[metric.entries.length - 1];
if (!entry) {
return;
}

DEBUG_BUILD && logger.log('[Measurements] Adding TTFB');
_measurements['ttfb'] = { value: metric.value, unit: 'millisecond' };
});
}

const INP_ENTRY_MAP: Record<string, 'click' | 'hover' | 'drag' | 'press'> = {
click: 'click',
pointerdown: 'click',
Expand Down Expand Up @@ -308,9 +325,6 @@ export function addPerformanceEntries(transaction: Transaction): void {

const performanceEntries = performance.getEntries();

let responseStartTimestamp: number | undefined;
let requestStartTimestamp: number | undefined;

const { op, start_timestamp: transactionStartTime } = spanToJSON(transaction);

// eslint-disable-next-line @typescript-eslint/no-explicit-any
Expand All @@ -326,8 +340,6 @@ export function addPerformanceEntries(transaction: Transaction): void {
switch (entry.entryType) {
case 'navigation': {
_addNavigationSpans(transaction, entry, timeOrigin);
responseStartTimestamp = timeOrigin + msToSec(entry.responseStart);
requestStartTimestamp = timeOrigin + msToSec(entry.requestStart);
break;
}
case 'mark':
Expand Down Expand Up @@ -365,7 +377,7 @@ export function addPerformanceEntries(transaction: Transaction): void {

// Measurements are only available for pageload transactions
if (op === 'pageload') {
_addTtfbToMeasurements(_measurements, responseStartTimestamp, requestStartTimestamp, transactionStartTime);
_addTtfbRequestTimeToMeasurements(_measurements);

['fcp', 'fp', 'lcp'].forEach(name => {
if (!_measurements[name] || !transactionStartTime || timeOrigin >= transactionStartTime) {
Expand Down Expand Up @@ -657,40 +669,20 @@ function setResourceEntrySizeData(
}

/**
* Add ttfb information to measurements
* Add ttfb request time information to measurements.
*
* Exported for tests
* ttfb information is added via vendored web vitals library.
*/
export function _addTtfbToMeasurements(
_measurements: Measurements,
responseStartTimestamp: number | undefined,
requestStartTimestamp: number | undefined,
transactionStartTime: number | undefined,
): void {
// Generate TTFB (Time to First Byte), which measured as the time between the beginning of the transaction and the
// start of the response in milliseconds
if (typeof responseStartTimestamp === 'number' && transactionStartTime) {
DEBUG_BUILD && logger.log('[Measurements] Adding TTFB');
_measurements['ttfb'] = {
// As per https://developer.mozilla.org/en-US/docs/Web/API/PerformanceResourceTiming/responseStart,
// responseStart can be 0 if the request is coming straight from the cache.
// This might lead us to calculate a negative ttfb if we don't use Math.max here.
//
// This logic is the same as what is in the web-vitals library to calculate ttfb
// https://github.com/GoogleChrome/web-vitals/blob/2301de5015e82b09925238a228a0893635854587/src/onTTFB.ts#L92
// TODO(abhi): We should use the web-vitals library instead of this custom calculation.
value: Math.max(responseStartTimestamp - transactionStartTime, 0) * 1000,
function _addTtfbRequestTimeToMeasurements(_measurements: Measurements): void {
const navEntry = getNavigationEntry() as TTFBMetric['entries'][number];
const { responseStart, requestStart } = navEntry;

if (requestStart <= responseStart) {
DEBUG_BUILD && logger.log('[Measurements] Adding TTFB Request Time');
_measurements['ttfb.requestTime'] = {
value: responseStart - requestStart,
unit: 'millisecond',
};

if (typeof requestStartTimestamp === 'number' && requestStartTimestamp <= responseStartTimestamp) {
// Capture the time spent making the request and receiving the first byte of the response.
// This is the time between the start of the request and the start of the response in milliseconds.
_measurements['ttfb.requestTime'] = {
value: (responseStartTimestamp - requestStartTimestamp) * 1000,
unit: 'millisecond',
};
}
}
}

Expand Down
23 changes: 20 additions & 3 deletions packages/tracing-internal/src/browser/web-vitals/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,18 +4,22 @@
This was vendored from: https://github.com/GoogleChrome/web-vitals: v3.0.4

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

Current vendored web vitals are:

- LCP (Largest Contentful Paint)
- FID (First Input Delay)
- CLS (Cumulative Layout Shift)
- INP (Interaction to Next Paint)
- TTFB (Time to First Byte)

## 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.
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

Expand All @@ -24,16 +28,29 @@ As such, logic around `BFCache` and multiple reports were removed from the libra
## CHANGELOG

https://github.com/getsentry/sentry-javascript/pull/5987

- Bumped from Web Vitals v2.1.0 to v3.0.4

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)

https://github.com/getsentry/sentry-javascript/pull/9690

- Added support for INP (Interaction to Next Paint)

https://github.com/getsentry/sentry-javascript/pull/11231

- Add support for TTFB (Time to First Byte)
91 changes: 91 additions & 0 deletions packages/tracing-internal/src/browser/web-vitals/onTTFB.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
/*
* 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 { WINDOW } from '../types';
import { bindReporter } from './lib/bindReporter';
import { getActivationStart } from './lib/getActivationStart';
import { getNavigationEntry } from './lib/getNavigationEntry';
import { initMetric } from './lib/initMetric';
import type { ReportCallback, ReportOpts } from './types';
import type { TTFBMetric } from './types/ttfb';

/**
* Runs in the next task after the page is done loading and/or prerendering.
* @param callback
*/
const whenReady = (callback: () => void): void => {
if (!WINDOW.document) {
return;
}

if (WINDOW.document.prerendering) {
addEventListener('prerenderingchange', () => whenReady(callback), true);
} else if (WINDOW.document.readyState !== 'complete') {
addEventListener('load', () => whenReady(callback), true);
} else {
// Queue a task so the callback runs after `loadEventEnd`.
setTimeout(callback, 0);
}
};

/**
* Calculates the [TTFB](https://web.dev/time-to-first-byte/) value for the
* current page and calls the `callback` function once the page has loaded,
* along with the relevant `navigation` performance entry used to determine the
* value. The reported value is a `DOMHighResTimeStamp`.
*
* Note, this function waits until after the page is loaded to call `callback`
* in order to ensure all properties of the `navigation` entry are populated.
* This is useful if you want to report on other metrics exposed by the
* [Navigation Timing API](https://w3c.github.io/navigation-timing/). For
* example, the TTFB metric starts from the page's [time
* origin](https://www.w3.org/TR/hr-time-2/#sec-time-origin), which means it
* includes time spent on DNS lookup, connection negotiation, network latency,
* and server processing time.
*/
export const onTTFB = (onReport: ReportCallback, opts?: ReportOpts): void => {
// Set defaults
// eslint-disable-next-line no-param-reassign
opts = opts || {};

// https://web.dev/ttfb/#what-is-a-good-ttfb-score
// const thresholds = [800, 1800];

const metric = initMetric('TTFB');
const report = bindReporter(onReport, metric, opts.reportAllChanges);

whenReady(() => {
const navEntry = getNavigationEntry() as TTFBMetric['entries'][number];

if (navEntry) {
// The activationStart reference is used because TTFB should be
// relative to page activation rather than navigation start if the
// page was prerendered. But in cases where `activationStart` occurs
// after the first byte is received, this time should be clamped at 0.
metric.value = Math.max(navEntry.responseStart - getActivationStart(), 0);

// In some cases the value reported is negative or is larger
// than the current page time. Ignore these cases:
// https://github.com/GoogleChrome/web-vitals/issues/137
// https://github.com/GoogleChrome/web-vitals/issues/162
if (metric.value < 0 || metric.value > performance.now()) return;

metric.entries = [navEntry];

report(true);
}
});
};
Loading

0 comments on commit bf4ea76

Please sign in to comment.