Skip to content

Commit

Permalink
feat(rum): Add more web vitals: CLS and TTFB (#2964)
Browse files Browse the repository at this point in the history
  • Loading branch information
dashed authored Oct 12, 2020
1 parent 8601648 commit 9961e17
Show file tree
Hide file tree
Showing 3 changed files with 212 additions and 0 deletions.
36 changes: 36 additions & 0 deletions packages/tracing/src/browser/metrics.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,10 @@ import { browserPerformanceTimeOrigin, getGlobalObject, logger } from '@sentry/u
import { Span } from '../span';
import { Transaction } from '../transaction';
import { msToSec } from '../utils';
import { getCLS } from './web-vitals/getCLS';
import { getFID } from './web-vitals/getFID';
import { getLCP } from './web-vitals/getLCP';
import { getTTFB } from './web-vitals/getTTFB';

const global = getGlobalObject<Window>();

Expand All @@ -23,8 +25,10 @@ export class MetricsInstrumentation {
global.performance.mark('sentry-tracing-init');
}

this._trackCLS();
this._trackLCP();
this._trackFID();
this._trackTTFB();
}
}

Expand Down Expand Up @@ -126,6 +130,20 @@ export class MetricsInstrumentation {
}
}

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

if (!entry) {
return;
}

logger.log('[Measurements] Adding CLS');
this._measurements['cls'] = { value: metric.value };
});
}

/** Starts tracking the Largest Contentful Paint on the current page. */
private _trackLCP(): void {
getLCP(metric => {
Expand Down Expand Up @@ -159,6 +177,24 @@ export class MetricsInstrumentation {
this._measurements['mark.fid'] = { value: timeOrigin + startTime };
});
}

/** Starts tracking the Time to First Byte on the current page. */
private _trackTTFB(): void {
getTTFB(metric => {
const entry = metric.entries.pop();

if (!entry) {
return;
}

logger.log('[Measurements] Adding TTFB');
this._measurements['ttfb'] = { value: metric.value };

// Capture the time spent making the request and receiving the first byte of the response
const requestTime = metric.value - ((metric.entries[0] ?? entry) as PerformanceNavigationTiming).requestStart;
this._measurements['ttfb.requestTime'] = { value: requestTime };
});
}
}

/** Instrument navigation entries */
Expand Down
56 changes: 56 additions & 0 deletions packages/tracing/src/browser/web-vitals/getCLS.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
/*
* 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 { bindReporter } from './lib/bindReporter';
import { initMetric } from './lib/initMetric';
import { observe, PerformanceEntryHandler } from './lib/observe';
import { onHidden } from './lib/onHidden';
import { ReportHandler } from './types';

// https://wicg.github.io/layout-instability/#sec-layout-shift
interface LayoutShift extends PerformanceEntry {
value: number;
hadRecentInput: boolean;
}

export const getCLS = (onReport: ReportHandler, reportAllChanges = false): 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();
}
};

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

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

if (isUnloading) {
metric.isFinal = true;
}
report();
});
}
};
120 changes: 120 additions & 0 deletions packages/tracing/src/browser/web-vitals/getTTFB.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,120 @@
/*
* 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 { initMetric } from './lib/initMetric';
import { ReportHandler } from './types';

interface NavigationEntryShim {
// From `PerformanceNavigationTimingEntry`.
entryType: string;
startTime: number;

// From `performance.timing`.
connectEnd?: number;
connectStart?: number;
domComplete?: number;
domContentLoadedEventEnd?: number;
domContentLoadedEventStart?: number;
domInteractive?: number;
domainLookupEnd?: number;
domainLookupStart?: number;
fetchStart?: number;
loadEventEnd?: number;
loadEventStart?: number;
redirectEnd?: number;
redirectStart?: number;
requestStart?: number;
responseEnd?: number;
responseStart?: number;
secureConnectionStart?: number;
unloadEventEnd?: number;
unloadEventStart?: number;
}

type PerformanceTimingKeys =
| 'connectEnd'
| 'connectStart'
| 'domComplete'
| 'domContentLoadedEventEnd'
| 'domContentLoadedEventStart'
| 'domInteractive'
| 'domainLookupEnd'
| 'domainLookupStart'
| 'fetchStart'
| 'loadEventEnd'
| 'loadEventStart'
| 'redirectEnd'
| 'redirectStart'
| 'requestStart'
| 'responseEnd'
| 'responseStart'
| 'secureConnectionStart'
| 'unloadEventEnd'
| 'unloadEventStart';

const afterLoad = (callback: () => void): void => {
if (document.readyState === 'complete') {
// Queue a task so the callback runs after `loadEventEnd`.
setTimeout(callback, 0);
} else {
// Use `pageshow` so the callback runs after `loadEventEnd`.
addEventListener('pageshow', callback);
}
};

const getNavigationEntryFromPerformanceTiming = (): PerformanceNavigationTiming => {
// Really annoying that TypeScript errors when using `PerformanceTiming`.
// Note: browsers that do not support navigation entries will fall back to using performance.timing
// (with the timestamps converted from epoch time to DOMHighResTimeStamp).
// eslint-disable-next-line deprecation/deprecation
const timing = performance.timing;

const navigationEntry: NavigationEntryShim = {
entryType: 'navigation',
startTime: 0,
};

for (const key in timing) {
if (key !== 'navigationStart' && key !== 'toJSON') {
navigationEntry[key as PerformanceTimingKeys] = Math.max(
timing[key as PerformanceTimingKeys] - timing.navigationStart,
0,
);
}
}
return navigationEntry as PerformanceNavigationTiming;
};

export const getTTFB = (onReport: ReportHandler): void => {
const metric = initMetric('TTFB');

afterLoad(() => {
try {
// Use the NavigationTiming L2 entry if available.
const navigationEntry =
performance.getEntriesByType('navigation')[0] || getNavigationEntryFromPerformanceTiming();

metric.value = metric.delta = (navigationEntry as PerformanceNavigationTiming).responseStart;

metric.entries = [navigationEntry];
metric.isFinal = true;

onReport(metric);
} catch (error) {
// Do nothing.
}
});
};

0 comments on commit 9961e17

Please sign in to comment.