lighthouse 12.6.0 → 12.6.1-dev.20250603

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.
Files changed (86) hide show
  1. package/core/audits/byte-efficiency/render-blocking-resources.js +1 -1
  2. package/core/audits/deprecations.js +1 -1
  3. package/core/audits/dobetterweb/doctype.js +1 -1
  4. package/core/audits/dobetterweb/inspector-issues.js +6 -11
  5. package/core/audits/insights/document-latency-insight.js +8 -1
  6. package/core/audits/insights/dom-size-insight.js +9 -1
  7. package/core/audits/insights/insight-audit.js +7 -0
  8. package/core/audits/insights/render-blocking-insight.js +1 -1
  9. package/core/audits/is-on-https.js +1 -1
  10. package/core/audits/third-party-cookies.js +1 -1
  11. package/core/computed/metrics/timing-summary.js +9 -1
  12. package/core/computed/page-dependency-graph.js +4 -0
  13. package/core/computed/trace-engine-result.js +7 -0
  14. package/core/config/constants.d.ts +2 -2
  15. package/core/config/constants.js +2 -2
  16. package/core/config/lr-desktop-config.js +1 -0
  17. package/core/config/lr-mobile-config.js +1 -0
  18. package/core/gather/driver/execution-context.d.ts +3 -1
  19. package/core/gather/driver/execution-context.js +3 -1
  20. package/core/gather/gatherers/css-usage.js +4 -1
  21. package/core/gather/gatherers/inspector-issues.d.ts +2 -2
  22. package/core/gather/gatherers/inspector-issues.js +31 -18
  23. package/core/gather/gatherers/stylesheets.js +4 -1
  24. package/core/lib/network-request.js +1 -0
  25. package/core/lib/trace-engine.d.ts +2 -0
  26. package/core/lib/trace-engine.js +1 -1
  27. package/dist/report/bundle.esm.js +14 -6
  28. package/dist/report/flow.js +12 -4
  29. package/dist/report/standalone.js +14 -6
  30. package/package.json +9 -9
  31. package/report/assets/styles.css +11 -3
  32. package/report/renderer/components.js +1 -1
  33. package/report/renderer/performance-category-renderer.js +1 -2
  34. package/shared/localization/locales/ar-XB.json +7 -10
  35. package/shared/localization/locales/ar.json +7 -10
  36. package/shared/localization/locales/bg.json +7 -10
  37. package/shared/localization/locales/ca.json +7 -10
  38. package/shared/localization/locales/cs.json +7 -10
  39. package/shared/localization/locales/da.json +7 -10
  40. package/shared/localization/locales/de.json +7 -10
  41. package/shared/localization/locales/el.json +7 -10
  42. package/shared/localization/locales/en-GB.json +7 -10
  43. package/shared/localization/locales/en-US.json +49 -7
  44. package/shared/localization/locales/en-XL.json +49 -7
  45. package/shared/localization/locales/es-419.json +7 -10
  46. package/shared/localization/locales/es.json +7 -10
  47. package/shared/localization/locales/fi.json +7 -10
  48. package/shared/localization/locales/fil.json +7 -10
  49. package/shared/localization/locales/fr.json +7 -10
  50. package/shared/localization/locales/he.json +7 -10
  51. package/shared/localization/locales/hi.json +7 -10
  52. package/shared/localization/locales/hr.json +7 -10
  53. package/shared/localization/locales/hu.json +7 -10
  54. package/shared/localization/locales/id.json +7 -10
  55. package/shared/localization/locales/it.json +7 -10
  56. package/shared/localization/locales/ja.json +7 -10
  57. package/shared/localization/locales/ko.json +7 -10
  58. package/shared/localization/locales/lt.json +7 -10
  59. package/shared/localization/locales/lv.json +7 -10
  60. package/shared/localization/locales/nl.json +7 -10
  61. package/shared/localization/locales/no.json +7 -10
  62. package/shared/localization/locales/pl.json +7 -10
  63. package/shared/localization/locales/pt-PT.json +7 -10
  64. package/shared/localization/locales/pt.json +7 -10
  65. package/shared/localization/locales/ro.json +7 -10
  66. package/shared/localization/locales/ru.json +7 -10
  67. package/shared/localization/locales/sk.json +7 -10
  68. package/shared/localization/locales/sl.json +7 -10
  69. package/shared/localization/locales/sr-Latn.json +7 -10
  70. package/shared/localization/locales/sr.json +7 -10
  71. package/shared/localization/locales/sv.json +7 -10
  72. package/shared/localization/locales/ta.json +7 -10
  73. package/shared/localization/locales/te.json +7 -10
  74. package/shared/localization/locales/th.json +7 -10
  75. package/shared/localization/locales/tr.json +7 -10
  76. package/shared/localization/locales/uk.json +7 -10
  77. package/shared/localization/locales/vi.json +7 -10
  78. package/shared/localization/locales/zh-HK.json +7 -10
  79. package/shared/localization/locales/zh-TW.json +7 -10
  80. package/shared/localization/locales/zh.json +7 -10
  81. package/shared/localization/locales.d.ts +2 -0
  82. package/shared/localization/locales.js +130 -139
  83. package/shared/tsconfig.json +2 -0
  84. package/third-party/chromium-synchronization/inspector-issueAdded-types-test.js +1 -0
  85. package/types/artifacts.d.ts +9 -26
  86. package/types/utility-types.d.ts +4 -0
@@ -291,7 +291,7 @@ class RenderBlockingResources extends Audit {
291
291
  const headings = [
292
292
  {key: 'url', valueType: 'url', label: str_(i18n.UIStrings.columnURL)},
293
293
  {key: 'totalBytes', valueType: 'bytes', label: str_(i18n.UIStrings.columnTransferSize)},
294
- {key: 'wastedMs', valueType: 'timespanMs', label: str_(i18n.UIStrings.columnWastedMs)},
294
+ {key: 'wastedMs', valueType: 'timespanMs', label: str_(i18n.UIStrings.columnDuration)},
295
295
  ];
296
296
 
297
297
  const details = Audit.makeOpportunityDetails(headings, results,
@@ -58,7 +58,7 @@ class Deprecations extends Audit {
58
58
  static async audit(artifacts, context) {
59
59
  const bundles = await JSBundles.request(artifacts, context);
60
60
 
61
- const deprecations = artifacts.InspectorIssues.deprecationIssue
61
+ const deprecations = (artifacts.InspectorIssues.deprecationIssue ?? [])
62
62
  .map(deprecation => {
63
63
  const {scriptId, url, lineNumber, columnNumber} = deprecation.sourceCodeLocation;
64
64
  const bundle = bundles.find(bundle => bundle.script.scriptId === scriptId);
@@ -71,7 +71,7 @@ class Doctype extends Audit {
71
71
 
72
72
  /** @type {LH.Crdp.Audits.QuirksModeIssueDetails[]} */
73
73
  let quirksModeIssues = [];
74
- if (trace && artifacts.InspectorIssues) {
74
+ if (trace && artifacts.InspectorIssues?.quirksModeIssue) {
75
75
  const processedTrace = await ProcessedTrace.request(trace, context);
76
76
  const mainFrameId = processedTrace.mainFrameInfo.frameId;
77
77
  quirksModeIssues =
@@ -160,25 +160,20 @@ class IssuesPanelEntries extends Audit {
160
160
  /** @type LH.Audit.Details.TableItem[] */
161
161
  const items = [];
162
162
 
163
- if (issues.mixedContentIssue.length) {
163
+ if (issues.mixedContentIssue?.length) {
164
164
  items.push(this.getMixedContentRow(issues.mixedContentIssue));
165
165
  }
166
- if (issues.cookieIssue.length) {
166
+ if (issues.cookieIssue?.length) {
167
167
  items.push(this.getCookieRow(issues.cookieIssue));
168
168
  }
169
- if (issues.blockedByResponseIssue.length) {
169
+ if (issues.blockedByResponseIssue?.length) {
170
170
  items.push(this.getBlockedByResponseRow(issues.blockedByResponseIssue));
171
171
  }
172
- if (issues.heavyAdIssue.length) {
172
+ if (issues.heavyAdIssue?.length) {
173
173
  items.push({issueType: str_(UIStrings.issueTypeHeavyAds)});
174
174
  }
175
- const cspIssues = issues.contentSecurityPolicyIssue.filter(issue => {
176
- // kTrustedTypesSinkViolation and kTrustedTypesPolicyViolation aren't currently supported by the Issues panel
177
- return issue.contentSecurityPolicyViolationType !== 'kTrustedTypesSinkViolation' &&
178
- issue.contentSecurityPolicyViolationType !== 'kTrustedTypesPolicyViolation';
179
- });
180
- if (cspIssues.length) {
181
- items.push(this.getContentSecurityPolicyRow(cspIssues));
175
+ if (issues.contentSecurityPolicyIssue?.length) {
176
+ items.push(this.getContentSecurityPolicyRow(issues.contentSecurityPolicyIssue));
182
177
  }
183
178
  return {
184
179
  score: items.length > 0 ? 0 : 1,
@@ -40,7 +40,14 @@ class DocumentLatencyInsight extends Audit {
40
40
  return;
41
41
  }
42
42
 
43
- return Audit.makeChecklistDetails(insight.data.checklist);
43
+ const details = Audit.makeChecklistDetails(insight.data.checklist);
44
+ details.debugData = {
45
+ type: 'debugdata',
46
+ redirectDuration: insight.data.redirectDuration,
47
+ serverResponseTime: insight.data.serverResponseTime,
48
+ uncompressedResponseBytes: insight.data.uncompressedResponseBytes,
49
+ };
50
+ return details;
44
51
  });
45
52
  }
46
53
  }
@@ -77,7 +77,15 @@ class DOMSizeInsight extends Audit {
77
77
  },
78
78
  },
79
79
  ];
80
- return Audit.makeTableDetails(headings, items);
80
+
81
+ const details = Audit.makeTableDetails(headings, items);
82
+ details.debugData = {
83
+ type: 'debugdata',
84
+ totalElements,
85
+ maxChildren: maxChildren.numChildren,
86
+ maxDepth: maxDepth.depth,
87
+ };
88
+ return details;
81
89
  });
82
90
  }
83
91
  }
@@ -75,6 +75,13 @@ async function adaptInsightToAuditProduct(artifacts, context, insightName, creat
75
75
  };
76
76
  }
77
77
 
78
+ if (insight.wastedBytes !== undefined) {
79
+ if (!details.debugData) {
80
+ details.debugData = {type: 'debugdata'};
81
+ }
82
+ details.debugData.wastedBytes = insight.wastedBytes;
83
+ }
84
+
78
85
  // This hack is to add metric adorners if an insight category links it to a metric,
79
86
  // but doesn't output a metric savings for that metric.
80
87
  let metricSavings = insight.metricSavings;
@@ -41,7 +41,7 @@ class RenderBlockingInsight extends Audit {
41
41
  const headings = [
42
42
  {key: 'url', valueType: 'url', label: str_(i18n.UIStrings.columnURL)},
43
43
  {key: 'totalBytes', valueType: 'bytes', label: str_(i18n.UIStrings.columnTransferSize)},
44
- {key: 'wastedMs', valueType: 'timespanMs', label: str_(i18n.UIStrings.columnWastedMs)},
44
+ {key: 'wastedMs', valueType: 'timespanMs', label: str_(i18n.UIStrings.columnDuration)},
45
45
  ];
46
46
  /** @type {LH.Audit.Details.Table['items']} */
47
47
  const items = insight.renderBlockingRequests.map(request => ({
@@ -86,7 +86,7 @@ class HTTPS extends Audit {
86
86
  {key: 'resolution', valueType: 'text', label: str_(UIStrings.columnResolution)},
87
87
  ];
88
88
 
89
- for (const details of artifacts.InspectorIssues.mixedContentIssue) {
89
+ for (const details of artifacts.InspectorIssues.mixedContentIssue ?? []) {
90
90
  let item = items.find(item => item.url === details.insecureURL);
91
91
  if (!item) {
92
92
  item = {url: details.insecureURL};
@@ -68,7 +68,7 @@ class ThirdPartyCookies extends Audit {
68
68
 
69
69
  /** @type {LH.Audit.Details.TableItem[]} */
70
70
  const items = [];
71
- for (const issue of artifacts.InspectorIssues.cookieIssue) {
71
+ for (const issue of artifacts.InspectorIssues.cookieIssue ?? []) {
72
72
  const isPhaseoutWarn = issue.cookieWarningReasons.includes('WarnThirdPartyPhaseout');
73
73
  const isPhaseoutExclude = issue.cookieExclusionReasons.includes('ExcludeThirdPartyPhaseout');
74
74
  if (!isPhaseoutWarn && !isPhaseoutExclude) continue;
@@ -4,6 +4,8 @@
4
4
  * SPDX-License-Identifier: Apache-2.0
5
5
  */
6
6
 
7
+ import log from 'lighthouse-logger';
8
+
7
9
  import {ProcessedTrace} from '../processed-trace.js';
8
10
  import {ProcessedNavigation} from '../processed-navigation.js';
9
11
  import {Speedline} from '../speedline.js';
@@ -19,6 +21,7 @@ import {TotalBlockingTime} from './total-blocking-time.js';
19
21
  import {makeComputedArtifact} from '../computed-artifact.js';
20
22
  import {TimeToFirstByte} from './time-to-first-byte.js';
21
23
  import {LCPBreakdown} from './lcp-breakdown.js';
24
+ import {isUnderTest} from '../../lib/lh-env.js';
22
25
 
23
26
  class TimingSummary {
24
27
  /**
@@ -43,7 +46,12 @@ class TimingSummary {
43
46
  * @return {Promise<TReturn|undefined>}
44
47
  */
45
48
  const requestOrUndefined = (Artifact, artifact) => {
46
- return Artifact.request(artifact, context).catch(_ => undefined);
49
+ return Artifact.request(artifact, context).catch(err => {
50
+ if (isUnderTest) {
51
+ log.error('lh:computed:TimingSummary', err);
52
+ }
53
+ return undefined;
54
+ });
47
55
  };
48
56
 
49
57
  /* eslint-disable max-len */
@@ -36,6 +36,10 @@ class PageDependencyGraph {
36
36
  return graph;
37
37
  }
38
38
 
39
+ // TODO: currently the trace version has no requests that failed, or requests that have "Preflight".
40
+ // so the following gets the devtools log version _closer_ to the exact same results as the trace.
41
+ // const lanternRequests = networkRecords.map(NetworkRequest.asLanternNetworkRequest).filter(r => !r.failed && r.resourceType !== 'Preflight');
42
+
39
43
  const lanternRequests = networkRecords.map(NetworkRequest.asLanternNetworkRequest);
40
44
  return Lantern.Graph.PageDependencyGraph.createGraph(mainThreadEvents, lanternRequests, URL);
41
45
  }
@@ -97,6 +97,9 @@ class TraceEngineResult {
97
97
  if (value && typeof value === 'object' && '__i18nBytes' in value) {
98
98
  values[key] = value.__i18nBytes;
99
99
  // TODO: use an actual byte formatter. Right now, this shows the exact number of bytes.
100
+ } else if (value && typeof value === 'object' && '__i18nMillis' in value) {
101
+ values[key] = `${value.__i18nMillis} ms`;
102
+ // TODO: use an actual time formatter.
100
103
  } else if (value && typeof value === 'object' && 'i18nId' in value) {
101
104
  // TODO: add support for str_ values to be IcuMessage. For now, we translate it here.
102
105
  // This means that locale swapping won't work for this portion of the IcuMessage.
@@ -172,6 +175,10 @@ class TraceEngineResult {
172
175
  // @ts-expect-error
173
176
  values[key] = value.__i18nBytes;
174
177
  // TODO: use an actual byte formatter. Right now, this shows the exact number of bytes.
178
+ } else if (value && typeof value === 'object' && '__i18nMillis' in value) {
179
+ // @ts-expect-error
180
+ values[key] = `${value.__i18nMillis} ms`;
181
+ // TODO: use an actual time formatter.
175
182
  } else if (value && typeof value === 'object' && 'i18nId' in value) {
176
183
  // TODO: add support for str_ values to be IcuMessage.
177
184
  // @ts-expect-error
@@ -51,7 +51,7 @@ declare const MOTOGPOWER_EMULATION_METRICS: Required<LH.SharedFlagsSettings["scr
51
51
  * @type {Required<LH.SharedFlagsSettings['screenEmulation']>}
52
52
  */
53
53
  declare const DESKTOP_EMULATION_METRICS: Required<LH.SharedFlagsSettings["screenEmulation"]>;
54
- declare const MOTOG4_USERAGENT: "Mozilla/5.0 (Linux; Android 11; moto g power (2022)) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/119.0.0.0 Mobile Safari/537.36";
55
- declare const DESKTOP_USERAGENT: "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/119.0.0.0 Safari/537.36";
54
+ declare const MOTOG4_USERAGENT: "Mozilla/5.0 (Linux; Android 11; moto g power (2022)) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/136.0.0.0 Mobile Safari/537.36";
55
+ declare const DESKTOP_USERAGENT: "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/136.0.0.0 Safari/537.36";
56
56
  export {};
57
57
  //# sourceMappingURL=constants.d.ts.map
@@ -39,8 +39,8 @@ const screenEmulationMetrics = {
39
39
  };
40
40
 
41
41
 
42
- const MOTOG4_USERAGENT = 'Mozilla/5.0 (Linux; Android 11; moto g power (2022)) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/119.0.0.0 Mobile Safari/537.36'; // eslint-disable-line max-len
43
- const DESKTOP_USERAGENT = 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/119.0.0.0 Safari/537.36'; // eslint-disable-line max-len
42
+ const MOTOG4_USERAGENT = 'Mozilla/5.0 (Linux; Android 11; moto g power (2022)) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/136.0.0.0 Mobile Safari/537.36'; // eslint-disable-line max-len
43
+ const DESKTOP_USERAGENT = 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/136.0.0.0 Safari/537.36'; // eslint-disable-line max-len
44
44
 
45
45
  const userAgents = {
46
46
  mobile: MOTOG4_USERAGENT,
@@ -19,6 +19,7 @@ const config = {
19
19
  skipAudits: [
20
20
  // Skip the h2 audit so it doesn't lie to us. See https://github.com/GoogleChrome/lighthouse/issues/6539
21
21
  'uses-http2',
22
+ 'modern-http-insight',
22
23
  // There are always bf-cache failures when testing in headless. Reenable when headless can give us realistic bf-cache insights.
23
24
  'bf-cache',
24
25
  ],
@@ -18,6 +18,7 @@ const config = {
18
18
  skipAudits: [
19
19
  // Skip the h2 audit so it doesn't lie to us. See https://github.com/GoogleChrome/lighthouse/issues/6539
20
20
  'uses-http2',
21
+ 'modern-http-insight',
21
22
  // There are always bf-cache failures when testing in headless. Reenable when headless can give us realistic bf-cache insights.
22
23
  'bf-cache',
23
24
  ],
@@ -58,11 +58,13 @@ export class ExecutionContext {
58
58
  */
59
59
  _evaluateInContext(expression: string, contextId: number | undefined, timeout: number): Promise<any>;
60
60
  /**
61
- * Note: Prefer `evaluate` instead.
62
61
  * Evaluate an expression in the context of the current page. If useIsolation is true, the expression
63
62
  * will be evaluated in a content script that has access to the page's DOM but whose JavaScript state
64
63
  * is completely separate.
65
64
  * Returns a promise that resolves on the expression's value.
65
+ *
66
+ * @deprecated Use `evaluate` instead! It has a better API, and unlike `evaluateAsync` doesn't sometimes
67
+ * execute invalid code.
66
68
  * @param {string} expression
67
69
  * @param {{useIsolation?: boolean}=} options
68
70
  * @return {Promise<*>}
@@ -151,11 +151,13 @@ class ExecutionContext {
151
151
  }
152
152
 
153
153
  /**
154
- * Note: Prefer `evaluate` instead.
155
154
  * Evaluate an expression in the context of the current page. If useIsolation is true, the expression
156
155
  * will be evaluated in a content script that has access to the page's DOM but whose JavaScript state
157
156
  * is completely separate.
158
157
  * Returns a promise that resolves on the expression's value.
158
+ *
159
+ * @deprecated Use `evaluate` instead! It has a better API, and unlike `evaluateAsync` doesn't sometimes
160
+ * execute invalid code.
159
161
  * @param {string} expression
160
162
  * @param {{useIsolation?: boolean}=} options
161
163
  * @return {Promise<*>}
@@ -30,7 +30,10 @@ class CSSUsage extends BaseGatherer {
30
30
 
31
31
  // Force style to recompute.
32
32
  // Doesn't appear to be necessary in newer versions of Chrome.
33
- await executionContext.evaluateAsync('getComputedStyle(document.body)');
33
+ /* global window, document */
34
+ await executionContext.evaluate(() => window.getComputedStyle(document.body), {
35
+ args: [],
36
+ });
34
37
 
35
38
  const {ruleUsage} = await session.sendCommand('CSS.stopRuleUsageTracking');
36
39
  await session.sendCommand('CSS.disable');
@@ -19,9 +19,9 @@ declare class InspectorIssues extends BaseGatherer {
19
19
  stopInstrumentation(context: LH.Gatherer.Context): Promise<void>;
20
20
  /**
21
21
  * @param {LH.Gatherer.Context<'DevtoolsLog'>} context
22
- * @return {Promise<LH.Artifacts['InspectorIssues']>}
22
+ * @return {Promise<LH.Artifacts.InspectorIssues>}
23
23
  */
24
- getArtifact(context: LH.Gatherer.Context<"DevtoolsLog">): Promise<LH.Artifacts["InspectorIssues"]>;
24
+ getArtifact(context: LH.Gatherer.Context<"DevtoolsLog">): Promise<LH.Artifacts.InspectorIssues>;
25
25
  }
26
26
  import BaseGatherer from '../base-gatherer.js';
27
27
  //# sourceMappingURL=inspector-issues.d.ts.map
@@ -54,7 +54,7 @@ class InspectorIssues extends BaseGatherer {
54
54
 
55
55
  /**
56
56
  * @param {LH.Gatherer.Context<'DevtoolsLog'>} context
57
- * @return {Promise<LH.Artifacts['InspectorIssues']>}
57
+ * @return {Promise<LH.Artifacts.InspectorIssues>}
58
58
  */
59
59
  async getArtifact(context) {
60
60
  const devtoolsLog = context.dependencies.DevtoolsLog;
@@ -62,6 +62,7 @@ class InspectorIssues extends BaseGatherer {
62
62
 
63
63
  /** @type {LH.Artifacts.InspectorIssues} */
64
64
  const artifact = {
65
+ // TODO(v13): remove empty arrays.
65
66
  attributionReportingIssue: [],
66
67
  blockedByResponseIssue: [],
67
68
  bounceTrackingIssue: [],
@@ -86,27 +87,39 @@ class InspectorIssues extends BaseGatherer {
86
87
  stylesheetLoadingIssue: [],
87
88
  sriMessageSignatureIssue: [],
88
89
  federatedAuthUserInfoRequestIssue: [],
90
+ userReidentificationIssue: [],
89
91
  };
90
- const keys = /** @type {Array<keyof LH.Artifacts['InspectorIssues']>} */(Object.keys(artifact));
91
- for (const key of keys) {
92
- /** @type {`${key}Details`} */
93
- const detailsKey = `${key}Details`;
94
- const allDetails = this._issues.map(issue => issue.details[detailsKey]);
95
- for (const detail of allDetails) {
96
- if (!detail) {
97
- continue;
98
- }
99
- // Duplicate issues can occur for the same request; only use the one with a matching networkRequest.
100
- const requestId = 'request' in detail && detail.request && detail.request.requestId;
101
- if (requestId) {
102
- if (networkRecords.find(req => req.requestId === requestId)) {
103
- // @ts-expect-error - detail types are not all compatible
104
- artifact[key].push(detail);
92
+
93
+ for (const issue of this._issues) {
94
+ const detailsKey = /** @type {keyof LH.Crdp.Audits.InspectorIssueDetails} */(
95
+ Object.keys(issue.details)[0]);
96
+ const details = issue.details[detailsKey];
97
+ if (!details) {
98
+ continue;
99
+ }
100
+
101
+ const artifactKey =
102
+ /** @type {LH.Artifacts.InspectorIssuesKeyToArtifactKey<typeof detailsKey>} */(
103
+ detailsKey.replace('Details', ''));
104
+
105
+ // Duplicate issues can occur for the same request; only use the one with a matching networkRequest.
106
+ const requestId = 'request' in details && details.request && details.request.requestId;
107
+ if (requestId) {
108
+ if (networkRecords.find(req => req.requestId === requestId)) {
109
+ if (!artifact[artifactKey]) {
110
+ artifact[artifactKey] = [];
105
111
  }
106
- } else {
112
+
107
113
  // @ts-expect-error - detail types are not all compatible
108
- artifact[key].push(detail);
114
+ artifact[artifactKey].push(details);
109
115
  }
116
+ } else {
117
+ if (!artifact[artifactKey]) {
118
+ artifact[artifactKey] = [];
119
+ }
120
+
121
+ // @ts-expect-error - detail types are not all compatible
122
+ artifact[artifactKey].push(details);
110
123
  }
111
124
  }
112
125
 
@@ -74,7 +74,10 @@ class Stylesheets extends BaseGatherer {
74
74
 
75
75
  // Force style to recompute.
76
76
  // Doesn't appear to be necessary in newer versions of Chrome.
77
- await executionContext.evaluateAsync('getComputedStyle(document.body)');
77
+ /* global window, document */
78
+ await executionContext.evaluate(() => window.getComputedStyle(document.body), {
79
+ args: [],
80
+ });
78
81
 
79
82
  session.off('CSS.styleSheetAdded', this._onStylesheetAdded);
80
83
 
@@ -107,6 +107,7 @@ const RESOURCE_TYPES = {
107
107
  Preflight: 'Preflight',
108
108
  CSPViolationReport: 'CSPViolationReport',
109
109
  Prefetch: 'Prefetch',
110
+ FedCM: 'FedCM',
110
111
  };
111
112
 
112
113
  class NetworkRequest {
@@ -8,6 +8,8 @@ export type DevToolsIcuMessage = {
8
8
  i18nId: string;
9
9
  values: Record<string, string | number | {
10
10
  __i18nBytes: number;
11
+ } | {
12
+ __i18nMillis: number;
11
13
  }>;
12
14
  };
13
15
  export const TraceProcessor: typeof TraceEngine.Processor.TraceProcessor;
@@ -4,7 +4,7 @@ import {polyfillDOMRect} from './polyfill-dom-rect.js';
4
4
 
5
5
  /** @typedef {import('@paulirish/trace_engine').Types.Events.SyntheticLayoutShift} SyntheticLayoutShift */
6
6
  /** @typedef {SyntheticLayoutShift & {args: {data: NonNullable<SyntheticLayoutShift['args']['data']>}}} SaneSyntheticLayoutShift */
7
- /** @typedef {{i18nId: string, values: Record<string, string|number|{__i18nBytes: number}>}} DevToolsIcuMessage */
7
+ /** @typedef {{i18nId: string, values: Record<string, string|number|{__i18nBytes: number}|{__i18nMillis: number}>}} DevToolsIcuMessage */
8
8
 
9
9
  polyfillDOMRect();
10
10