8000 feat(rum): Add more web vitals: CLS and TTFB (#2964) · rchl/sentry-javascript@9961e17 · GitHub
[go: up one dir, main page]

Skip to content

Commit 9961e17

Browse files
authored
feat(rum): Add more web vitals: CLS and TTFB (getsentry#2964)
1 parent 8601648 commit 9961e17

File tree

3 files changed

+212
-0
lines changed

3 files changed

+212
-0
lines changed

packages/tracing/src/browser/metrics.ts

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,8 +6,10 @@ import { browserPerformanceTimeOrigin, getGlobalObject, logger } from '@sentry/u
66
import { Span } from '../span';
77
import { Transaction } from '../transaction';
88
import { msToSec } from '../utils';
9+
import { getCLS } from './web-vitals/getCLS';
910
import { getFID } from './web-vitals/getFID';
1011
import { getLCP } from './web-vitals/getLCP';
12+
import { getTTFB } from './web-vitals/getTTFB';
1113

1214
const global = getGlobalObject<Window>();
1315

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

28+
this._trackCLS();
2629
this._trackLCP();
2730
this._trackFID();
31+
this._trackTTFB();
2832
}
2933
}
3034

@@ -126,6 +130,20 @@ export class MetricsInstrumentation {
126130
}
127131
}
128132

133+
/** Starts tracking the Cumulative Layout Shift on the current page. */
134+
private _trackCLS(): void {
135+
getCLS(metric => {
136+
const entry = metric.entries.pop();
137+
138+
if (!entry) {
139+
return;
140+
}
141+
142+
logger.log('[Measurements] Adding CLS');
143+
this._measurements['cls'] = { value: metric.value };
144+
});
145+
}
146+
129147
/** Starts tracking the Largest Contentful Paint on the current page. */
130148
private _trackLCP(): void {
131149
getLCP(metric => {
@@ -159,6 +177,24 @@ export class MetricsInstrumentation {
159177
this._measurements['mark.fid'] = { value: timeOrigin + startTime };
160178
});
161179
}
180+
181+
/** Starts tracking the Time to First Byte on the current page. */
182+
private _trackTTFB(): void {
183+
getTTFB(metric => {
184+
const entry = metric.entries.pop();
185+
186+
if (!entry) {
187+
return;
188+
}
189+
190+
logger.log('[Measurements] Adding TTFB');
191+
this._measurements['ttfb'] = { value: metric.value };
192+
193+
// Capture the time spent making the request and receiving the first byte of the response
194+
const requestTime = metric.value - ((metric.entries[0] ?? entry) as PerformanceNavigationTiming).requestStart;
195+
this._measurements['ttfb.requestTime'] = { value: requestTime };
196+
});
197+
}
162198
}
163199

164200
/** Instrument navigation entries */
Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
/*
2+
* Copyright 2020 Google LLC
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
import { bindReporter } from './lib/bindReporter';
18+
import { initMetric } from './lib/initMetric';
19+
import { observe, PerformanceEntryHandler } from './lib/observe';
20+
import { onHidden } from './lib/onHidden';
21+
import { ReportHandler } from './types';
22+
23+
// https://wicg.github.io/layout-instability/#sec-layout-shift
24+
interface LayoutShift extends PerformanceEntry {
25+
value: number;
26+
hadRecentInput: boolean;
27+
}
28+
29+
export const getCLS = (onReport: ReportHandler, reportAllChanges = false): void => {
30+
const metric = initMetric('CLS', 0);
31+
32+
let report: ReturnType<typeof bindReporter>;
33+
34+
const entryHandler = (entry: LayoutShift): void => {
35+
// Only count layout shifts without recent user input.
36+
if (!entry.hadRecentInput) {
37+
(metric.value as number) += entry.value;
38+
metric.entries.push(entry);
39+
report();
40+
}
41+
};
42+
43+
const po = observe('layout-shift', entryHandler as PerformanceEntryHandler);
44+
if (po) {
45+
report = bindReporter(onReport, metric, po, reportAllChanges);
46+
47+
onHidden(({ isUnloading }) => {
48+
po.takeRecords().map(entryHandler as PerformanceEntryHandler);
49+
50+
if (isUnloading) {
51+
metric.isFinal = true;
52+
}
53+
report();
54+
});
55+
}
56+
};
Lines changed: 120 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,120 @@
1+
/*
2+
* Copyright 2020 Google LLC
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
import { initMetric } from './lib/initMetric';
18+
import { ReportHandler } from './types';
19+
20+
interface NavigationEntryShim {
21+
// From `PerformanceNavigationTimingEntry`.
22+
entryType: string;
23+
startTime: number;
24+
25+
// From `performance.timing`.
26+
connectEnd?: number;
27+
connectStart?: number;
28+
domComplete?: number;
29+
domContentLoadedEventEnd?: number;
30+
domContentLoadedEventStart?: number;
31+
domInteractive?: number;
32+
domainLookupEnd?: number;
33+
domainLookupStart?: number;
34+
fetchStart?: number;
35+
loadEventEnd?: number;
36+
loadEventStart?: number;
37+
redirectEnd?: number;
38+
redirectStart?: number;
39+
requestStart?: number;
40+
responseEnd?: number;
41+
responseStart?: number;
42+
secureConnectionStart?: number;
43+
unloadEventEnd?: number;
44+
unloadEventStart?: number;
45+
}
46+
47+
type PerformanceTimingKeys =
48+
| 'connectEnd'
49+
| 'connectStart'
50+
| 'domComplete'
51+
| 'domContentLoadedEventEnd'
52+
| 'domContentLoadedEventStart'
53+
| 'domInteractive'
54+
| 'domainLookupEnd'
55+
| 'domainLookupStart'
56+
| 'fetchStart'
57+
| 'loadEventEnd'
58+
| 'loadEventStart'
59+
| 'redirectEnd'
60+
| 'redirectStart'
61+
| 'requestStart'
62+
| 'responseEnd'
63+
| 'responseStart'
64+
| 'secureConnectionStart'
65+
| 'unloadEventEnd'
66+
| 'unloadEventStart';
67+
68+
const afterLoad = (callback: () => void): void => {
69+
if (document.readyState === 'complete') {
70+
// Queue a task so the callback runs after `loadEventEnd`.
71+
setTimeout(callback, 0);
72+
} else {
73+
// Use `pageshow` so the callback runs after `loadEventEnd`.
74+
addEventListener('pageshow', callback);
75+
}
76+
};
77+
78+
const getNavigationEntryFromPerformanceTiming = (): PerformanceNavigationTiming => {
79+
// Really annoying that TypeScript errors when using `PerformanceTiming`.
80+
// Note: browsers that do not support navigation entries will fall back to using performance.timing
81+
// (with the timestamps converted from epoch time to DOMHighResTimeStamp).
82+
// eslint-disable-next-line deprecation/deprecation
83+
const timing = performance.timing;
84+
85+
const navigationEntry: NavigationEntryShim = {
86+
entryType: 'navigation',
87+
startTime: 0,
88+
};
89+
90+
for (const key in timing) {
91+
if (key !== 'navigationStart' && key !== 'toJSON') {
92+
navigationEntry[key as PerformanceTimingKeys] = Math.max(
93+
timing[key as PerformanceTimingKeys] - timing.navigationStart,
94+
0,
95+
);
96+
}
97+
}
98+
return navigationEntry as PerformanceNavigationTiming;
99+
};
100+
101+
export const getTTFB = (onReport: ReportHandler): void => {
102+
const metric = initMetric('TTFB');
103+
104+
afterLoad(() => {
105+
try {
106+
// Use the NavigationTiming L2 entry if available.
107+
const navigationEntry =
108+
performance.getEntriesByType('navigation')[0] || getNavigationEntryFromPerformanceTiming();
109+
110+
metric.value = metric.delta = (navigationEntry as PerformanceNavigationTiming).responseStart;
111+
112+
metric.entries = [navigationEntry];
113+
metric.isFinal = true;
114+
115+
onReport(metric);
116+
} catch (error) {
117+
// Do nothing.
118+
}
119+
});
120+
};

0 commit comments

Comments
 (0)
0