browsertime 27.3.0 → 27.4.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +14 -0
- package/bin/browsertime.js +30 -4
- package/lib/chrome/coverage.js +232 -0
- package/lib/chrome/webdriver/chromium.js +14 -0
- package/lib/core/engine/collector.js +8 -0
- package/lib/support/cli.js +15 -1
- package/lib/support/har/index.js +5 -1
- package/package.json +2 -2
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,19 @@
|
|
|
1
1
|
# Browsertime changelog (we do [semantic versioning](https://semver.org))
|
|
2
2
|
|
|
3
|
+
## 27.4.1 - 2026-05-25
|
|
4
|
+
|
|
5
|
+
### Fixed
|
|
6
|
+
* `--chrome.coverage` no longer reports "0 B unused" for functions that executed at least once. The JS used-bytes calculation took the union of every V8 range with count > 0, but V8 returns nested ranges — an outer range over the whole function body and inner ranges over sub-blocks. If a function ran at all, its outer range already covered the whole body and the inner count == 0 ranges (the actual dead branches) got absorbed into the union and contributed nothing. On modern bundles where module-evaluation top-level code runs for almost every function, the per-script unused-% collapsed to roughly zero across the board, leaving sitespeed.io rendering a "0 B unused" column on pages that obviously ship plenty of dead code. The calculation now walks the ranges as nested intervals, painting the innermost range's count at every byte (the same definition Chrome DevTools' Coverage panel uses), so dead branches inside executed functions show up correctly. CSS rule-usage tracking returns flat, non-overlapping ranges and is unchanged [#2484](https://github.com/sitespeedio/browsertime/pull/2484).
|
|
7
|
+
|
|
8
|
+
## 27.4.0 - 2026-05-24
|
|
9
|
+
|
|
10
|
+
### Added
|
|
11
|
+
* JS/CSS coverage collection for Chrome. `--chrome.coverage` turns it on for every iteration for users who want the data and accept that detailed V8 coverage deoptimizes scripts and will skew their timing metrics. `--enableProfileRun` also turns coverage on for Chrome alongside the existing trace, so users who want coverage without affecting their timings can lean on the same extra-iteration pattern they already use for tracing — the result is merged back into the main `browsertime.json` so consumers like sitespeed.io see the data in the same place they read every other metric. Each entry has a per-file breakdown (`url`, `totalBytes`, `usedBytes`, `unusedBytes`, `unusedPercent`) [#2480](https://github.com/sitespeedio/browsertime/pull/2480).
|
|
12
|
+
|
|
13
|
+
### Fixed
|
|
14
|
+
* HAR post-processing no longer throws when a page has no matching entries. Occasionally a HAR contains a page whose `pageref` has no entries — typically a navigation that didn't produce any requests, or an extension-injected page. `getDocumentRequests` and `getFinalURL` dereferenced `shift()` / `pop()` without a guard, and `getMainDocumentTimings` wrapped its entire per-page loop in a single try/catch so one bad page wiped out the main-document timings for every other page in the same HAR. Downstream this surfaced as a noisy triplet of warnings followed by an empty coach-data payload in sitespeed.io. The three crash points are now guarded and unresolvable pages are skipped [#2483](https://github.com/sitespeedio/browsertime/pull/2483).
|
|
15
|
+
* Bumped `geckodriver` to a version with native ARM-64 support [#2482](https://github.com/sitespeedio/browsertime/pull/2482).
|
|
16
|
+
|
|
3
17
|
## 27.3.0 - 2026-05-20
|
|
4
18
|
|
|
5
19
|
### Added
|
package/bin/browsertime.js
CHANGED
|
@@ -95,12 +95,16 @@ async function run(urls, options) {
|
|
|
95
95
|
scriptsByCategory = merge(scriptsByCategory, userScripts);
|
|
96
96
|
}
|
|
97
97
|
|
|
98
|
+
let result;
|
|
99
|
+
let storageManager;
|
|
100
|
+
let jsonName;
|
|
101
|
+
|
|
98
102
|
try {
|
|
99
103
|
if (options.preWarmServer) {
|
|
100
104
|
await preWarmServer(urls, options);
|
|
101
105
|
}
|
|
102
106
|
await engine.start();
|
|
103
|
-
|
|
107
|
+
result = await engine.runMultiple(urls, scriptsByCategory);
|
|
104
108
|
let saveOperations = [];
|
|
105
109
|
|
|
106
110
|
// TODO setup by name
|
|
@@ -109,9 +113,9 @@ async function run(urls, options) {
|
|
|
109
113
|
if (Array.isArray(firstUrl)) {
|
|
110
114
|
firstUrl = firstUrl[0];
|
|
111
115
|
}
|
|
112
|
-
|
|
116
|
+
storageManager = new StorageManager(firstUrl, options);
|
|
113
117
|
const harName = options.har ?? 'browsertime';
|
|
114
|
-
|
|
118
|
+
jsonName = options.output ?? 'browsertime';
|
|
115
119
|
|
|
116
120
|
saveOperations.push(storageManager.writeJson(jsonName + '.json', result));
|
|
117
121
|
|
|
@@ -169,6 +173,7 @@ async function run(urls, options) {
|
|
|
169
173
|
options.chrome.traceCategory = [
|
|
170
174
|
'disabled-by-default-v8.cpu_profiler'
|
|
171
175
|
];
|
|
176
|
+
options.chrome.coverage = true;
|
|
172
177
|
}
|
|
173
178
|
}
|
|
174
179
|
if (options.enableVideoRun) {
|
|
@@ -186,9 +191,30 @@ async function run(urls, options) {
|
|
|
186
191
|
}
|
|
187
192
|
const traceEngine = new Engine(options);
|
|
188
193
|
await traceEngine.start();
|
|
189
|
-
await traceEngine.runMultiple(
|
|
194
|
+
const profileResult = await traceEngine.runMultiple(
|
|
195
|
+
urls,
|
|
196
|
+
scriptsByCategory
|
|
197
|
+
);
|
|
190
198
|
await traceEngine.stop();
|
|
191
199
|
log.info('Extra run finished');
|
|
200
|
+
|
|
201
|
+
// Fill the main JSON's coverage from the profile run only when
|
|
202
|
+
// the main iterations didn't collect any (--enableProfileRun
|
|
203
|
+
// without --chrome.coverage). Otherwise the main run's
|
|
204
|
+
// per-iteration samples are what the user asked for.
|
|
205
|
+
let mergedCoverage = false;
|
|
206
|
+
for (const [i, pr] of profileResult.entries()) {
|
|
207
|
+
const main = result[i];
|
|
208
|
+
if (!main || !pr.coverage) continue;
|
|
209
|
+
if (main.coverage.js.length > 0 || main.coverage.css.length > 0) {
|
|
210
|
+
continue;
|
|
211
|
+
}
|
|
212
|
+
main.coverage = pr.coverage;
|
|
213
|
+
mergedCoverage = true;
|
|
214
|
+
}
|
|
215
|
+
if (mergedCoverage) {
|
|
216
|
+
await storageManager.writeJson(jsonName + '.json', result);
|
|
217
|
+
}
|
|
192
218
|
}
|
|
193
219
|
} catch (error) {
|
|
194
220
|
log.error('Error running browsertime', error);
|
|
@@ -0,0 +1,232 @@
|
|
|
1
|
+
import { getLogger } from '@sitespeed.io/log';
|
|
2
|
+
|
|
3
|
+
const log = getLogger('browsertime.chrome.coverage');
|
|
4
|
+
|
|
5
|
+
// Sum the union length of half-open [start, end) ranges. Used for the
|
|
6
|
+
// flat, non-nested ranges that CSS rule-usage tracking returns.
|
|
7
|
+
function unionLength(ranges) {
|
|
8
|
+
if (ranges.length === 0) return 0;
|
|
9
|
+
const sorted = ranges
|
|
10
|
+
.map(r => [r.startOffset, r.endOffset])
|
|
11
|
+
.toSorted((a, b) => a[0] - b[0]);
|
|
12
|
+
let total = 0;
|
|
13
|
+
let [curStart, curEnd] = sorted[0];
|
|
14
|
+
for (let i = 1; i < sorted.length; i++) {
|
|
15
|
+
const [s, e] = sorted[i];
|
|
16
|
+
if (s <= curEnd) {
|
|
17
|
+
if (e > curEnd) curEnd = e;
|
|
18
|
+
} else {
|
|
19
|
+
total += curEnd - curStart;
|
|
20
|
+
curStart = s;
|
|
21
|
+
curEnd = e;
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
total += curEnd - curStart;
|
|
25
|
+
return total;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
// Count used bytes for a script under detailed (block-level) V8
|
|
29
|
+
// coverage. The CDP returns nested ranges: the outermost range is the
|
|
30
|
+
// function body and inner ranges are sub-blocks. An inner range with
|
|
31
|
+
// count == 0 inside a containing range with count > 0 represents dead
|
|
32
|
+
// code in an otherwise-executed function — those bytes must read as
|
|
33
|
+
// unused. A naive union of every range whose count > 0 is wrong: the
|
|
34
|
+
// outer range alone covers the entire function, so inner count == 0
|
|
35
|
+
// ranges get masked and zero-count branches disappear, making typical
|
|
36
|
+
// modern bundles look as if every byte was executed. Walk every range
|
|
37
|
+
// across every function from outermost to innermost (start ascending,
|
|
38
|
+
// length descending), painting a per-byte coverage flag; inner ranges
|
|
39
|
+
// overwrite outer ones, so the final flag at each byte reflects the
|
|
40
|
+
// innermost range's count.
|
|
41
|
+
export function usedJsBytes(scriptCoverage, totalBytes) {
|
|
42
|
+
const ranges = [];
|
|
43
|
+
for (const fn of scriptCoverage.functions) {
|
|
44
|
+
for (const r of fn.ranges) {
|
|
45
|
+
if (r.endOffset > r.startOffset) ranges.push(r);
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
if (ranges.length === 0) return 0;
|
|
49
|
+
ranges.sort((a, b) =>
|
|
50
|
+
a.startOffset === b.startOffset
|
|
51
|
+
? b.endOffset - a.endOffset
|
|
52
|
+
: a.startOffset - b.startOffset
|
|
53
|
+
);
|
|
54
|
+
const used = new Uint8Array(totalBytes);
|
|
55
|
+
for (const r of ranges) {
|
|
56
|
+
const start = Math.max(0, r.startOffset);
|
|
57
|
+
const end = Math.min(totalBytes, r.endOffset);
|
|
58
|
+
if (end > start) used.fill(r.count > 0 ? 1 : 0, start, end);
|
|
59
|
+
}
|
|
60
|
+
let count = 0;
|
|
61
|
+
for (let i = 0; i < totalBytes; i++) {
|
|
62
|
+
if (used[i] === 1) count++;
|
|
63
|
+
}
|
|
64
|
+
return count;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function summarize(files) {
|
|
68
|
+
let totalBytes = 0;
|
|
69
|
+
let usedBytes = 0;
|
|
70
|
+
for (const file of files) {
|
|
71
|
+
totalBytes += file.totalBytes;
|
|
72
|
+
usedBytes += file.usedBytes;
|
|
73
|
+
}
|
|
74
|
+
const unusedBytes = totalBytes - usedBytes;
|
|
75
|
+
return {
|
|
76
|
+
totalBytes,
|
|
77
|
+
usedBytes,
|
|
78
|
+
unusedBytes,
|
|
79
|
+
unusedPercent: totalBytes > 0 ? (unusedBytes / totalBytes) * 100 : 0
|
|
80
|
+
};
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
export class Coverage {
|
|
84
|
+
constructor(cdp) {
|
|
85
|
+
this.rawClient = cdp.getRawClient();
|
|
86
|
+
this.started = false;
|
|
87
|
+
this.styleSheetUrls = new Map();
|
|
88
|
+
this.disposeStyleSheetListener = undefined;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
async start() {
|
|
92
|
+
const client = this.rawClient;
|
|
93
|
+
this.styleSheetUrls.clear();
|
|
94
|
+
this.disposeStyleSheetListener = client.CSS.styleSheetAdded(
|
|
95
|
+
({ header }) => {
|
|
96
|
+
if (header && header.styleSheetId) {
|
|
97
|
+
this.styleSheetUrls.set(
|
|
98
|
+
header.styleSheetId,
|
|
99
|
+
header.sourceURL || header.sourceMapURL || ''
|
|
100
|
+
);
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
);
|
|
104
|
+
|
|
105
|
+
await client.Debugger.enable();
|
|
106
|
+
await client.Profiler.enable();
|
|
107
|
+
await client.DOM.enable();
|
|
108
|
+
await client.CSS.enable();
|
|
109
|
+
await client.Profiler.startPreciseCoverage({
|
|
110
|
+
callCount: false,
|
|
111
|
+
detailed: true,
|
|
112
|
+
allowTriggeredUpdates: false
|
|
113
|
+
});
|
|
114
|
+
await client.CSS.startRuleUsageTracking();
|
|
115
|
+
this.started = true;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
async collect() {
|
|
119
|
+
if (!this.started) return;
|
|
120
|
+
const client = this.rawClient;
|
|
121
|
+
|
|
122
|
+
let js;
|
|
123
|
+
let css;
|
|
124
|
+
try {
|
|
125
|
+
const jsResult = await client.Profiler.takePreciseCoverage();
|
|
126
|
+
js = await this.#processJs(jsResult.result || []);
|
|
127
|
+
} catch (error) {
|
|
128
|
+
log.warn('Could not collect JS coverage: %s', error.message);
|
|
129
|
+
js = {
|
|
130
|
+
totalBytes: 0,
|
|
131
|
+
usedBytes: 0,
|
|
132
|
+
unusedBytes: 0,
|
|
133
|
+
unusedPercent: 0,
|
|
134
|
+
files: []
|
|
135
|
+
};
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
try {
|
|
139
|
+
const cssResult = await client.CSS.stopRuleUsageTracking();
|
|
140
|
+
css = await this.#processCss(cssResult.ruleUsage || []);
|
|
141
|
+
} catch (error) {
|
|
142
|
+
log.warn('Could not collect CSS coverage: %s', error.message);
|
|
143
|
+
css = {
|
|
144
|
+
totalBytes: 0,
|
|
145
|
+
usedBytes: 0,
|
|
146
|
+
unusedBytes: 0,
|
|
147
|
+
unusedPercent: 0,
|
|
148
|
+
files: []
|
|
149
|
+
};
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
try {
|
|
153
|
+
await client.Profiler.stopPreciseCoverage();
|
|
154
|
+
} catch {
|
|
155
|
+
// ignore — collection already returned data
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
if (this.disposeStyleSheetListener) {
|
|
159
|
+
this.disposeStyleSheetListener();
|
|
160
|
+
this.disposeStyleSheetListener = undefined;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
this.started = false;
|
|
164
|
+
return { js, css };
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
async #processJs(scripts) {
|
|
168
|
+
const client = this.rawClient;
|
|
169
|
+
const files = [];
|
|
170
|
+
for (const script of scripts) {
|
|
171
|
+
if (!script.url || !/^https?:/i.test(script.url)) {
|
|
172
|
+
continue;
|
|
173
|
+
}
|
|
174
|
+
let source;
|
|
175
|
+
try {
|
|
176
|
+
const r = await client.Debugger.getScriptSource({
|
|
177
|
+
scriptId: script.scriptId
|
|
178
|
+
});
|
|
179
|
+
source = r.scriptSource || '';
|
|
180
|
+
} catch {
|
|
181
|
+
continue;
|
|
182
|
+
}
|
|
183
|
+
const totalBytes = source.length;
|
|
184
|
+
if (totalBytes === 0) continue;
|
|
185
|
+
|
|
186
|
+
const usedBytes = usedJsBytes(script, totalBytes);
|
|
187
|
+
const unusedBytes = totalBytes - usedBytes;
|
|
188
|
+
files.push({
|
|
189
|
+
url: script.url,
|
|
190
|
+
totalBytes,
|
|
191
|
+
usedBytes,
|
|
192
|
+
unusedBytes,
|
|
193
|
+
unusedPercent: (unusedBytes / totalBytes) * 100
|
|
194
|
+
});
|
|
195
|
+
}
|
|
196
|
+
return { ...summarize(files), files };
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
async #processCss(ruleUsage) {
|
|
200
|
+
const client = this.rawClient;
|
|
201
|
+
const byStylesheet = new Map();
|
|
202
|
+
for (const u of ruleUsage) {
|
|
203
|
+
const list = byStylesheet.get(u.styleSheetId) || [];
|
|
204
|
+
list.push(u);
|
|
205
|
+
byStylesheet.set(u.styleSheetId, list);
|
|
206
|
+
}
|
|
207
|
+
const files = [];
|
|
208
|
+
for (const [styleSheetId, usages] of byStylesheet) {
|
|
209
|
+
let text;
|
|
210
|
+
try {
|
|
211
|
+
const r = await client.CSS.getStyleSheetText({ styleSheetId });
|
|
212
|
+
text = r.text || '';
|
|
213
|
+
} catch {
|
|
214
|
+
continue;
|
|
215
|
+
}
|
|
216
|
+
const totalBytes = text.length;
|
|
217
|
+
if (totalBytes === 0) continue;
|
|
218
|
+
|
|
219
|
+
const usedBytes = unionLength(usages.filter(u => u.used));
|
|
220
|
+
const unusedBytes = totalBytes - usedBytes;
|
|
221
|
+
files.push({
|
|
222
|
+
url: this.styleSheetUrls.get(styleSheetId) || '',
|
|
223
|
+
styleSheetId,
|
|
224
|
+
totalBytes,
|
|
225
|
+
usedBytes,
|
|
226
|
+
unusedBytes,
|
|
227
|
+
unusedPercent: (unusedBytes / totalBytes) * 100
|
|
228
|
+
});
|
|
229
|
+
}
|
|
230
|
+
return { ...summarize(files), files };
|
|
231
|
+
}
|
|
232
|
+
}
|
|
@@ -15,6 +15,7 @@ import { parse } from '../traceCategoriesParser.js';
|
|
|
15
15
|
import { pathToFolder } from '../../support/pathToFolder.js';
|
|
16
16
|
import { loadUsbPowerProfiler } from '../../support/usbPower.js';
|
|
17
17
|
import { ChromeDevtoolsProtocol } from '../chromeDevtoolsProtocol.js';
|
|
18
|
+
import { Coverage } from '../coverage.js';
|
|
18
19
|
import { NetworkManager } from '../networkManager.js';
|
|
19
20
|
import { Android, isAndroidConfigured } from '../../android/index.js';
|
|
20
21
|
import { getRenderBlocking } from './traceUtilities.js';
|
|
@@ -181,6 +182,11 @@ export class Chromium {
|
|
|
181
182
|
}
|
|
182
183
|
}
|
|
183
184
|
|
|
185
|
+
if (this.chrome.coverage) {
|
|
186
|
+
this.coverage = new Coverage(this.cdpClient);
|
|
187
|
+
await this.coverage.start();
|
|
188
|
+
}
|
|
189
|
+
|
|
184
190
|
if (
|
|
185
191
|
this.collectTracingEvents &&
|
|
186
192
|
!this.isTracing &&
|
|
@@ -325,6 +331,14 @@ export class Chromium {
|
|
|
325
331
|
);
|
|
326
332
|
}
|
|
327
333
|
|
|
334
|
+
if (this.coverage) {
|
|
335
|
+
const coverage = await this.coverage.collect();
|
|
336
|
+
this.coverage = undefined;
|
|
337
|
+
if (coverage) {
|
|
338
|
+
result.coverage = coverage;
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
|
|
328
342
|
if (this.chrome.cdp && this.chrome.cdp.performance) {
|
|
329
343
|
const rawCDPMetrics = await this.cdpClient.getPerformanceMetrics();
|
|
330
344
|
const cleanedMetrics = {};
|
|
@@ -39,6 +39,7 @@ function getNewResult(url, options) {
|
|
|
39
39
|
markedAsFailure: 0,
|
|
40
40
|
failureMessages: [],
|
|
41
41
|
cdp: { performance: [] },
|
|
42
|
+
coverage: { js: [], css: [] },
|
|
42
43
|
android: { batteryTemperature: [], power: [] },
|
|
43
44
|
timestamps: [],
|
|
44
45
|
browserScripts: [],
|
|
@@ -185,6 +186,13 @@ export class Collector {
|
|
|
185
186
|
});
|
|
186
187
|
}
|
|
187
188
|
|
|
189
|
+
// Only available for Chrome with --chrome.coverage or
|
|
190
|
+
// --enableProfileRun.
|
|
191
|
+
if (data.coverage) {
|
|
192
|
+
results.coverage.js.push(data.coverage.js);
|
|
193
|
+
results.coverage.css.push(data.coverage.css);
|
|
194
|
+
}
|
|
195
|
+
|
|
188
196
|
this._logIterationMetrics(data, url);
|
|
189
197
|
|
|
190
198
|
if (data.visualMetrics) {
|
package/lib/support/cli.js
CHANGED
|
@@ -373,6 +373,20 @@ export function parseCommandLine() {
|
|
|
373
373
|
'If you use --user-data-dir as an argument to Chrome and want to clean that directory between each iteration you should use --chrome.cleanUserDataDir true.',
|
|
374
374
|
group: 'chrome'
|
|
375
375
|
})
|
|
376
|
+
.option('chrome.coverage', {
|
|
377
|
+
type: 'boolean',
|
|
378
|
+
default: false,
|
|
379
|
+
describe:
|
|
380
|
+
'Collect JavaScript and CSS code coverage on every iteration. ' +
|
|
381
|
+
'Per-iteration summaries with a per-file breakdown land in the ' +
|
|
382
|
+
'per-URL result as coverage.js[] and coverage.css[]. This has a ' +
|
|
383
|
+
'real performance cost: detailed JS coverage causes V8 to ' +
|
|
384
|
+
'deoptimize and will skew timing metrics like LCP, TBT and INP. ' +
|
|
385
|
+
'Use --enableProfileRun instead if you want coverage without ' +
|
|
386
|
+
'affecting your timing data - it collects coverage during the ' +
|
|
387
|
+
'extra profile iteration only.',
|
|
388
|
+
group: 'chrome'
|
|
389
|
+
})
|
|
376
390
|
.option('cpu', {
|
|
377
391
|
type: 'boolean',
|
|
378
392
|
describe:
|
|
@@ -651,7 +665,7 @@ export function parseCommandLine() {
|
|
|
651
665
|
.option('enableProfileRun', {
|
|
652
666
|
type: 'boolean',
|
|
653
667
|
describe:
|
|
654
|
-
'Make one extra run that collects the profiling trace log (no other metrics is collected). For Chrome it will collect the timeline trace, for Firefox it will get the Geckoprofiler trace. This means you do not need to get the trace for all runs and can skip the overhead it produces.'
|
|
668
|
+
'Make one extra run that collects the profiling trace log (no other metrics is collected). For Chrome it will collect the timeline trace and JavaScript/CSS coverage, for Firefox it will get the Geckoprofiler trace. This means you do not need to get the trace for all runs and can skip the overhead it produces.'
|
|
655
669
|
})
|
|
656
670
|
.option('enableVideoRun', {
|
|
657
671
|
type: 'boolean',
|
package/lib/support/har/index.js
CHANGED
|
@@ -42,6 +42,7 @@ function getDocumentRequests(entries, pageId) {
|
|
|
42
42
|
|
|
43
43
|
do {
|
|
44
44
|
entry = pageEntries.shift();
|
|
45
|
+
if (!entry) break;
|
|
45
46
|
requests.push(entry);
|
|
46
47
|
} while (entry.response.redirectURL);
|
|
47
48
|
|
|
@@ -52,7 +53,7 @@ function getFinalURL(entries, pageref) {
|
|
|
52
53
|
const requests = getDocumentRequests(entries, pageref);
|
|
53
54
|
|
|
54
55
|
const finalEntry = requests.pop();
|
|
55
|
-
return finalEntry.request.url;
|
|
56
|
+
return finalEntry ? finalEntry.request.url : undefined;
|
|
56
57
|
}
|
|
57
58
|
|
|
58
59
|
function addExtrasToHAR(
|
|
@@ -249,13 +250,16 @@ export function getMainDocumentTimings(har) {
|
|
|
249
250
|
for (let page of har.log.pages) {
|
|
250
251
|
const pageId = page.id;
|
|
251
252
|
const url = page._url;
|
|
253
|
+
if (url === undefined) continue;
|
|
252
254
|
|
|
253
255
|
let pageEntries = [...entries];
|
|
254
256
|
const finalURL = getFinalURL(pageEntries, pageId);
|
|
257
|
+
if (finalURL === undefined) continue;
|
|
255
258
|
|
|
256
259
|
pageEntries = pageEntries.filter(
|
|
257
260
|
entry => entry.pageref === pageId && entry.request.url === finalURL
|
|
258
261
|
);
|
|
262
|
+
if (pageEntries.length === 0) continue;
|
|
259
263
|
|
|
260
264
|
timings.push({ url, timings: pageEntries[0].timings });
|
|
261
265
|
}
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "browsertime",
|
|
3
3
|
"description": "Get performance metrics from your web page using Browsertime.",
|
|
4
|
-
"version": "27.
|
|
4
|
+
"version": "27.4.1",
|
|
5
5
|
"bin": "./bin/browsertime.js",
|
|
6
6
|
"type": "module",
|
|
7
7
|
"types": "./scripting.d.ts",
|
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
"@devicefarmer/adbkit": "3.3.8",
|
|
10
10
|
"@sitespeed.io/chromedriver": "148.0.7778",
|
|
11
11
|
"@sitespeed.io/edgedriver": "148.0.3967",
|
|
12
|
-
"@sitespeed.io/geckodriver": "0.36.
|
|
12
|
+
"@sitespeed.io/geckodriver": "0.36.2",
|
|
13
13
|
"@sitespeed.io/log": "2.0.0",
|
|
14
14
|
"@sitespeed.io/throttle": "6.0.0",
|
|
15
15
|
"chrome-har": "1.3.1",
|