8000 feat(tracing): Track Total Blocking Time · jcomo/sentry-javascript@a42c09b · GitHub
[go: up one dir, main page]

Skip to content

Commit a42c09b

Browse files
authored
feat(tracing): Track Total Blocking Time
1 parent 4bec90f commit a42c09b

File tree

2 files changed

+132
-50
lines changed

2 files changed

+132
-50
lines changed

packages/tracing/src/browser/metrics.ts

Lines changed: 61 additions & 50 deletions
Original file line numberDiff line numberDiff line change
@@ -69,62 +69,73 @@ export class MetricsInstrumentation {
6969
global.performance
7070
.getEntries()
7171
.slice(this._performanceCursor)
72-
.forEach((entry: Record<string, any>) => {
73-
const startTime = msToSec(entry.startTime as number);
74-
const duration = msToSec(entry.duration as number);
75-
76-
if (transaction.op === 'navigation' && timeOrigin + startTime < transaction.startTimestamp) {
77-
return;
78-
}
79-
80-
switch (entry.entryType) {
81-
case 'navigation': {
82-
addNavigationSpans(transaction, entry, timeOrigin);
83-
responseStartTimestamp = timeOrigin + msToSec(entry.responseStart as number);
84-
requestStartTimestamp = timeOrigin + msToSec(entry.requestStart as number);
85-
break;
72+
.forEach(
73+
// eslint-disable-next-line complexity
74+
(entry: Record<string, any>) => {
75+
const startTime = msToSec(entry.startTime as number);
76+
const duration = msToSec(entry.duration as number);
77+
78+
if (transaction.op === 'navigation' && timeOrigin + startTime < transaction.startTimestamp) {
79+
return;
8680
}
87-
case 'mark':
88-
case 'paint':
89-
case 'measure': {
90-
const startTimestamp = addMeasureSpans(transaction, entry, startTime, duration, timeOrigin);
91-
if (tracingInitMarkStartTime === undefined && entry.name === 'sentry-tracing-init') {
92-
tracingInitMarkStartTime = startTimestamp;
93-
}
94-
95-
// capture web vitals
96-
97-
const firstHidden = getVisibilityWatcher();
98-
// Only report if the page wasn't hidden prior to the web vital.
99-
const shouldRecord = entry.startTime < firstHidden.firstHiddenTime;
10081

101-
if (entry.name === 'first-paint' && shouldRecord) {
102-
logger.log('[Measurements] Adding FP');
103-
this._measurements['fp'] = { value: entry.startTime };
104-
this._measurements['mark.fp'] = { value: startTimestamp };
82+
switch (entry.entryType) {
83+
case 'navigation': {
84+
addNavigationSpans(transaction, entry, timeOrigin);
85+
responseStartTimestamp = timeOrigin + msToSec(entry.responseStart as number);
86+
requestStartTimestamp = timeOrigin + msToSec(entry.requestStart as number);
87+
break;
10588
}
106-
107-
if (entry.name === 'first-contentful-paint' && shouldRecord) {
108-
logger.log('[Measurements] Adding FCP');
109-
this._measurements['fcp'] = { value: entry.startTime };
110-
this._measurements['mark.fcp'] = { value: startTimestamp };
89+
case 'mark':
90+
case 'paint':
91+
case 'measure': {
92+
const startTimestamp = addMeasureSpans(transaction, entry, startTime, duration, timeOrigin);
93+
if (tracingInitMarkStartTime === undefined && entry.name === 'sentry-tracing-init') {
94+
tracingInitMarkStartTime = startTimestamp;
95+
}
96+
97+
// capture web vitals
98+
99+
const firstHidden = getVisibilityWatcher();
100+
// Only report if the page wasn't hidden prior to the web vital.
101+
const shouldRecord = entry.startTime < firstHidden.firstHiddenTime;
102+
103+
if (entry.name === 'first-paint' && shouldRecord) {
104+
logger.log('[Measurements] Adding FP');
105+
this._measurements['fp'] = { value: entry.startTime };
106+
this._measurements['mark.fp'] = { value: startTimestamp };
107+
}
108+
109+
if (entry.name === 'first-contentful-paint' && shouldRecord) {
110+
logger.log('[Measurements] Adding FCP');
111+
this._measurements['fcp'] = { value: entry.startTime };
112+
this._measurements['mark.fcp'] = { value: startTimestamp };
113+
}
114+
115+
if (this._measurements['fcp']?.value && entry.startTime > this._measurements['fcp'].value) {
116+
logger.log('[Measurements] Adding TBT');
117+
const entryBlockingTime = entry.duration - 50;
118+
if (entryBlockingTime > 0) {
119+
this._measurements['tbt'] = { value: (this._measurements['tbt']?.value || 0) + entryBlockingTime };
120+
}
121+
}
122+
123+
break;
111124
}
112-
113-
break;
114-
}
115-
case 'resource': {
116-
const resourceName = (entry.name as string).replace(window.location.origin, '');
117-
const endTimestamp = addResourceSpans(transaction, entry, resourceName, startTime, duration, timeOrigin);
118-
// We remember the entry script end time to calculate the difference to the first init mark
119-
if (entryScriptStartTimestamp === undefined && (entryScriptSrc || '').indexOf(resourceName) > -1) {
120-
entryScriptStartTimestamp = endTimestamp;
125+
case 'resource': {
126+
const resourceName = (entry.name as string).replace(window.location.origin, '');
127+
const endTimestamp = addResourceSpans(transaction, entry, resourceName, startTime, duration, timeOrigin);
128+
// We remember the entry script end time to calculate the difference to the first init mark
129+
if (entryScriptStartTimestamp === undefined && (entryScriptSrc || '').indexOf(resourceName) > -1) {
130+
entryScriptStartTimestamp = endTimestamp;
131+
}
132+
break;
121133
}
122-
break;
134+
default:
135+
// Ignore other entry types.
123136
}
124-
default:
125-
// Ignore other entry types.
126-
}
127-
});
137+
},
138+
);
10000
128139

129140
if (entryScriptStartTimestamp !== undefined && tracingInitMarkStartTime !== undefined) {
130141
_startChild(transaction, {

packages/tracing/test/browser/metrics.test.ts

Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,10 @@
1+
// `browserPerformanceTimeOrigin` is an IIFE
2+
// so we need to mock it before importing modules that reference @sentry/utils.
3+
jest.mock('@sentry/utils', () => ({
4+
...jest.requireActual('@sentry/utils'),
5+
browserPerformanceTimeOrigin: 1,
6+
}));
7+
18
import { Span, Transaction } from '../../src';
29
import { _startChild, addResourceSpans, MetricsInstrumentation, ResourceEntry } from '../../src/browser/metrics';
310
import { addDOMPropertiesToGlobal } from '../testutils';
@@ -196,4 +203,68 @@ describe('MetricsInstrumentation', () => {
196203

197204
trackers.forEach(tracker => expect(tracker).toBeCalled());
198205
});
206+
207+
describe('addPerformanceEntries', () => {
208+
const transaction = new Transaction({ name: 'test_transaction' });
209+
210+
addDOMPropertiesToGlobal(['performance', 'document', 'addEventListener', 'window']);
211+
global.performance.now = jest.fn();
212+
global.performance.getEntries = jest.fn(() => [
213+
{
214+
startTime: 100,
215+
duration: 100,
216+
entryType: 'measure',
217+
name: 'first-paint',
218+
},
219+
{
220+
startTime: 400,
221+
duration: 200,
222+
entryType: 'measure',
223+
name: 'first-contentful-paint',
224+
},
225+
{
226+
startTime: 420,
227+
duration: 150,
228+
entryType: 'measure',
229+
},
230+
{
231+
startTime: 450,
232+
duration: 40,
233+
entryType: 'measure',
234+
},
235+
{
236+
startTime: 450,
237+
duration: 50,
238+
entryType: 'measure',
239+
},
240+
{
241+
startTime: 440,
242+
duration: 51,
243+
entryType: 'measure',
244+
},
245+
{
246+
startTime: 450,
247+
duration: 83,
248+
entryType: 'measure',
249+
},
250+
]);
251+
252+
const instrumentation = new MetricsInstrumentation();
253+
instrumentation.addPerformanceEntries(transaction);
254+
255+
it('records "first-paint" correctly.', () => {
256+
expect((instrumentation as any)._measurements).toHaveProperty('fp');
257+
expect((instrumentation as any)._measurements['fp']).toStrictEqual({ value: 100 });
258+
});
259+
260+
it('records "first-contentful-paint" correctly.', () => {
261+
expect((instrumentation as any)._measurements).toHaveProperty('fcp');
262+
expect((instrumentation as any)._measurements['fcp']).toStrictEqual({ value: 400 });
263+
});
264+
265+
it('records "total-blocking-time" correctly.', () => {
266+
expect((instrumentation as any)._measurements).toHaveProperty('tbt');
267+
expect((instrumentation as any)._measurements['tbt']).toStrictEqual({ value: 134 });
268+
});
269+
});
199270
});

0 commit comments

Comments
 (0)
0