lighthouse 10.0.2 → 10.1.0-dev.20230323

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 (117) hide show
  1. package/cli/bin.js +1 -1
  2. package/cli/run.js +1 -1
  3. package/cli/test/smokehouse/config/exclusions.js +0 -2
  4. package/cli/test/smokehouse/lighthouse-runners/bundle.js +1 -1
  5. package/core/audits/audit.d.ts +6 -0
  6. package/core/audits/audit.js +16 -4
  7. package/core/audits/bootup-time.js +2 -3
  8. package/core/audits/byte-efficiency/byte-efficiency-audit.d.ts +4 -0
  9. package/core/audits/byte-efficiency/byte-efficiency-audit.js +8 -2
  10. package/core/audits/byte-efficiency/legacy-javascript.d.ts +5 -0
  11. package/core/audits/byte-efficiency/legacy-javascript.js +42 -16
  12. package/core/audits/byte-efficiency/polyfill-graph-data.json +50 -50
  13. package/core/audits/byte-efficiency/total-byte-weight.js +1 -1
  14. package/core/audits/byte-efficiency/unminified-javascript.js +2 -1
  15. package/core/audits/byte-efficiency/uses-long-cache-ttl.js +2 -2
  16. package/core/audits/byte-efficiency/uses-responsive-images-snapshot.js +4 -0
  17. package/core/audits/dobetterweb/doctype.js +3 -2
  18. package/core/audits/long-tasks.js +2 -1
  19. package/core/audits/mainthread-work-breakdown.js +2 -1
  20. package/core/audits/network-rtt.js +1 -1
  21. package/core/audits/network-server-latency.js +2 -1
  22. package/core/audits/performance-budget.js +1 -1
  23. package/core/audits/prioritize-lcp-image.js +1 -1
  24. package/core/audits/seo/tap-targets.js +1 -1
  25. package/core/audits/third-party-summary.js +4 -1
  26. package/core/audits/timing-budget.js +2 -1
  27. package/core/audits/uses-rel-preconnect.js +1 -1
  28. package/core/audits/uses-rel-preload.js +1 -1
  29. package/core/audits/valid-source-maps.js +1 -1
  30. package/core/audits/work-during-interaction.js +1 -1
  31. package/core/computed/metrics/cumulative-layout-shift.d.ts +3 -3
  32. package/core/computed/metrics/cumulative-layout-shift.js +21 -9
  33. package/core/computed/unused-css.js +8 -7
  34. package/core/config/lr-mobile-config.js +0 -11
  35. package/core/gather/driver/prepare.js +15 -0
  36. package/core/gather/gatherers/image-elements.js +1 -0
  37. package/core/gather/gatherers/link-elements.js +2 -0
  38. package/core/lib/asset-saver.js +2 -12
  39. package/core/lib/i18n/i18n.d.ts +1 -1
  40. package/core/lib/i18n/i18n.js +1 -1
  41. package/core/lib/lighthouse-compatibility.js +17 -0
  42. package/core/lib/network-request.js +1 -1
  43. package/core/lib/page-functions.d.ts +17 -5
  44. package/core/lib/page-functions.js +41 -22
  45. package/core/lib/tracehouse/trace-processor.d.ts +8 -0
  46. package/core/lib/tracehouse/trace-processor.js +27 -6
  47. package/core/lib/url-utils.js +1 -1
  48. package/core/runner.js +1 -0
  49. package/dist/report/bundle.esm.js +258 -45
  50. package/dist/report/flow.js +18 -18
  51. package/dist/report/standalone.js +17 -17
  52. package/flow-report/src/i18n/i18n.d.ts +6 -0
  53. package/flow-report/src/summary/category.tsx +0 -1
  54. package/package.json +6 -7
  55. package/report/assets/styles.css +89 -5
  56. package/report/renderer/components.js +1 -1
  57. package/report/renderer/details-renderer.d.ts +26 -6
  58. package/report/renderer/details-renderer.js +144 -21
  59. package/report/renderer/performance-category-renderer.js +3 -3
  60. package/report/renderer/report-renderer.js +1 -0
  61. package/report/renderer/report-ui-features.js +18 -9
  62. package/report/renderer/report-utils.d.ts +13 -4
  63. package/report/renderer/report-utils.js +43 -11
  64. package/shared/localization/locales/ar-XB.json +1 -1
  65. package/shared/localization/locales/ar.json +1 -1
  66. package/shared/localization/locales/bg.json +1 -1
  67. package/shared/localization/locales/ca.json +1 -1
  68. package/shared/localization/locales/cs.json +1 -1
  69. package/shared/localization/locales/da.json +1 -1
  70. package/shared/localization/locales/de.json +1 -1
  71. package/shared/localization/locales/el.json +1 -1
  72. package/shared/localization/locales/en-GB.json +1 -1
  73. package/shared/localization/locales/en-US.json +11 -2
  74. package/shared/localization/locales/en-XA.json +1 -1
  75. package/shared/localization/locales/en-XL.json +11 -2
  76. package/shared/localization/locales/es-419.json +1 -1
  77. package/shared/localization/locales/es.json +1 -1
  78. package/shared/localization/locales/fi.json +1 -1
  79. package/shared/localization/locales/fil.json +1 -1
  80. package/shared/localization/locales/fr.json +1 -1
  81. package/shared/localization/locales/he.json +1 -1
  82. package/shared/localization/locales/hi.json +1 -1
  83. package/shared/localization/locales/hr.json +1 -1
  84. package/shared/localization/locales/hu.json +1 -1
  85. package/shared/localization/locales/id.json +1 -1
  86. package/shared/localization/locales/it.json +1 -1
  87. package/shared/localization/locales/ja.json +1 -1
  88. package/shared/localization/locales/ko.json +1 -1
  89. package/shared/localization/locales/lt.json +1 -1
  90. package/shared/localization/locales/lv.json +1 -1
  91. package/shared/localization/locales/nl.json +1 -1
  92. package/shared/localization/locales/no.json +1 -1
  93. package/shared/localization/locales/pl.json +1 -1
  94. package/shared/localization/locales/pt-PT.json +1 -1
  95. package/shared/localization/locales/pt.json +1 -1
  96. package/shared/localization/locales/ro.json +1 -1
  97. package/shared/localization/locales/ru.json +1 -1
  98. package/shared/localization/locales/sk.json +1 -1
  99. package/shared/localization/locales/sl.json +1 -1
  100. package/shared/localization/locales/sr-Latn.json +1 -1
  101. package/shared/localization/locales/sr.json +1 -1
  102. package/shared/localization/locales/sv.json +1 -1
  103. package/shared/localization/locales/ta.json +1 -1
  104. package/shared/localization/locales/te.json +1 -1
  105. package/shared/localization/locales/th.json +1 -1
  106. package/shared/localization/locales/tr.json +1 -1
  107. package/shared/localization/locales/uk.json +1 -1
  108. package/shared/localization/locales/vi.json +1 -1
  109. package/shared/localization/locales/zh-HK.json +1 -1
  110. package/shared/localization/locales/zh-TW.json +1 -1
  111. package/shared/localization/locales/zh.json +1 -1
  112. package/shared/tsconfig.json +1 -1
  113. package/shared/util.d.ts +6 -0
  114. package/shared/util.js +33 -0
  115. package/types/artifacts.d.ts +6 -1
  116. package/types/internal/node.d.ts +15 -2
  117. package/types/lhr/audit-details.d.ts +35 -11
@@ -319,7 +319,7 @@ class PrioritizeLcpImage extends Audit {
319
319
  {key: 'wastedMs', valueType: 'timespanMs', label: str_(i18n.UIStrings.columnWastedMs)},
320
320
  ];
321
321
  const details = Audit.makeOpportunityDetails(headings, results,
322
- {overallSavingsMs: wastedMs});
322
+ {overallSavingsMs: wastedMs, sortedBy: ['wastedMs']});
323
323
 
324
324
  // If LCP element was an image and had valid network records (regardless of
325
325
  // if it should be preloaded), it will be found first in the `initiatorPath`.
@@ -28,7 +28,7 @@ const UIStrings = {
28
28
  /** Descriptive title of a Lighthouse audit that provides detail on whether tap targets (like buttons and links) on a page are big enough so they can easily be tapped on a mobile device. This descriptive title is shown when tap targets are not easy to tap on. */
29
29
  failureTitle: 'Tap targets are not sized appropriately',
30
30
  /** Description of a Lighthouse audit that tells the user why buttons and links need to be big enough and what 'big enough' means. This is displayed after a user expands the section to see more. No character length limits. The last sentence starting with 'Learn' becomes link text to additional documentation. */
31
- description: 'Interactive elements like buttons and links should be large enough (48x48px), and have enough space around them, to be easy enough to tap without overlapping onto other elements. [Learn more about tap targets](https://developer.chrome.com/docs/lighthouse/seo/tap-targets/).',
31
+ description: 'Interactive elements like buttons and links should be large enough (48x48px), or have enough space around them, to be easy enough to tap without overlapping onto other elements. [Learn more about tap targets](https://developer.chrome.com/docs/lighthouse/seo/tap-targets/).',
32
32
  /** Label of a table column that identifies tap targets (like buttons and links) that have failed the audit and aren't easy to tap on. */
33
33
  tapTargetHeader: 'Tap Target',
34
34
  /** Label of a table column that identifies a tap target (like a link or button) that overlaps with another tap target. */
@@ -243,12 +243,15 @@ class ThirdPartySummary extends Audit {
243
243
  };
244
244
  }
245
245
 
246
+ const details = Audit.makeTableDetails(headings, results,
247
+ {...overallSummary, isEntityGrouped: true});
248
+
246
249
  return {
247
250
  score: Number(overallSummary.wastedMs <= PASS_THRESHOLD_IN_MS),
248
251
  displayValue: str_(UIStrings.displayValue, {
249
252
  timeInMs: overallSummary.wastedMs,
250
253
  }),
251
- details: Audit.makeTableDetails(headings, results, overallSummary),
254
+ details,
252
255
  };
253
256
  }
254
257
  }
@@ -164,7 +164,8 @@ class TimingBudget extends Audit {
164
164
  ];
165
165
 
166
166
  return {
167
- details: Audit.makeTableDetails(headers, this.tableItems(budget, summary)),
167
+ details: Audit.makeTableDetails(headers, this.tableItems(budget, summary),
168
+ {sortedBy: ['overBudget']}),
168
169
  score: 1,
169
170
  };
170
171
  }
@@ -234,7 +234,7 @@ class UsesRelPreconnectAudit extends Audit {
234
234
  ];
235
235
 
236
236
  const details = Audit.makeOpportunityDetails(headings, results,
237
- {overallSavingsMs: maxWasted});
237
+ {overallSavingsMs: maxWasted, sortedBy: ['wastedMs']});
238
238
 
239
239
  return {
240
240
  score: ByteEfficiencyAudit.scoreForWastedMs(maxWasted),
@@ -240,7 +240,7 @@ class UsesRelPreloadAudit extends Audit {
240
240
  {key: 'wastedMs', valueType: 'timespanMs', label: str_(i18n.UIStrings.columnWastedMs)},
241
241
  ];
242
242
  const details = Audit.makeOpportunityDetails(headings, results,
243
- {overallSavingsMs: wastedMs});
243
+ {overallSavingsMs: wastedMs, sortedBy: ['wastedMs']});
244
244
 
245
245
  return {
246
246
  score: ByteEfficiencyAudit.scoreForWastedMs(wastedMs),
@@ -19,7 +19,7 @@ const UIStrings = {
19
19
  description: 'Source maps translate minified code to the original source code. This helps ' +
20
20
  'developers debug in production. In addition, Lighthouse is able to provide further ' +
21
21
  'insights. Consider deploying source maps to take advantage of these benefits. ' +
22
- '[Learn more about source maps](https://developers.google.com/web/tools/chrome-devtools/javascript/source-maps).',
22
+ '[Learn more about source maps](https://developer.chrome.com/docs/devtools/javascript/source-maps/).',
23
23
  /** Label for a column in a data table. Entries will be URLs to JavaScript source maps. */
24
24
  columnMapURL: 'Map URL',
25
25
  /** Label for a possible error message indicating that a source map for a large, first-party JavaScript script is missing. */
@@ -193,7 +193,7 @@ class WorkDuringInteraction extends Audit {
193
193
  ];
194
194
 
195
195
  return {
196
- table: Audit.makeTableDetails(headings, items),
196
+ table: Audit.makeTableDetails(headings, items, {sortedBy: ['total']}),
197
197
  phases,
198
198
  };
199
199
  }
@@ -13,17 +13,16 @@ declare const CumulativeLayoutShiftComputed: typeof CumulativeLayoutShift & {
13
13
  totalCumulativeLayoutShift: number;
14
14
  }>;
15
15
  };
16
- /** @typedef {{ts: number, isMainFrame: boolean, weightedScore: number}} LayoutShiftEvent */
17
16
  declare class CumulativeLayoutShift {
18
17
  /**
19
18
  * Returns all LayoutShift events that had no recent input.
20
19
  * Only a `weightedScore` per event is returned. For non-main-frame events, this is
21
20
  * the only score that matters. For main-frame events, `weighted_score_delta === score`.
22
21
  * @see https://source.chromium.org/chromium/chromium/src/+/main:third_party/blink/renderer/core/layout/layout_shift_tracker.cc;l=492-495;drc=de3b3a8a8839269c6b44403fa38a13a1ed12fed5
23
- * @param {LH.TraceEvent[]} traceEvents
22
+ * @param {LH.Artifacts.ProcessedTrace} processedTrace
24
23
  * @return {Array<LayoutShiftEvent>}
25
24
  */
26
- static getLayoutShiftEvents(traceEvents: LH.TraceEvent[]): Array<LayoutShiftEvent>;
25
+ static getLayoutShiftEvents(processedTrace: LH.Artifacts.ProcessedTrace): Array<LayoutShiftEvent>;
27
26
  /**
28
27
  * Calculates cumulative layout shifts per cluster (session) of LayoutShift
29
28
  * events -- where a new cluster is created when there's a gap of more than
@@ -50,4 +49,5 @@ declare class CumulativeLayoutShift {
50
49
  totalCumulativeLayoutShift: number;
51
50
  }>;
52
51
  }
52
+ import { ProcessedTrace } from "../processed-trace.js";
53
53
  //# sourceMappingURL=cumulative-layout-shift.d.ts.map
@@ -9,16 +9,18 @@ import {ProcessedTrace} from '../processed-trace.js';
9
9
 
10
10
  /** @typedef {{ts: number, isMainFrame: boolean, weightedScore: number}} LayoutShiftEvent */
11
11
 
12
+ const RECENT_INPUT_WINDOW = 500;
13
+
12
14
  class CumulativeLayoutShift {
13
15
  /**
14
16
  * Returns all LayoutShift events that had no recent input.
15
17
  * Only a `weightedScore` per event is returned. For non-main-frame events, this is
16
18
  * the only score that matters. For main-frame events, `weighted_score_delta === score`.
17
19
  * @see https://source.chromium.org/chromium/chromium/src/+/main:third_party/blink/renderer/core/layout/layout_shift_tracker.cc;l=492-495;drc=de3b3a8a8839269c6b44403fa38a13a1ed12fed5
18
- * @param {LH.TraceEvent[]} traceEvents
20
+ * @param {LH.Artifacts.ProcessedTrace} processedTrace
19
21
  * @return {Array<LayoutShiftEvent>}
20
22
  */
21
- static getLayoutShiftEvents(traceEvents) {
23
+ static getLayoutShiftEvents(processedTrace) {
22
24
  const layoutShiftEvents = [];
23
25
 
24
26
  // Chromium will set `had_recent_input` if there was recent user input, which
@@ -26,9 +28,20 @@ class CumulativeLayoutShift {
26
28
  // Lighthouse changes the emulation size. This results in the first few shift
27
29
  // events having `had_recent_input` set, so ignore it for those events.
28
30
  // See https://bugs.chromium.org/p/chromium/issues/detail?id=1094974.
29
- let ignoreHadRecentInput = true;
31
+ let mustRespectHadRecentInput = false;
32
+
33
+ // Even if emulation was applied before navigating, Chrome will issue a viewport
34
+ // change event after a navigation starts which is treated as an interaction when
35
+ // deciding the `had_recent_input` flag. Anything within 500ms of this event should
36
+ // always be counted for CLS regardless of the `had_recent_input` flag.
37
+ // See https://bugs.chromium.org/p/chromium/issues/detail?id=1302667
38
+ let viewportChangeTs = processedTrace.timestamps.timeOrigin;
39
+ const firstViewportEvent = processedTrace.frameEvents.find(event => event.name === 'viewport');
40
+ if (firstViewportEvent) {
41
+ viewportChangeTs = firstViewportEvent.ts;
42
+ }
30
43
 
31
- for (const event of traceEvents) {
44
+ for (const event of processedTrace.frameTreeEvents) {
32
45
  if (event.name !== 'LayoutShift' ||
33
46
  !event.args.data ||
34
47
  event.args.data.is_main_frame === undefined) {
@@ -42,11 +55,10 @@ class CumulativeLayoutShift {
42
55
  }
43
56
 
44
57
  if (event.args.data.had_recent_input) {
45
- // `had_recent_input` events aren't used unless currently ignoring.
46
- if (!ignoreHadRecentInput) continue;
58
+ const timing = (event.ts - viewportChangeTs) / 1000;
59
+ if (timing > RECENT_INPUT_WINDOW || mustRespectHadRecentInput) continue;
47
60
  } else {
48
- // After a false `had_recent_input`, stop ignoring property.
49
- ignoreHadRecentInput = false;
61
+ mustRespectHadRecentInput = true;
50
62
  }
51
63
 
52
64
  layoutShiftEvents.push({
@@ -106,7 +118,7 @@ class CumulativeLayoutShift {
106
118
  const processedTrace = await ProcessedTrace.request(trace, context);
107
119
 
108
120
  const allFrameShiftEvents =
109
- CumulativeLayoutShift.getLayoutShiftEvents(processedTrace.frameTreeEvents);
121
+ CumulativeLayoutShift.getLayoutShiftEvents(processedTrace);
110
122
  const mainFrameShiftEvents = allFrameShiftEvents.filter(e => e.isMainFrame);
111
123
 
112
124
  // The original Cumulative Layout Shift metric, the sum of all main-frame shift events.
@@ -7,6 +7,7 @@
7
7
  import {makeComputedArtifact} from './computed-artifact.js';
8
8
  import {ByteEfficiencyAudit} from '../audits/byte-efficiency/byte-efficiency-audit.js';
9
9
  import {NetworkRecords} from './network-records.js';
10
+ import {Util} from '../../shared/util.js';
10
11
 
11
12
  const PREVIEW_LENGTH = 100;
12
13
 
@@ -87,8 +88,7 @@ class UnusedCSS {
87
88
  * @return {string}
88
89
  */
89
90
  static determineContentPreview(content) {
90
- let preview = (content || '')
91
- .slice(0, PREVIEW_LENGTH * 5)
91
+ let preview = Util.truncate(content || '', PREVIEW_LENGTH * 5, '')
92
92
  .replace(/( {2,}|\t)+/g, ' ') // remove leading indentation if present
93
93
  .replace(/\n\s+}/g, '\n}') // completely remove indentation of closing braces
94
94
  .trim(); // trim the leading whitespace
@@ -101,16 +101,17 @@ class UnusedCSS {
101
101
  firstRuleStart > firstRuleEnd ||
102
102
  firstRuleStart > PREVIEW_LENGTH) {
103
103
  // We couldn't determine the first rule-set or it's not within the preview
104
- preview = preview.slice(0, PREVIEW_LENGTH) + '...';
104
+ preview = Util.truncate(preview, PREVIEW_LENGTH);
105
105
  } else if (firstRuleEnd < PREVIEW_LENGTH) {
106
106
  // The entire first rule-set fits within the preview
107
- preview = preview.slice(0, firstRuleEnd + 1) + ' ...';
107
+ preview = preview.slice(0, firstRuleEnd + 1) + ' ';
108
108
  } else {
109
109
  // The first rule-set doesn't fit within the preview, just show as many as we can
110
- const lastSemicolonIndex = preview.slice(0, PREVIEW_LENGTH).lastIndexOf(';');
110
+ const truncated = Util.truncate(preview, PREVIEW_LENGTH, '');
111
+ const lastSemicolonIndex = truncated.lastIndexOf(';');
111
112
  preview = lastSemicolonIndex < firstRuleStart ?
112
- preview.slice(0, PREVIEW_LENGTH) + '... } ...' :
113
- preview.slice(0, lastSemicolonIndex + 1) + ' ... } ...';
113
+ truncated + ' } ' :
114
+ preview.slice(0, lastSemicolonIndex + 1) + ' } ';
114
115
  }
115
116
  }
116
117
 
@@ -17,17 +17,6 @@ const config = {
17
17
  'bf-cache',
18
18
  ],
19
19
  },
20
- audits: [
21
- 'metrics/first-contentful-paint-3g',
22
- ],
23
- categories: {
24
- // TODO(bckenny): type extended Config where e.g. category.title isn't required
25
- performance: /** @type {LH.Config.CategoryJson} */({
26
- auditRefs: [
27
- {id: 'first-contentful-paint-3g', weight: 0},
28
- ],
29
- }),
30
- },
31
20
  };
32
21
 
33
22
  export default config;
@@ -167,10 +167,23 @@ async function prepareTargetForTimespanMode(driver, settings) {
167
167
  disableThrottling: false,
168
168
  blockedUrlPatterns: undefined,
169
169
  });
170
+ await warmUpIntlSegmenter(driver);
170
171
 
171
172
  log.timeEnd(status);
172
173
  }
173
174
 
175
+ /**
176
+ * Ensure the `Intl.Segmenter` created in `pageFunctions.truncate` is cached by v8 before
177
+ * recording the trace begins.
178
+ *
179
+ * @param {LH.Gatherer.FRTransitionalDriver} driver
180
+ */
181
+ async function warmUpIntlSegmenter(driver) {
182
+ await driver.executionContext.evaluate(pageFunctions.truncate, {
183
+ args: ['aaa', 2],
184
+ });
185
+ }
186
+
174
187
  /**
175
188
  * Prepares a target to be analyzed in navigation mode by enabling protocol domains, emulation, and new document
176
189
  * handlers for global APIs or error handling.
@@ -197,6 +210,8 @@ async function prepareTargetForNavigationMode(driver, settings) {
197
210
  await shimRequestIdleCallbackOnNewDocument(driver, settings);
198
211
  }
199
212
 
213
+ await warmUpIntlSegmenter(driver);
214
+
200
215
  log.timeEnd(status);
201
216
  }
202
217
 
@@ -84,6 +84,7 @@ function getHTMLImages(allElements) {
84
84
  isPicture,
85
85
  loading: element.loading,
86
86
  isInShadowDOM: element.getRootNode() instanceof ShadowRoot,
87
+ fetchPriority: element.fetchPriority,
87
88
  // @ts-expect-error - getNodeDetails put into scope via stringification
88
89
  node: getNodeDetails(element),
89
90
  };
@@ -70,6 +70,7 @@ function getLinkElementsInDOM() {
70
70
  crossOrigin: link.crossOrigin,
71
71
  hrefRaw,
72
72
  source,
73
+ fetchPriority: link.fetchPriority,
73
74
  // @ts-expect-error - put into scope via stringification
74
75
  node: getNodeDetails(link),
75
76
  });
@@ -134,6 +135,7 @@ class LinkElements extends FRGatherer {
134
135
  as: link.as || '',
135
136
  crossOrigin: getCrossoriginFromHeader(link.crossorigin),
136
137
  source: 'headers',
138
+ fetchPriority: link.fetchpriority,
137
139
  node: null,
138
140
  });
139
141
  }
@@ -7,7 +7,6 @@
7
7
  import fs from 'fs';
8
8
  import path from 'path';
9
9
  import stream from 'stream';
10
- import util from 'util';
11
10
 
12
11
  import log from 'lighthouse-logger';
13
12
 
@@ -18,15 +17,6 @@ import {NetworkAnalysis} from '../computed/network-analysis.js';
18
17
  import {LoadSimulator} from '../computed/load-simulator.js';
19
18
  import {LighthouseError} from '../lib/lh-error.js';
20
19
 
21
- const {promisify} = util;
22
-
23
- // Rollup does not support `promisfy` or `stream.pipeline`. Bundled files
24
- // don't need anything in this file except for `stringifyReplacer`, so a check for
25
- // truthiness before using is enough.
26
- // TODO: Can remove promisify(pipeline) in Node 15.
27
- // https://nodejs.org/api/stream.html#streams-promises-api
28
- const pipeline = promisify && promisify(stream.pipeline);
29
-
30
20
  const optionsFilename = 'options.json';
31
21
  const artifactsFilename = 'artifacts.json';
32
22
  const traceSuffix = '.trace.json';
@@ -349,7 +339,7 @@ async function saveTrace(traceData, traceFilename) {
349
339
  const traceIter = traceJsonGenerator(traceData);
350
340
  const writeStream = fs.createWriteStream(traceFilename);
351
341
 
352
- return pipeline(traceIter, writeStream);
342
+ return stream.promises.pipeline(traceIter, writeStream);
353
343
  }
354
344
 
355
345
  /**
@@ -361,7 +351,7 @@ async function saveTrace(traceData, traceFilename) {
361
351
  function saveDevtoolsLog(devtoolsLog, devtoolLogFilename) {
362
352
  const writeStream = fs.createWriteStream(devtoolLogFilename);
363
353
 
364
- return pipeline(function* () {
354
+ return stream.promises.pipeline(function* () {
365
355
  yield* arrayOfObjectsJsonGenerator(devtoolsLog);
366
356
  yield '\n';
367
357
  }, writeStream);
@@ -57,7 +57,7 @@ export namespace UIStrings {
57
57
  * If `locale` isn't provided or one could not be found, DEFAULT_LOCALE is returned.
58
58
  *
59
59
  * By default any of the locales Lighthouse has strings for can be returned, but this
60
- * can be overriden with `possibleLocales`, useful e.g. when Lighthouse is bundled and
60
+ * can be overridden with `possibleLocales`, useful e.g. when Lighthouse is bundled and
61
61
  * only DEFAULT_LOCALE is available, but `possibleLocales` can be used to select a
62
62
  * locale available to be downloaded on demand.
63
63
  * @param {string|string[]=} locales
@@ -123,7 +123,7 @@ const UIStrings = {
123
123
  * If `locale` isn't provided or one could not be found, DEFAULT_LOCALE is returned.
124
124
  *
125
125
  * By default any of the locales Lighthouse has strings for can be returned, but this
126
- * can be overriden with `possibleLocales`, useful e.g. when Lighthouse is bundled and
126
+ * can be overridden with `possibleLocales`, useful e.g. when Lighthouse is bundled and
127
127
  * only DEFAULT_LOCALE is available, but `possibleLocales` can be used to select a
128
128
  * locale available to be downloaded on demand.
129
129
  * @param {string|string[]=} locales
@@ -81,6 +81,23 @@ function upgradeLhrForCompatibility(lhr) {
81
81
  }
82
82
  }
83
83
 
84
+ // In 10.0, third-party-summary deprecated entity: LinkValue and switched to entity name string
85
+ if (audit.id === 'third-party-summary') {
86
+ if (audit.details.type === 'opportunity' || audit.details.type === 'table') {
87
+ const {headings, items} = audit.details;
88
+ if (headings[0].valueType === 'link') {
89
+ // Apply upgrade only if we are dealing with an older version (valueType=link marker).
90
+ headings[0].valueType = 'text';
91
+ for (const item of items) {
92
+ if (typeof item.entity === 'object' && item.entity.type === 'link') {
93
+ item.entity = item.entity.text;
94
+ }
95
+ }
96
+ audit.details.isEntityGrouped = true;
97
+ }
98
+ }
99
+ }
100
+
84
101
  // TODO: convert printf-style displayValue.
85
102
  // Added: #5099, v3
86
103
  // Removed: #6767, v4
@@ -225,7 +225,7 @@ class NetworkRequest {
225
225
  this.isSecure = UrlUtils.isSecureScheme(this.parsedURL.scheme);
226
226
 
227
227
  this.rendererStartTime = data.timestamp * 1000;
228
- // Expected to be overriden with better value in `_recomputeTimesWithResourceTiming`.
228
+ // Expected to be overridden with better value in `_recomputeTimesWithResourceTiming`.
229
229
  this.networkRequestTime = this.rendererStartTime;
230
230
 
231
231
  this.requestMethod = data.request.method;
@@ -10,16 +10,12 @@ export namespace pageFunctions {
10
10
  export { isPositionFixed };
11
11
  export { wrapRequestIdleCallback };
12
12
  export { getBoundingClientRect };
13
+ export { truncate };
13
14
  }
14
15
  /**
15
16
  * `typed-query-selector`'s CSS selector parser.
16
17
  */
17
18
  export type ParseSelector<T extends string> = import('typed-query-selector/parser').ParseSelector<T>;
18
- /**
19
- * @license Copyright 2018 The Lighthouse Authors. All Rights Reserved.
20
- * 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
21
- * 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.
22
- */
23
19
  /**
24
20
  * @fileoverview
25
21
  * Helper functions that are passed by `toString()` by Driver to be evaluated in target page.
@@ -65,6 +61,9 @@ declare function getElementsInDocument<T extends string>(selector: T): import("t
65
61
  * @return {string}
66
62
  */
67
63
  declare function getOuterHTMLSnippet(element: Element | ShadowRoot, ignoreAttrs?: Array<string> | undefined, snippetCharacterLimit?: number): string;
64
+ declare namespace getOuterHTMLSnippet {
65
+ function toString(): string;
66
+ }
68
67
  /**
69
68
  * Computes a memory/CPU performance benchmark index to determine rough device class.
70
69
  * @see https://github.com/GoogleChrome/lighthouse/issues/9085
@@ -125,6 +124,9 @@ declare function getNodeSelector(element: Element): string;
125
124
  * @return {string | null}
126
125
  */
127
126
  declare function getNodeLabel(element: Element): string | null;
127
+ declare namespace getNodeLabel {
128
+ function toString(): string;
129
+ }
128
130
  /**
129
131
  * This function checks if an element or an ancestor of an element is `position:fixed`.
130
132
  * In addition we ensure that the element is capable of behaving as a `position:fixed`
@@ -145,5 +147,15 @@ declare function wrapRequestIdleCallback(cpuSlowdownMultiplier: number): void;
145
147
  * @return {LH.Artifacts.Rect}
146
148
  */
147
149
  declare function getBoundingClientRect(element: Element): LH.Artifacts.Rect;
150
+ /**
151
+ *
152
+ * @param {string} string
153
+ * @param {number} characterLimit
154
+ * @return {string}
155
+ */
156
+ declare function truncate(string: string, characterLimit: number): string;
157
+ declare namespace truncate {
158
+ function toString(): string;
159
+ }
148
160
  export {};
149
161
  //# sourceMappingURL=page-functions.d.ts.map
@@ -4,6 +4,8 @@
4
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
5
  */
6
6
 
7
+ import {Util} from '../../shared/util.js';
8
+
7
9
  /**
8
10
  * @fileoverview
9
11
  * Helper functions that are passed by `toString()` by Driver to be evaluated in target page.
@@ -132,10 +134,9 @@ function getOuterHTMLSnippet(element, ignoreAttrs = [], snippetCharacterLimit =
132
134
  }
133
135
 
134
136
  // Elide attribute value if too long.
135
- if (attributeValue.length > ATTRIBUTE_CHAR_LIMIT) {
136
- attributeValue = attributeValue.slice(0, ATTRIBUTE_CHAR_LIMIT - 1) + '…';
137
- dirty = true;
138
- }
137
+ const truncatedString = truncate(attributeValue, ATTRIBUTE_CHAR_LIMIT);
138
+ if (truncatedString !== attributeValue) dirty = true;
139
+ attributeValue = truncatedString;
139
140
 
140
141
  if (dirty) {
141
142
  // Style attributes can be blocked by the CSP if they are set via `setAttribute`.
@@ -372,22 +373,6 @@ function isPositionFixed(element) {
372
373
  * @return {string | null}
373
374
  */
374
375
  function getNodeLabel(element) {
375
- // Inline so that audits that import getNodeLabel don't
376
- // also need to import truncate
377
- /**
378
- * @param {string} str
379
- * @param {number} maxLength
380
- * @return {string}
381
- */
382
- function truncate(str, maxLength) {
383
- if (str.length <= maxLength) {
384
- return str;
385
- }
386
- // Take advantage of string iterator multi-byte character awareness.
387
- // Regular `.slice` will ignore unicode character boundaries and lead to malformed text.
388
- return Array.from(str).slice(0, maxLength - 1).join('') + '…';
389
- }
390
-
391
376
  const tagName = element.tagName.toLowerCase();
392
377
  // html and body content is too broad to be useful, since they contain all page content
393
378
  if (tagName !== 'html' && tagName !== 'body') {
@@ -518,14 +503,47 @@ function getNodeDetails(element) {
518
503
  return details;
519
504
  }
520
505
 
506
+ /**
507
+ *
508
+ * @param {string} string
509
+ * @param {number} characterLimit
510
+ * @return {string}
511
+ */
512
+ function truncate(string, characterLimit) {
513
+ return Util.truncate(string, characterLimit);
514
+ }
515
+
516
+ /** @type {string} */
517
+ const truncateRawString = truncate.toString();
518
+ truncate.toString = () => `function truncate(string, characterLimit) {
519
+ const Util = { ${Util.truncate} };
520
+ return (${truncateRawString})(string, characterLimit);
521
+ }`;
522
+
523
+ /** @type {string} */
524
+ const getNodeLabelRawString = getNodeLabel.toString();
525
+ getNodeLabel.toString = () => `function getNodeLabel(element) {
526
+ ${truncate};
527
+ return (${getNodeLabelRawString})(element);
528
+ }`;
529
+
530
+ /** @type {string} */
531
+ const getOuterHTMLSnippetRawString = getOuterHTMLSnippet.toString();
532
+ // eslint-disable-next-line max-len
533
+ getOuterHTMLSnippet.toString = () => `function getOuterHTMLSnippet(element, ignoreAttrs = [], snippetCharacterLimit = 500) {
534
+ ${truncate};
535
+ return (${getOuterHTMLSnippetRawString})(element, ignoreAttrs, snippetCharacterLimit);
536
+ }`;
537
+
521
538
  /** @type {string} */
522
539
  const getNodeDetailsRawString = getNodeDetails.toString();
523
540
  getNodeDetails.toString = () => `function getNodeDetails(element) {
541
+ ${truncate};
524
542
  ${getNodePath};
525
543
  ${getNodeSelector};
526
544
  ${getBoundingClientRect};
527
- ${getOuterHTMLSnippet};
528
- ${getNodeLabel};
545
+ ${getOuterHTMLSnippetRawString};
546
+ ${getNodeLabelRawString};
529
547
  return (${getNodeDetailsRawString})(element);
530
548
  }`;
531
549
 
@@ -541,4 +559,5 @@ export const pageFunctions = {
541
559
  isPositionFixed,
542
560
  wrapRequestIdleCallback,
543
561
  getBoundingClientRect,
562
+ truncate,
544
563
  };
@@ -179,6 +179,14 @@ export class TraceProcessor {
179
179
  * @return {evt is LCPCandidateEvent}
180
180
  */
181
181
  static isLCPCandidateEvent(evt: LH.TraceEvent): evt is LCPCandidateEvent;
182
+ /**
183
+ * The associated frame ID is set in different locations for different trace events.
184
+ * This function checks all known locations for the frame ID and returns `undefined` if it's not found.
185
+ *
186
+ * @param {LH.TraceEvent} evt
187
+ * @return {string|undefined}
188
+ */
189
+ static getFrameId(evt: LH.TraceEvent): string | undefined;
182
190
  /**
183
191
  * Returns the maximum LCP event across all frames in `events`.
184
192
  * Sets `invalidated` flag if LCP of every frame is invalidated.
@@ -506,14 +506,22 @@ class TraceProcessor {
506
506
  const pidToTid = new Map();
507
507
 
508
508
  for (const pid of new Set(mainFramePids)) {
509
- // While renderer tids are generally predictable, we'll doublecheck it
510
- const threadNameEvt = keyEvents.find(e =>
509
+ const threadEvents = keyEvents.filter(e =>
511
510
  e.cat === '__metadata' &&
512
511
  e.pid === pid &&
513
512
  e.ph === 'M' &&
514
- e.name === 'thread_name' &&
515
- e.args.name === 'CrRendererMain'
513
+ e.name === 'thread_name'
516
514
  );
515
+
516
+ // While renderer tids are generally predictable, we'll doublecheck it
517
+ let threadNameEvt = threadEvents.find(e => e.args.name === 'CrRendererMain');
518
+
519
+ // `CrRendererMain` can be missing if chrome is launched with the `--single-process` flag.
520
+ // In this case, page tasks will be run in the browser thread.
521
+ if (!threadNameEvt) {
522
+ threadNameEvt = threadEvents.find(e => e.args.name === 'CrBrowserMain');
523
+ }
524
+
517
525
  const tid = threadNameEvt?.tid;
518
526
 
519
527
  if (!tid) {
@@ -559,6 +567,19 @@ class TraceProcessor {
559
567
  );
560
568
  }
561
569
 
570
+ /**
571
+ * The associated frame ID is set in different locations for different trace events.
572
+ * This function checks all known locations for the frame ID and returns `undefined` if it's not found.
573
+ *
574
+ * @param {LH.TraceEvent} evt
575
+ * @return {string|undefined}
576
+ */
577
+ static getFrameId(evt) {
578
+ return evt.args?.data?.frame ||
579
+ evt.args.data?.frameID ||
580
+ evt.args.frame;
581
+ }
582
+
562
583
  /**
563
584
  * Returns the maximum LCP event across all frames in `events`.
564
585
  * Sets `invalidated` flag if LCP of every frame is invalidated.
@@ -704,13 +725,13 @@ class TraceProcessor {
704
725
  // Filter to just events matching the main frame ID, just to make sure.
705
726
  /** @param {LH.TraceEvent} e */
706
727
  function associatedToMainFrame(e) {
707
- const frameId = e.args?.data?.frame || e.args.frame;
728
+ const frameId = TraceProcessor.getFrameId(e);
708
729
  return frameId === mainFrameInfo.frameId;
709
730
  }
710
731
 
711
732
  /** @param {LH.TraceEvent} e */
712
733
  function associatedToAllFrames(e) {
713
- const frameId = e.args?.data?.frame || e.args.frame;
734
+ const frameId = TraceProcessor.getFrameId(e);
714
735
  return frameId ? inspectedTreeFrameIds.includes(frameId) : false;
715
736
  }
716
737
  const frameEvents = keyEvents.filter(e => associatedToMainFrame(e));
@@ -138,7 +138,7 @@ class UrlUtils {
138
138
  static elideDataURI(url) {
139
139
  try {
140
140
  const parsed = new URL(url);
141
- return parsed.protocol === 'data:' ? url.slice(0, 100) : url;
141
+ return parsed.protocol === 'data:' ? Util.truncate(url, 100) : url;
142
142
  } catch (e) {
143
143
  return url;
144
144
  }
package/core/runner.js CHANGED
@@ -170,6 +170,7 @@ class Runner {
170
170
  // Reduce payload size in LHR JSON by omitting whats falsy.
171
171
  if (entity === classifiedEntities.firstParty) shortEntity.isFirstParty = true;
172
172
  if (entity.isUnrecognized) shortEntity.isUnrecognized = true;
173
+ if (entity.category) shortEntity.category = entity.category;
173
174
  entities.push(shortEntity);
174
175
  }
175
176