lighthouse 11.3.0-dev.20231206 → 11.3.0-dev.20231207

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.
@@ -15,6 +15,7 @@ export namespace UIStrings {
15
15
  const supportPendingFailureType: string;
16
16
  const failureReasonColumn: string;
17
17
  const failureTypeColumn: string;
18
+ const warningHeadless: string;
18
19
  const displayValue: string;
19
20
  }
20
21
  import { Audit } from './audit.js';
@@ -26,6 +26,8 @@ const UIStrings = {
26
26
  failureReasonColumn: 'Failure reason',
27
27
  /** Label for a column in a data table; entries in the column will be a string representing the type of failure preventing the back/forward cache from being used. */
28
28
  failureTypeColumn: 'Failure type',
29
+ /** Warning explaining that the back/forward cache results cannot be shown in the old Headless Chrome. "back/forward" refers to the back and forward buttons found in modern browsers. "Headless Chrome" is a product name and should not be translated. */
30
+ warningHeadless: 'Back/forward cache cannot be tested in old Headless Chrome (`--chrome-flags="--headless=old"`). To see audit results, use the new Headless Chrome (`--chrome-flags="--headless=new"`) or standard Chrome.',
29
31
  /**
30
32
  * @description [ICU Syntax] Label for an audit identifying the number of back/forward cache failure reasons found in the page.
31
33
  */
@@ -60,7 +62,7 @@ class BFCache extends Audit {
60
62
  description: str_(UIStrings.description),
61
63
  supportedModes: ['navigation', 'timespan'],
62
64
  guidanceLevel: 2,
63
- requiredArtifacts: ['BFCacheFailures'],
65
+ requiredArtifacts: ['BFCacheFailures', 'HostProduct'],
64
66
  scoreDisplayMode: Audit.SCORING_MODES.BINARY,
65
67
  };
66
68
  }
@@ -70,6 +72,14 @@ class BFCache extends Audit {
70
72
  * @return {Promise<LH.Audit.Product>}
71
73
  */
72
74
  static async audit(artifacts) {
75
+ if (/HeadlessChrome/.test(artifacts.HostProduct)) {
76
+ return {
77
+ score: null,
78
+ notApplicable: true,
79
+ warnings: [str_(UIStrings.warningHeadless)],
80
+ };
81
+ }
82
+
73
83
  const failures = artifacts.BFCacheFailures;
74
84
  if (!failures.length) return {score: 1};
75
85
 
@@ -25,12 +25,6 @@ declare class DuplicatedJavascript extends ByteEfficiencyAudit {
25
25
  scriptUrl: string;
26
26
  resourceSize: number;
27
27
  }[]>>;
28
- /**
29
- *
30
- * @param {LH.Artifacts.NetworkRequest|undefined} networkRecord
31
- * @param {number} contentLength
32
- */
33
- static _estimateTransferRatio(networkRecord: LH.Artifacts.NetworkRequest | undefined, contentLength: number): number;
34
28
  /**
35
29
  * This audit highlights JavaScript modules that appear to be duplicated across all resources,
36
30
  * either within the same bundle or between different bundles. Each details item returned is
@@ -11,7 +11,7 @@
11
11
  import {ByteEfficiencyAudit} from './byte-efficiency-audit.js';
12
12
  import {ModuleDuplication} from '../../computed/module-duplication.js';
13
13
  import * as i18n from '../../lib/i18n/i18n.js';
14
- import {estimateTransferSize, getRequestForScript} from '../../lib/script-helpers.js';
14
+ import {estimateCompressionRatioForContent} from '../../lib/script-helpers.js';
15
15
 
16
16
  const UIStrings = {
17
17
  /** Imperative title of a Lighthouse audit that tells the user to remove duplicate JavaScript from their code. This is displayed in a list of audit titles that Lighthouse generates. */
@@ -103,16 +103,6 @@ class DuplicatedJavascript extends ByteEfficiencyAudit {
103
103
  return groupedDuplication;
104
104
  }
105
105
 
106
- /**
107
- *
108
- * @param {LH.Artifacts.NetworkRequest|undefined} networkRecord
109
- * @param {number} contentLength
110
- */
111
- static _estimateTransferRatio(networkRecord, contentLength) {
112
- const transferSize = estimateTransferSize(networkRecord, contentLength, 'Script');
113
- return transferSize / contentLength;
114
- }
115
-
116
106
  /**
117
107
  * This audit highlights JavaScript modules that appear to be duplicated across all resources,
118
108
  * either within the same bundle or between different bundles. Each details item returned is
@@ -131,7 +121,7 @@ class DuplicatedJavascript extends ByteEfficiencyAudit {
131
121
  await DuplicatedJavascript._getDuplicationGroupedByNodeModules(artifacts, context);
132
122
 
133
123
  /** @type {Map<string, number>} */
134
- const transferRatioByUrl = new Map();
124
+ const compressionRatioByUrl = new Map();
135
125
 
136
126
  /** @type {Item[]} */
137
127
  const items = [];
@@ -144,10 +134,10 @@ class DuplicatedJavascript extends ByteEfficiencyAudit {
144
134
  for (const [source, sourceDatas] of duplication.entries()) {
145
135
  // One copy of this module is treated as the canonical version - the rest will have
146
136
  // non-zero `wastedBytes`. In the case of all copies being the same version, all sizes are
147
- // equal and the selection doesn't matter. When the copies are different versions, it does
148
- // matter. Ideally the newest version would be the canonical copy, but version information
149
- // is not present. Instead, size is used as a heuristic for latest version. This makes the
150
- // audit conserative in its estimation.
137
+ // equal and the selection doesn't matter (ignoring compression ratios). When the copies are
138
+ // different versions, it does matter. Ideally the newest version would be the canonical
139
+ // copy, but version information is not present. Instead, size is used as a heuristic for
140
+ // latest version. This makes the audit conserative in its estimation.
151
141
 
152
142
  /** @type {SubItem[]} */
153
143
  const subItems = [];
@@ -158,27 +148,9 @@ class DuplicatedJavascript extends ByteEfficiencyAudit {
158
148
  const scriptId = sourceData.scriptId;
159
149
  const script = artifacts.Scripts.find(script => script.scriptId === scriptId);
160
150
  const url = script?.url || '';
161
-
162
- /** @type {number|undefined} */
163
- let transferRatio = transferRatioByUrl.get(url);
164
- if (transferRatio === undefined) {
165
- if (!script || script.length === undefined) {
166
- // This should never happen because we found the wasted bytes from bundles, which required contents in a Script.
167
- continue;
168
- }
169
-
170
- const contentLength = script.length;
171
- const networkRecord = getRequestForScript(networkRecords, script);
172
- transferRatio = DuplicatedJavascript._estimateTransferRatio(networkRecord, contentLength);
173
- transferRatioByUrl.set(url, transferRatio);
174
- }
175
-
176
- if (transferRatio === undefined) {
177
- // Shouldn't happen for above reasons.
178
- continue;
179
- }
180
-
181
- const transferSize = Math.round(sourceData.resourceSize * transferRatio);
151
+ const compressionRatio = estimateCompressionRatioForContent(
152
+ compressionRatioByUrl, url, artifacts, networkRecords);
153
+ const transferSize = Math.round(sourceData.resourceSize * compressionRatio);
182
154
 
183
155
  subItems.push({
184
156
  url,
@@ -417,7 +417,7 @@ class LegacyJavascript extends ByteEfficiencyAudit {
417
417
  const scriptToMatchResults =
418
418
  this.detectAcrossScripts(matcher, artifacts.Scripts, networkRecords, bundles);
419
419
  for (const [script, matches] of scriptToMatchResults.entries()) {
420
- const compressionRatio = await estimateCompressionRatioForContent(
420
+ const compressionRatio = estimateCompressionRatioForContent(
421
421
  compressionRatioByUrl, script.url, artifacts, networkRecords);
422
422
  const wastedBytes = Math.round(this.estimateWastedBytes(matches) * compressionRatio);
423
423
  /** @type {typeof items[number]} */
@@ -7,7 +7,7 @@
7
7
  import {ByteEfficiencyAudit} from './byte-efficiency-audit.js';
8
8
  import * as i18n from '../../lib/i18n/i18n.js';
9
9
  import {computeJSTokenLength as computeTokenLength} from '../../lib/minification-estimator.js';
10
- import {estimateTransferSize, getRequestForScript, isInline} from '../../lib/script-helpers.js';
10
+ import {estimateCompressedContentSize, getRequestForScript, isInline} from '../../lib/script-helpers.js';
11
11
  import {Util} from '../../../shared/util.js';
12
12
 
13
13
  const UIStrings = {
@@ -58,7 +58,7 @@ class UnminifiedJavaScript extends ByteEfficiencyAudit {
58
58
  const contentLength = scriptContent.length;
59
59
  const totalTokenLength = computeTokenLength(scriptContent);
60
60
 
61
- const totalBytes = estimateTransferSize(networkRecord, contentLength, 'Script');
61
+ const totalBytes = estimateCompressedContentSize(networkRecord, contentLength, 'Script');
62
62
  const wastedRatio = 1 - totalTokenLength / contentLength;
63
63
  const wastedBytes = Math.round(totalBytes * wastedRatio);
64
64
 
@@ -8,7 +8,7 @@ import {ByteEfficiencyAudit} from './byte-efficiency-audit.js';
8
8
  import {UnusedJavascriptSummary} from '../../computed/unused-javascript-summary.js';
9
9
  import {JSBundles} from '../../computed/js-bundles.js';
10
10
  import * as i18n from '../../lib/i18n/i18n.js';
11
- import {estimateTransferSize, getRequestForScript} from '../../lib/script-helpers.js';
11
+ import {estimateCompressionRatioForContent} from '../../lib/script-helpers.js';
12
12
 
13
13
  const UIStrings = {
14
14
  /** Imperative title of a Lighthouse audit that tells the user to reduce JavaScript that is never evaluated during page load. This is displayed in a list of audit titles that Lighthouse generates. */
@@ -86,26 +86,26 @@ class UnusedJavaScript extends ByteEfficiencyAudit {
86
86
  bundleSourceUnusedThreshold = UNUSED_BYTES_IGNORE_BUNDLE_SOURCE_THRESHOLD,
87
87
  } = context.options || {};
88
88
 
89
+ /** @type {Map<string, number>} */
90
+ const compressionRatioByUrl = new Map();
91
+
89
92
  const items = [];
90
93
  for (const [scriptId, scriptCoverage] of Object.entries(artifacts.JsUsage)) {
91
94
  const script = artifacts.Scripts.find(s => s.scriptId === scriptId);
92
95
  if (!script) continue; // This should never happen.
93
96
 
94
- const networkRecord = getRequestForScript(networkRecords, script);
95
- if (!networkRecord) continue;
96
-
97
97
  const bundle = bundles.find(b => b.script.scriptId === scriptId);
98
98
  const unusedJsSummary =
99
99
  await UnusedJavascriptSummary.request({scriptId, scriptCoverage, bundle}, context);
100
100
  if (unusedJsSummary.wastedBytes === 0 || unusedJsSummary.totalBytes === 0) continue;
101
101
 
102
- const transfer = estimateTransferSize(networkRecord, unusedJsSummary.totalBytes, 'Script');
103
- const transferRatio = transfer / unusedJsSummary.totalBytes;
102
+ const compressionRatio = estimateCompressionRatioForContent(
103
+ compressionRatioByUrl, script.url, artifacts, networkRecords);
104
104
  /** @type {LH.Audit.ByteEfficiencyItem} */
105
105
  const item = {
106
106
  url: script.url,
107
- totalBytes: Math.round(transferRatio * unusedJsSummary.totalBytes),
108
- wastedBytes: Math.round(transferRatio * unusedJsSummary.wastedBytes),
107
+ totalBytes: Math.round(compressionRatio * unusedJsSummary.totalBytes),
108
+ wastedBytes: Math.round(compressionRatio * unusedJsSummary.wastedBytes),
109
109
  wastedPercent: unusedJsSummary.wastedPercent,
110
110
  };
111
111
 
@@ -126,8 +126,8 @@ class UnusedJavaScript extends ByteEfficiencyAudit {
126
126
  const total = source === '(unmapped)' ? sizes.unmappedBytes : sizes.files[source];
127
127
  return {
128
128
  source,
129
- unused: Math.round(unused * transferRatio),
130
- total: Math.round(total * transferRatio),
129
+ unused: Math.round(unused * compressionRatio),
130
+ total: Math.round(total * compressionRatio),
131
131
  };
132
132
  })
133
133
  .filter(d => d.unused >= bundleSourceUnusedThreshold);
@@ -0,0 +1,11 @@
1
+ export default ResourceSummary;
2
+ declare class ResourceSummary extends Audit {
3
+ /**
4
+ * @param {LH.Artifacts} artifacts
5
+ * @param {LH.Audit.Context} context
6
+ * @return {Promise<LH.Audit.Product>}
7
+ */
8
+ static audit(artifacts: LH.Artifacts, context: LH.Audit.Context): Promise<LH.Audit.Product>;
9
+ }
10
+ import { Audit } from './audit.js';
11
+ //# sourceMappingURL=resource-summary.d.ts.map
@@ -0,0 +1,86 @@
1
+ /**
2
+ * @license Copyright 2019 The Lighthouse Authors. All Rights Reserved.
3
+ * 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 http://www.apache.org/licenses/LICENSE-2.0
4
+ * 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.
5
+ */
6
+
7
+ import {Audit} from './audit.js';
8
+ import {ResourceSummary as ComputedResourceSummary} from '../computed/resource-summary.js';
9
+ import * as i18n from '../lib/i18n/i18n.js';
10
+
11
+ const str_ = i18n.createIcuMessageFn(import.meta.url);
12
+
13
+ class ResourceSummary extends Audit {
14
+ /**
15
+ * @return {LH.Audit.Meta}
16
+ */
17
+ static get meta() {
18
+ return {
19
+ id: 'resource-summary',
20
+ title: 'Resources Summary',
21
+ description: 'Aggregates all network requests and groups them by type',
22
+ scoreDisplayMode: Audit.SCORING_MODES.INFORMATIVE,
23
+ requiredArtifacts: ['devtoolsLogs', 'URL'],
24
+ };
25
+ }
26
+
27
+ /**
28
+ * @param {LH.Artifacts} artifacts
29
+ * @param {LH.Audit.Context} context
30
+ * @return {Promise<LH.Audit.Product>}
31
+ */
32
+ static async audit(artifacts, context) {
33
+ const devtoolsLog = artifacts.devtoolsLogs[Audit.DEFAULT_PASS];
34
+ const summary = await ComputedResourceSummary
35
+ .request({devtoolsLog, URL: artifacts.URL, budgets: context.settings.budgets}, context);
36
+
37
+ /** @type {LH.Audit.Details.Table['headings']} */
38
+ const headings = [
39
+ {key: 'label', valueType: 'text', label: str_(i18n.UIStrings.columnResourceType)},
40
+ {key: 'requestCount', valueType: 'numeric', label: str_(i18n.UIStrings.columnRequests)},
41
+ {key: 'transferSize', valueType: 'bytes', label: str_(i18n.UIStrings.columnTransferSize)},
42
+ ];
43
+
44
+
45
+ /** @type {Record<LH.Budget.ResourceType, LH.IcuMessage>} */
46
+ const strMappings = {
47
+ 'total': str_(i18n.UIStrings.totalResourceType),
48
+ 'document': str_(i18n.UIStrings.documentResourceType),
49
+ 'script': str_(i18n.UIStrings.scriptResourceType),
50
+ 'stylesheet': str_(i18n.UIStrings.stylesheetResourceType),
51
+ 'image': str_(i18n.UIStrings.imageResourceType),
52
+ 'media': str_(i18n.UIStrings.mediaResourceType),
53
+ 'font': str_(i18n.UIStrings.fontResourceType),
54
+ 'other': str_(i18n.UIStrings.otherResourceType),
55
+ 'third-party': str_(i18n.UIStrings.thirdPartyResourceType),
56
+ };
57
+
58
+ const types = /** @type {Array<LH.Budget.ResourceType>} */ (Object.keys(summary));
59
+ const rows = types.map(type => {
60
+ return {
61
+ // ResourceType is included as an "id" for ease of use.
62
+ // It does not appear directly in the table.
63
+ resourceType: type,
64
+ label: strMappings[type],
65
+ requestCount: summary[type].count,
66
+ transferSize: summary[type].transferSize,
67
+ };
68
+ });
69
+ // Force third-party to be last, descending by size otherwise
70
+ const thirdPartyRow = rows.find(r => r.resourceType === 'third-party') || [];
71
+ const otherRows = rows.filter(r => r.resourceType !== 'third-party')
72
+ .sort((a, b) => {
73
+ return b.transferSize - a.transferSize;
74
+ });
75
+ const tableItems = otherRows.concat(thirdPartyRow);
76
+
77
+ const tableDetails = Audit.makeTableDetails(headings, tableItems);
78
+
79
+ return {
80
+ details: tableDetails,
81
+ score: null,
82
+ };
83
+ }
84
+ }
85
+
86
+ export default ResourceSummary;
@@ -215,6 +215,7 @@ const defaultConfig = {
215
215
  'metrics',
216
216
  'performance-budget',
217
217
  'timing-budget',
218
+ 'resource-summary',
218
219
  'third-party-summary',
219
220
  'third-party-facades',
220
221
  'largest-contentful-paint-element',
@@ -499,6 +500,7 @@ const defaultConfig = {
499
500
  {id: 'screenshot-thumbnails', weight: 0, group: 'hidden'},
500
501
  {id: 'final-screenshot', weight: 0, group: 'hidden'},
501
502
  {id: 'script-treemap-data', weight: 0, group: 'hidden'},
503
+ {id: 'resource-summary', weight: 0, group: 'hidden'},
502
504
  ],
503
505
  },
504
506
  'accessibility': {
@@ -20,6 +20,7 @@ const baseArtifactKeySource = {
20
20
  PageLoadError: '',
21
21
  HostFormFactor: '',
22
22
  HostUserAgent: '',
23
+ HostProduct: '',
23
24
  GatherContext: '',
24
25
  };
25
26
 
@@ -19,7 +19,7 @@ import {
19
19
  */
20
20
  async function getBaseArtifacts(resolvedConfig, driver, context) {
21
21
  const BenchmarkIndex = await getBenchmarkIndex(driver.executionContext);
22
- const {userAgent} = await getBrowserVersion(driver.defaultSession);
22
+ const {userAgent, product} = await getBrowserVersion(driver.defaultSession);
23
23
 
24
24
  return {
25
25
  // Meta artifacts.
@@ -32,6 +32,7 @@ async function getBaseArtifacts(resolvedConfig, driver, context) {
32
32
  HostUserAgent: userAgent,
33
33
  HostFormFactor: userAgent.includes('Android') || userAgent.includes('Mobile') ?
34
34
  'mobile' : 'desktop',
35
+ HostProduct: product,
35
36
  // Contextual artifacts whose collection changes based on gather mode.
36
37
  URL: {
37
38
  finalDisplayedUrl: '',
@@ -69,9 +69,9 @@ export function lookupLocale(locales?: (string | string[]) | undefined, possible
69
69
  * Returns a function that generates `LH.IcuMessage` objects to localize the
70
70
  * messages in `fileStrings` and the shared `i18n.UIStrings`.
71
71
  * @param {string} filename
72
- * @param {Record<string, string>} fileStrings
72
+ * @param {Record<string, string>=} fileStrings
73
73
  */
74
- export function createIcuMessageFn(filename: string, fileStrings: Record<string, string>): (message: string, values?: Record<string, string | number> | undefined) => LH.IcuMessage;
74
+ export function createIcuMessageFn(filename: string, fileStrings?: Record<string, string> | undefined): (message: string, values?: Record<string, string | number> | undefined) => LH.IcuMessage;
75
75
  /**
76
76
  * Returns true if the given value is a string or an LH.IcuMessage.
77
77
  * @param {unknown} value
@@ -168,9 +168,9 @@ function lookupLocale(locales, possibleLocales) {
168
168
  * Returns a function that generates `LH.IcuMessage` objects to localize the
169
169
  * messages in `fileStrings` and the shared `i18n.UIStrings`.
170
170
  * @param {string} filename
171
- * @param {Record<string, string>} fileStrings
171
+ * @param {Record<string, string>=} fileStrings
172
172
  */
173
- function createIcuMessageFn(filename, fileStrings) {
173
+ function createIcuMessageFn(filename, fileStrings = {}) {
174
174
  if (filename.startsWith('file://')) filename = url.fileURLToPath(filename);
175
175
 
176
176
  // In the common case, `filename` is an absolute path that needs to be transformed
@@ -614,7 +614,10 @@ class NetworkRequest {
614
614
  * @return {boolean}
615
615
  */
616
616
  static isContentEncoded(record) {
617
- return record.responseHeaders.some(item => item.name === 'Content-Encoding');
617
+ // FYI: older devtools logs (like our test fixtures) seems to be lower case, while modern logs
618
+ // are Cased-Like-This.
619
+ const pattern = /^content-encoding$/i;
620
+ return record.responseHeaders.some(item => item.name.match(pattern));
618
621
  }
619
622
 
620
623
  /**
@@ -41,6 +41,6 @@ export function estimateTransferSize(networkRecord: LH.Artifacts.NetworkRequest
41
41
  * @param {LH.Artifacts} artifacts
42
42
  * @param {Array<LH.Artifacts.NetworkRequest>} networkRecords
43
43
  */
44
- export function estimateCompressionRatioForContent(compressionRatioByUrl: Map<string, number>, url: string, artifacts: LH.Artifacts, networkRecords: Array<LH.Artifacts.NetworkRequest>): Promise<number>;
44
+ export function estimateCompressionRatioForContent(compressionRatioByUrl: Map<string, number>, url: string, artifacts: LH.Artifacts, networkRecords: Array<LH.Artifacts.NetworkRequest>): number;
45
45
  import { NetworkRequest } from './network-request.js';
46
46
  //# sourceMappingURL=script-helpers.d.ts.map
@@ -137,7 +137,7 @@ function estimateCompressedContentSize(networkRecord, totalBytes, resourceType)
137
137
  * @param {LH.Artifacts} artifacts
138
138
  * @param {Array<LH.Artifacts.NetworkRequest>} networkRecords
139
139
  */
140
- async function estimateCompressionRatioForContent(compressionRatioByUrl, url,
140
+ function estimateCompressionRatioForContent(compressionRatioByUrl, url,
141
141
  artifacts, networkRecords) {
142
142
  let compressionRatio = compressionRatioByUrl.get(url);
143
143
  if (compressionRatio !== undefined) return compressionRatio;
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "lighthouse",
3
3
  "type": "module",
4
- "version": "11.3.0-dev.20231206",
4
+ "version": "11.3.0-dev.20231207",
5
5
  "description": "Automated auditing, performance metrics, and best practices for the web.",
6
6
  "main": "./core/index.js",
7
7
  "bin": {
@@ -605,6 +605,9 @@
605
605
  "core/audits/bf-cache.js | title": {
606
606
  "message": "Page didn't prevent back/forward cache restoration"
607
607
  },
608
+ "core/audits/bf-cache.js | warningHeadless": {
609
+ "message": "Back/forward cache cannot be tested in old Headless Chrome (`--chrome-flags=\"--headless=old\"`). To see audit results, use the new Headless Chrome (`--chrome-flags=\"--headless=new\"`) or standard Chrome."
610
+ },
608
611
  "core/audits/bootup-time.js | chromeExtensionsWarning": {
609
612
  "message": "Chrome extensions negatively affected this page's load performance. Try auditing the page in incognito mode or from a Chrome profile without extensions."
610
613
  },
@@ -605,6 +605,9 @@
605
605
  "core/audits/bf-cache.js | title": {
606
606
  "message": "P̂áĝé d̂íd̂ń't̂ ṕr̂év̂én̂t́ b̂áĉḱ/f̂ór̂ẃâŕd̂ ćâćĥé r̂éŝt́ôŕât́îón̂"
607
607
  },
608
+ "core/audits/bf-cache.js | warningHeadless": {
609
+ "message": "B̂áĉḱ/f̂ór̂ẃâŕd̂ ćâćĥé ĉán̂ńôt́ b̂é t̂éŝt́êd́ îń ôĺd̂ H́êád̂ĺêśŝ Ćĥŕôḿê (`--chrome-flags=\"--headless=old\"`). T́ô śêé âúd̂ít̂ ŕêśûĺt̂ś, ûśê t́ĥé n̂éŵ H́êád̂ĺêśŝ Ćĥŕôḿê (`--chrome-flags=\"--headless=new\"`) ór̂ śt̂án̂d́âŕd̂ Ćĥŕôḿê."
610
+ },
608
611
  "core/audits/bootup-time.js | chromeExtensionsWarning": {
609
612
  "message": "Ĉh́r̂óm̂é êx́t̂én̂śîón̂ś n̂éĝát̂ív̂él̂ý âf́f̂éĉt́êd́ t̂h́îś p̂áĝé'ŝ ĺôád̂ ṕêŕf̂ór̂ḿâńĉé. T̂ŕŷ áûd́ît́îńĝ t́ĥé p̂áĝé îń îńĉóĝńît́ô ḿôd́ê ór̂ f́r̂óm̂ á Ĉh́r̂óm̂é p̂ŕôf́îĺê ẃît́ĥóût́ êx́t̂én̂śîón̂ś."
610
613
  },
@@ -53,6 +53,8 @@ interface UniversalBaseArtifacts {
53
53
  HostFormFactor: 'desktop'|'mobile';
54
54
  /** The user agent string of the version of Chrome used. */
55
55
  HostUserAgent: string;
56
+ /** The product string of the version of Chrome used. Example: HeadlessChrome/123.2.2.0 would be from old headless. */
57
+ HostProduct: string;
56
58
  /** Information about how Lighthouse artifacts were gathered. */
57
59
  GatherContext: {gatherMode: Gatherer.GatherMode};
58
60
  }