lighthouse 13.5.0-dev.20261021 → 13.5.0-dev.20261023

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/cli/cli-flags.js CHANGED
@@ -232,7 +232,8 @@ function getYargsParser(manualArgv) {
232
232
  'output-path': {
233
233
  type: 'string',
234
234
  coerce: coerceOutputPath,
235
- describe: `The file path to output the results. Use 'stdout' to write to stdout.
235
+ describe: `The file or directory path to output the results. Use 'stdout' to write to stdout.
236
+ Directories in the path must already exist. If a directory is specified, the file name will be based on the test URL and date.
236
237
  If using JSON output, default is stdout.
237
238
  If using HTML or CSV output, default is a file in the working directory with a name based on the test URL and date.
238
239
  If using multiple outputs, --output-path is appended with the standard extension for each output type. "reports/my-run" -> "reports/my-run.report.html", "reports/my-run.report.json", etc.
@@ -327,7 +328,7 @@ Example: --output-path=./lighthouse-results.html`,
327
328
  if (
328
329
  typeof argv.outputPath === 'string' &&
329
330
  argv.outputPath &&
330
- (!fs.existsSync(argv.outputPath) || !fs.lstatSync(argv.outputPath).isDirectory())
331
+ (!fs.existsSync(argv.outputPath) || !fs.statSync(argv.outputPath).isDirectory())
331
332
  ) {
332
333
  throw new Error('--output-path must be an existing directory when auditing multiple URLs');
333
334
  }
@@ -458,7 +459,7 @@ function coerceUrlFile(value) {
458
459
  typeof value !== 'string' ||
459
460
  !value ||
460
461
  !fs.existsSync(value) ||
461
- fs.lstatSync(value).isDirectory()
462
+ fs.statSync(value).isDirectory()
462
463
  ) {
463
464
  throw new Error(`--url-file (${value}) cannot be read`);
464
465
  }
package/cli/run.js CHANGED
@@ -149,7 +149,7 @@ async function saveResults(runnerResult, flags) {
149
149
  const isDirectoryOutput = Boolean(
150
150
  flags.outputPath &&
151
151
  fs.existsSync(flags.outputPath) &&
152
- fs.lstatSync(flags.outputPath).isDirectory()
152
+ fs.statSync(flags.outputPath).isDirectory()
153
153
  );
154
154
 
155
155
  // Use the output path as the prefix for all generated files.
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=smokehouse-test.d.ts.map
@@ -0,0 +1,120 @@
1
+ /**
2
+ * @license
3
+ * Copyright 2026 Google LLC
4
+ * SPDX-License-Identifier: Apache-2.0
5
+ */
6
+
7
+ import * as td from 'testdouble';
8
+
9
+ import {runSmokehouse} from './smokehouse.js';
10
+ import {ChildProcessError} from './lib/child-process-error.js';
11
+
12
+ /** @type {Smokehouse.TestDfn} */
13
+ const smokeTestDefn = {
14
+ id: 'test-smoke',
15
+ expectations: {
16
+ lhr: {
17
+ requestedUrl: 'http://localhost:10200/simple-page.html',
18
+ finalDisplayedUrl: 'http://localhost:10200/simple-page.html',
19
+ audits: {},
20
+ },
21
+ },
22
+ };
23
+
24
+ describe('runSmokehouse error logging', () => {
25
+ /** @type {string[]} */
26
+ let loggedMessages;
27
+
28
+ beforeEach(() => {
29
+ loggedMessages = [];
30
+ td.replace(console, 'log', (/** @type {unknown[]} */ ...args) => {
31
+ loggedMessages.push(args.map(String).join(' '));
32
+ });
33
+ });
34
+
35
+ afterEach(() => {
36
+ td.reset();
37
+ });
38
+
39
+ it('logs runner logger output when lighthouseRunner returns a malformed result', async () => {
40
+ /** @type {Smokehouse.LighthouseRunner} */
41
+ // @ts-expect-error - testing malformed runner result
42
+ const lighthouseRunner = async (_url, _config, logger) => {
43
+ logger?.log('runner progress before malformed result');
44
+ return {lhr: {}, artifacts: undefined};
45
+ };
46
+
47
+ const {success, testResults} = await runSmokehouse([smokeTestDefn], {
48
+ lighthouseRunner,
49
+ retries: 0,
50
+ });
51
+
52
+ expect(success).toBe(false);
53
+ expect(testResults).toHaveLength(1);
54
+ expect(testResults[0].failed).toBe(1);
55
+
56
+ const combinedOutput = loggedMessages.join('\n');
57
+ expect(combinedOutput).toContain('lighthouse runner returned a bad result');
58
+ expect(combinedOutput).toContain('runner progress before malformed result');
59
+ });
60
+
61
+ it('logs runner logger output when lighthouseRunner throws a regular Error', async () => {
62
+ /** @type {Smokehouse.LighthouseRunner} */
63
+ const lighthouseRunner = async (_url, _config, logger) => {
64
+ logger?.log('runner progress before throwing');
65
+ throw new Error('unexpected runner failure');
66
+ };
67
+
68
+ const {success, testResults} = await runSmokehouse([smokeTestDefn], {
69
+ lighthouseRunner,
70
+ retries: 0,
71
+ });
72
+
73
+ expect(success).toBe(false);
74
+ expect(testResults).toHaveLength(1);
75
+ expect(testResults[0].failed).toBe(1);
76
+
77
+ const combinedOutput = loggedMessages.join('\n');
78
+ expect(combinedOutput).toContain('unexpected runner failure');
79
+ expect(combinedOutput).toContain('runner progress before throwing');
80
+ });
81
+
82
+ it('does not duplicate runner logger output for ChildProcessError', async () => {
83
+ /** @type {Smokehouse.LighthouseRunner} */
84
+ const lighthouseRunner = async (_url, _config, logger) => {
85
+ logger?.log('unique-cli-runner-log-line');
86
+ throw new ChildProcessError('Lighthouse run failed', logger?.getLog());
87
+ };
88
+
89
+ const {success} = await runSmokehouse([smokeTestDefn], {
90
+ lighthouseRunner,
91
+ retries: 0,
92
+ });
93
+
94
+ expect(success).toBe(false);
95
+
96
+ const combinedOutput = loggedMessages.join('\n');
97
+ const occurrences = combinedOutput.split('unique-cli-runner-log-line').length - 1;
98
+ expect(occurrences).toBe(1);
99
+ });
100
+
101
+ it('does not duplicate runner logger output when Error message already includes it', async () => {
102
+ /** @type {Smokehouse.LighthouseRunner} */
103
+ const lighthouseRunner = async (_url, _config, logger) => {
104
+ logger?.log('unique-worker-runner-log-line');
105
+ const log = logger?.getLog() || '';
106
+ throw new Error(`Worker returned an error: boom\nLog:\n${log}\n`);
107
+ };
108
+
109
+ const {success} = await runSmokehouse([smokeTestDefn], {
110
+ lighthouseRunner,
111
+ retries: 0,
112
+ });
113
+
114
+ expect(success).toBe(false);
115
+
116
+ const combinedOutput = loggedMessages.join('\n');
117
+ const occurrences = combinedOutput.split('unique-worker-runner-log-line').length - 1;
118
+ expect(occurrences).toBe(1);
119
+ });
120
+ });
@@ -190,10 +190,10 @@ async function runSmokeTest(smokeTestDefn, testOptions) {
190
190
  clearTimeout(timeoutId);
191
191
  }
192
192
 
193
- if (!timedResult.lhr?.audits || !timedResult.artifacts) {
193
+ if (!timedResult?.lhr?.audits || !timedResult.artifacts) {
194
194
  // Something went really wrong and the runner didn't catch it.
195
195
  throw new Error('lighthouse runner returned a bad result. got lhr:\n' +
196
- JSON.stringify(timedResult.lhr, null, 2));
196
+ JSON.stringify(timedResult?.lhr, null, 2));
197
197
  }
198
198
 
199
199
  result = {
@@ -206,9 +206,17 @@ async function runSmokeTest(smokeTestDefn, testOptions) {
206
206
  if (takeNetworkRequestUrls) takeNetworkRequestUrls();
207
207
 
208
208
  logChildProcessError(bufferedConsole, e);
209
+ const runnerLog = logger.getLog();
209
210
  if (isTimeout) {
210
211
  bufferedConsole.log('Timed out. log from lighthouseRunner:');
211
- bufferedConsole.log(logger.getLog());
212
+ bufferedConsole.log(runnerLog);
213
+ } else if (
214
+ !(e instanceof ChildProcessError) &&
215
+ runnerLog &&
216
+ !e.message?.includes(runnerLog)
217
+ ) {
218
+ bufferedConsole.log('log from lighthouseRunner:');
219
+ bufferedConsole.log(runnerLog);
212
220
  }
213
221
  continue; // Retry, if possible.
214
222
  }
@@ -178,21 +178,31 @@ class ScriptTreemapDataAudit extends Audit {
178
178
  const htmlNodesByFrameId = new Map();
179
179
  /** @type {Set<LH.Artifacts.NetworkRequest>} */
180
180
  const seenNetworkRecords = new Set();
181
- const bundles = await JSBundles.request(artifacts, context);
182
- const duplicationByPath = await ModuleDuplication.request(artifacts, context);
181
+ const deduplicatedScripts = artifacts.Scripts.filter(script => {
182
+ if (script.scriptLanguage !== 'JavaScript') return false;
183
183
 
184
- for (const script of artifacts.Scripts) {
185
- if (script.scriptLanguage !== 'JavaScript') continue;
186
-
187
- const isInlineScript = isInline(script);
188
- const networkRecord = isInlineScript ?
184
+ const networkRecord = isInline(script) ?
189
185
  undefined :
190
186
  getRequestForScript(networkRecords, script);
191
187
  if (networkRecord) {
192
- if (seenNetworkRecords.has(networkRecord)) continue;
188
+ if (seenNetworkRecords.has(networkRecord)) return false;
193
189
  seenNetworkRecords.add(networkRecord);
194
190
  }
195
191
 
192
+ return true;
193
+ });
194
+ const deduplicatedArtifacts = deduplicatedScripts.length === artifacts.Scripts.length ?
195
+ artifacts :
196
+ {...artifacts, Scripts: deduplicatedScripts};
197
+ const bundles = await JSBundles.request(deduplicatedArtifacts, context);
198
+ const duplicationByPath = await ModuleDuplication.request(deduplicatedArtifacts, context);
199
+
200
+ for (const script of deduplicatedScripts) {
201
+ const isInlineScript = isInline(script);
202
+ const networkRecord = isInlineScript ?
203
+ undefined :
204
+ getRequestForScript(networkRecords, script);
205
+
196
206
  const name = script.url;
197
207
  const bundle = bundles.find(bundle => script.scriptId === bundle.script.scriptId) ?? null;
198
208
  const scriptCoverage = /** @type {LH.Artifacts['JsUsage'][string] | undefined} */
@@ -40,7 +40,7 @@ async function getComputationDataParamsFromTrace(data, context) {
40
40
  const traceEngineResult = await TraceEngineResult.request(data, context);
41
41
  const frameId = traceEngineResult.data.Meta.mainFrameId;
42
42
  const navigation =
43
- traceEngineResult.data.Meta.mainFrameNavigations[0];
43
+ traceEngineResult.data.Meta.mainFrameNavigations.at(-1);
44
44
  if (!navigation) {
45
45
  throw new Error(`Lantern metrics could not be calculated due to missing navigation id`);
46
46
  }
@@ -11,6 +11,26 @@ import {ProcessedTrace} from './processed-trace.js';
11
11
  import {NetworkRecords} from './network-records.js';
12
12
  import {TraceEngineResult} from './trace-engine-result.js';
13
13
 
14
+ /**
15
+ * ResourceSendRequest URLs in the trace retain #hash fragments, whereas CDP
16
+ * Network.requestWillBeSent, Page.frameNavigated, initiator.url, documentURL,
17
+ * and V8 script events strip #hash fragments, and Lantern's NetworkAnalyzer
18
+ * requires request.url not to have a fragment.
19
+ * TODO: Remove once @paulirish/trace_engine includes the fix in LanternComputationData.
20
+ * @param {string} urlStr
21
+ * @return {string}
22
+ */
23
+ function stripUrlHash(urlStr) {
24
+ if (!urlStr.includes('#')) return urlStr;
25
+ try {
26
+ const url = new URL(urlStr);
27
+ url.hash = '';
28
+ return url.href;
29
+ } catch {
30
+ return urlStr;
31
+ }
32
+ }
33
+
14
34
  class PageDependencyGraph {
15
35
  /**
16
36
  * @param {{trace: LH.Trace, devtoolsLog: LH.DevtoolsLog, settings: LH.Audit.Context['settings'], URL: LH.Artifacts['URL'], SourceMaps: LH.Artifacts['SourceMaps'], HostDPR: LH.Artifacts['HostDPR'], fromTrace: boolean}} data
@@ -27,7 +47,41 @@ class PageDependencyGraph {
27
47
  if (data.fromTrace) {
28
48
  const traceEngineResult =
29
49
  await TraceEngineResult.request({trace, settings, SourceMaps, HostDPR}, context);
30
- const parsedTrace = traceEngineResult.data;
50
+ let parsedTrace = traceEngineResult.data;
51
+ if (parsedTrace.NetworkRequests.byTime.some(
52
+ r => r.args.data.url.includes('#') ||
53
+ r.args.data.redirects.some(red => red.url.includes('#'))
54
+ )) {
55
+ const byTime = parsedTrace.NetworkRequests.byTime.map(request => {
56
+ const url = stripUrlHash(request.args.data.url);
57
+ const redirects = request.args.data.redirects.map(redirect => ({
58
+ ...redirect,
59
+ url: stripUrlHash(redirect.url),
60
+ }));
61
+ if (url === request.args.data.url &&
62
+ redirects.every((red, i) => red.url === request.args.data.redirects[i].url)) {
63
+ return request;
64
+ }
65
+ return {
66
+ ...request,
67
+ args: {
68
+ ...request.args,
69
+ data: {
70
+ ...request.args.data,
71
+ url,
72
+ redirects,
73
+ },
74
+ },
75
+ };
76
+ });
77
+ parsedTrace = {
78
+ ...parsedTrace,
79
+ NetworkRequests: {
80
+ ...parsedTrace.NetworkRequests,
81
+ byTime,
82
+ },
83
+ };
84
+ }
31
85
  const requests =
32
86
  Lantern.TraceEngineComputationData.createNetworkRequests(trace, parsedTrace);
33
87
  const graph =
@@ -66,8 +66,21 @@ class UnusedCSS {
66
66
  let usedUncompressedBytes = 0;
67
67
  const totalUncompressedBytes = stylesheetInfo.content.length;
68
68
 
69
- for (const usedRule of stylesheetInfo.usedRules) {
70
- usedUncompressedBytes += usedRule.endOffset - usedRule.startOffset;
69
+ // CSS rule usage tracking can return overlapping ranges (e.g. a nested rule inside its
70
+ // parent rule's range) or duplicate ranges (parent rules are emitted once per used child rule).
71
+ // Sort and merge overlapping ranges so used bytes are only counted once.
72
+ const usedRules = stylesheetInfo.usedRules
73
+ .slice()
74
+ .sort((a, b) => a.startOffset - b.startOffset);
75
+
76
+ let lastEndOffset = -1;
77
+ for (const usedRule of usedRules) {
78
+ if (usedRule.endOffset <= lastEndOffset) {
79
+ continue;
80
+ }
81
+ const startOffset = Math.max(usedRule.startOffset, lastEndOffset);
82
+ usedUncompressedBytes += usedRule.endOffset - startOffset;
83
+ lastEndOffset = usedRule.endOffset;
71
84
  }
72
85
 
73
86
  const compressedSize = estimateCompressedContentSize(
@@ -24,6 +24,19 @@ const config = {
24
24
  'bf-cache',
25
25
  ],
26
26
  },
27
+ audits: [
28
+ // Lightrider does not support WebSocket connections.
29
+ // See https://github.com/GoogleChrome/lighthouse/issues/12459
30
+ {
31
+ path: 'errors-in-console',
32
+ options: {
33
+ ignoredPatterns: [
34
+ 'ERR_BLOCKED_BY_CLIENT.Inspector',
35
+ /^WebSocket connection to 'wss?:\/\/.*' failed:/,
36
+ ],
37
+ },
38
+ },
39
+ ],
27
40
  };
28
41
 
29
42
  export default config;
@@ -24,6 +24,19 @@ const config = {
24
24
  'bf-cache',
25
25
  ],
26
26
  },
27
+ audits: [
28
+ // Lightrider does not support WebSocket connections.
29
+ // See https://github.com/GoogleChrome/lighthouse/issues/12459
30
+ {
31
+ path: 'errors-in-console',
32
+ options: {
33
+ ignoredPatterns: [
34
+ 'ERR_BLOCKED_BY_CLIENT.Inspector',
35
+ /^WebSocket connection to 'wss?:\/\/.*' failed:/,
36
+ ],
37
+ },
38
+ },
39
+ ],
27
40
  };
28
41
 
29
42
  export default config;
@@ -1,6 +1,12 @@
1
1
  export default Trace;
2
2
  declare class Trace extends BaseGatherer {
3
3
  static getDefaultTraceCategories(): string[];
4
+ /**
5
+ * @param {LH.Gatherer.ProtocolSession} session
6
+ * @param {string} handle
7
+ * @return {Promise<LH.Trace>}
8
+ */
9
+ static _readTraceFromStream(session: LH.Gatherer.ProtocolSession, handle: string): Promise<LH.Trace>;
4
10
  /**
5
11
  * @param {LH.Gatherer.ProtocolSession} session
6
12
  * @return {Promise<LH.Trace>}
@@ -73,29 +73,68 @@ class Trace extends BaseGatherer {
73
73
 
74
74
  /**
75
75
  * @param {LH.Gatherer.ProtocolSession} session
76
+ * @param {string} handle
76
77
  * @return {Promise<LH.Trace>}
77
78
  */
78
- static async endTraceAndCollectEvents(session) {
79
+ static async _readTraceFromStream(session, handle) {
79
80
  /** @type {Array<LH.TraceEvent>} */
80
81
  const traceEvents = [];
82
+ const decoder = new TextDecoder();
83
+ let isEof = false;
84
+ let remainder = '';
85
+
86
+ try {
87
+ while (!isEof) {
88
+ const response = await session.sendCommand('IO.read', {handle});
89
+ isEof = Boolean(response.eof);
90
+ remainder += response.base64Encoded ?
91
+ decoder.decode(Buffer.from(response.data, 'base64'), {stream: !isEof}) :
92
+ response.data;
93
+
94
+ // Perfetto's `ExportJson` serializes each trace event and trailing `metadata`
95
+ // as compact single-line JSON (with string newlines escaped as `\\n`), emitting
96
+ // raw `,\n` only between top-level elements of `traceEvents`.
97
+ // Maintain the invariant that `remainder` always starts with `{"traceEvents":[`.
98
+ const lastEventSep = remainder.lastIndexOf('},\n');
99
+ if (lastEventSep !== -1) {
100
+ const {traceEvents: batch} = JSON.parse(`${remainder.slice(0, lastEventSep + 1)}]}`);
101
+ for (const event of batch) {
102
+ traceEvents.push(event);
103
+ }
104
+ remainder = `{"traceEvents":[${remainder.slice(lastEventSep + 3)}`;
105
+ }
106
+ }
107
+ } finally {
108
+ await session.sendCommand('IO.close', {handle});
109
+ }
81
110
 
82
- /**
83
- * Listener for when dataCollected events fire for each trace chunk
84
- * @param {LH.Crdp.Tracing.DataCollectedEvent} data
85
- */
86
- const dataListener = function(data) {
87
- traceEvents.push(...data.value);
88
- };
89
- session.on('Tracing.dataCollected', dataListener);
90
-
91
- return new Promise((resolve, reject) => {
92
- session.once('Tracing.tracingComplete', _ => {
93
- session.off('Tracing.dataCollected', dataListener);
94
- resolve({traceEvents});
95
- });
111
+ const traceJson = JSON.parse(remainder);
112
+ for (const event of traceJson.traceEvents) {
113
+ traceEvents.push(event);
114
+ }
115
+ traceJson.traceEvents = traceEvents;
116
+ return traceJson;
117
+ }
96
118
 
97
- session.sendCommand('Tracing.end').catch(reject);
119
+ /**
120
+ * @param {LH.Gatherer.ProtocolSession} session
121
+ * @return {Promise<LH.Trace>}
122
+ */
123
+ static async endTraceAndCollectEvents(session) {
124
+ /** @type {LH.Crdp.Tracing.TracingCompleteEvent} */
125
+ const completeEvent = await new Promise((resolve, reject) => {
126
+ session.once('Tracing.tracingComplete', resolve);
127
+ session.sendCommand('Tracing.end').catch(err => {
128
+ session.off('Tracing.tracingComplete', resolve);
129
+ reject(err);
130
+ });
98
131
  });
132
+
133
+ if (!completeEvent.stream) {
134
+ throw new Error('Tracing.tracingComplete did not return a stream handle');
135
+ }
136
+
137
+ return Trace._readTraceFromStream(session, completeEvent.stream);
99
138
  }
100
139
 
101
140
  static symbol = Symbol('Trace');
@@ -115,6 +154,7 @@ class Trace extends BaseGatherer {
115
154
  await driver.defaultSession.sendCommand('Page.enable');
116
155
  await driver.defaultSession.sendCommand('Tracing.start', {
117
156
  categories: traceCategories.join(','),
157
+ transferMode: 'ReturnAsStream',
118
158
  options: 'sampling-frequency=10000', // 1000 is default and too slow.
119
159
  });
120
160
 
@@ -50,9 +50,9 @@ export class ArbitraryEqualityMap {
50
50
  /**
51
51
  * @param {*} a
52
52
  * @param {*} b
53
- * @param {WeakMap<object, object>=} seen
53
+ * @param {WeakMap<object, WeakSet<object>>=} seen
54
54
  * @return {boolean}
55
55
  */
56
- declare function deepEquals(a: any, b: any, seen?: WeakMap<object, object> | undefined): boolean;
56
+ declare function deepEquals(a: any, b: any, seen?: WeakMap<object, WeakSet<object>> | undefined): boolean;
57
57
  export { deepEquals as isEqual };
58
58
  //# sourceMappingURL=arbitrary-equality-map.d.ts.map
@@ -7,7 +7,7 @@
7
7
  /**
8
8
  * @param {*} a
9
9
  * @param {*} b
10
- * @param {WeakMap<object, object>=} seen
10
+ * @param {WeakMap<object, WeakSet<object>>=} seen
11
11
  * @return {boolean}
12
12
  */
13
13
  function deepEquals(a, b, seen) {
@@ -32,13 +32,27 @@ function deepEquals(a, b, seen) {
32
32
  }
33
33
  return true;
34
34
  }
35
+ if (a instanceof ArrayBuffer && b instanceof ArrayBuffer) {
36
+ if (a.byteLength !== b.byteLength) return false;
37
+ const viewA = new Uint8Array(a);
38
+ const viewB = new Uint8Array(b);
39
+ for (let i = 0; i < viewA.length; i++) {
40
+ if (viewA[i] !== viewB[i]) return false;
41
+ }
42
+ return true;
43
+ }
35
44
 
36
- if (seen) {
37
- if (seen.get(a) === b) return true;
38
- } else {
45
+ if (!seen) {
39
46
  seen = new WeakMap();
40
47
  }
41
- seen.set(a, b);
48
+ let seenB = seen.get(a);
49
+ if (seenB) {
50
+ if (seenB.has(b)) return true;
51
+ } else {
52
+ seenB = new WeakSet();
53
+ seen.set(a, seenB);
54
+ }
55
+ seenB.add(b);
42
56
 
43
57
  if (Array.isArray(a)) {
44
58
  const arrB = /** @type {unknown[]} */ (b);
@@ -59,12 +73,38 @@ function deepEquals(a, b, seen) {
59
73
 
60
74
  if (a instanceof Set && b instanceof Set) {
61
75
  if (a.size !== b.size) return false;
76
+ /** @type {unknown[]} */
77
+ const unmatchedA = [];
62
78
  for (const v of a) {
63
- if (!b.has(v)) return false;
79
+ if (!b.has(v)) {
80
+ if (typeof v !== 'object' || v === null) return false;
81
+ unmatchedA.push(v);
82
+ }
83
+ }
84
+ if (unmatchedA.length === 0) return true;
85
+ /** @type {unknown[]} */
86
+ const unmatchedB = [];
87
+ for (const v of b) {
88
+ if (!a.has(v)) {
89
+ if (typeof v !== 'object' || v === null) return false;
90
+ unmatchedB.push(v);
91
+ }
92
+ }
93
+ if (unmatchedA.length !== unmatchedB.length) return false;
94
+ for (const itemA of unmatchedA) {
95
+ const matchIndex = unmatchedB.findIndex(itemB => deepEquals(itemA, itemB, seen));
96
+ if (matchIndex === -1) return false;
97
+ unmatchedB.splice(matchIndex, 1);
64
98
  }
65
99
  return true;
66
100
  }
67
101
 
102
+ if (a instanceof Error && b instanceof Error) {
103
+ if (a.name !== b.name || a.message !== b.message || !deepEquals(a.cause, b.cause, seen)) {
104
+ return false;
105
+ }
106
+ }
107
+
68
108
  const keysA = Object.keys(a);
69
109
  const keysB = Object.keys(b);
70
110
  if (keysA.length !== keysB.length) return false;
@@ -189,8 +189,8 @@ function getPolyfillPatterns() {
189
189
  // 4. es-shims: `no(Object,{entries:r},{entries:function`
190
190
  const esShimsBranches = [];
191
191
  for (const [object, props] of byObject) {
192
- const propAlt = `(?:${props.join('|')})`;
193
- esShimsBranches.push(`${escapeRegExp(object)},\\s*{${propAlt}:.*},\\s*{${propAlt}`);
192
+ const propBranches = props.map(p => `${p}:[^};]*},\\s*{${p}\\b`).join('|');
193
+ esShimsBranches.push(`${escapeRegExp(object)},\\s*{(?:${propBranches})`);
194
194
  }
195
195
 
196
196
  // 5. core-js@3 minified pattern:
@@ -345,6 +345,9 @@ class NetworkRequest {
345
345
  this.networkEndTime = data.timestamp * 1000;
346
346
 
347
347
  this._updateResponseHeadersEndTimeIfNecessary();
348
+ this._updateProtocolForLightrider();
349
+ this._updateTransferSizeForLightrider();
350
+ this._updateTimingsForLightrider();
348
351
  }
349
352
 
350
353
  /**