lighthouse 13.1.0 → 13.3.0

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 (118) hide show
  1. package/cli/bin.js +5 -0
  2. package/cli/cli-flags.js +2 -2
  3. package/cli/test/smokehouse/lighthouse-runners/bundle.js +7 -3
  4. package/cli/test/smokehouse/lighthouse-runners/cli.js +5 -1
  5. package/cli/test/smokehouse/lighthouse-runners/devtools-mcp.js +7 -3
  6. package/cli/test/smokehouse/lighthouse-runners/devtools.js +4 -1
  7. package/cli/test/smokehouse/smokehouse.js +7 -2
  8. package/core/audits/accessibility/autocomplete-valid.d.ts +10 -0
  9. package/core/audits/accessibility/autocomplete-valid.js +44 -0
  10. package/core/audits/accessibility/presentation-role-conflict.d.ts +10 -0
  11. package/core/audits/accessibility/presentation-role-conflict.js +46 -0
  12. package/core/audits/accessibility/svg-img-alt.d.ts +10 -0
  13. package/core/audits/accessibility/svg-img-alt.js +44 -0
  14. package/core/audits/agentic/agent-accessibility-tree.d.ts +19 -0
  15. package/core/audits/agentic/agent-accessibility-tree.js +115 -0
  16. package/core/audits/agentic/llms-txt.d.ts +20 -0
  17. package/core/audits/agentic/llms-txt.js +111 -0
  18. package/core/audits/insights/insight-audit.d.ts +2 -2
  19. package/core/audits/insights/insight-audit.js +16 -6
  20. package/core/audits/layout-shifts.js +1 -1
  21. package/core/audits/server-response-time.js +3 -3
  22. package/core/audits/webmcp-form-coverage.d.ts +16 -0
  23. package/core/audits/webmcp-form-coverage.js +90 -0
  24. package/core/audits/webmcp-registered-tools.d.ts +21 -0
  25. package/core/audits/webmcp-registered-tools.js +149 -0
  26. package/core/audits/webmcp-schema-validity.d.ts +22 -0
  27. package/core/audits/webmcp-schema-validity.js +141 -0
  28. package/core/computed/document-urls.js +0 -1
  29. package/core/computed/main-resource.js +0 -2
  30. package/core/computed/metrics/lantern-metric.js +4 -4
  31. package/core/computed/metrics/lcp-breakdown.js +1 -1
  32. package/core/computed/metrics/time-to-first-byte.js +1 -1
  33. package/core/computed/navigation-insights.js +2 -1
  34. package/core/computed/network-analysis.js +0 -1
  35. package/core/config/agentic-browsing-config.d.ts +12 -0
  36. package/core/config/agentic-browsing-config.js +73 -0
  37. package/core/config/default-config.js +51 -0
  38. package/core/gather/gatherers/accessibility.js +5 -1
  39. package/core/gather/gatherers/agentic/llms-txt.d.ts +10 -0
  40. package/core/gather/gatherers/agentic/llms-txt.js +28 -0
  41. package/core/gather/gatherers/inputs.js +2 -0
  42. package/core/gather/gatherers/meta-elements.js +1 -1
  43. package/core/gather/gatherers/trace-elements.js +1 -1
  44. package/core/gather/gatherers/webmcp-schema.d.ts +25 -0
  45. package/core/gather/gatherers/webmcp-schema.js +105 -0
  46. package/core/gather/gatherers/webmcp.d.ts +58 -0
  47. package/core/gather/gatherers/webmcp.js +159 -0
  48. package/core/lib/baseline/web-features-metadata.json +1 -1
  49. package/core/lib/cdt/generated/SourceMap.js +2 -2
  50. package/core/lib/deprecations-strings.d.ts +26 -20
  51. package/core/lib/deprecations-strings.js +7 -0
  52. package/core/lib/navigation-error.js +0 -6
  53. package/core/lib/network-request.js +0 -1
  54. package/core/lib/page-functions.d.ts +3 -3
  55. package/core/lib/page-functions.js +11 -4
  56. package/core/lib/tracehouse/trace-processor.d.ts +5 -4
  57. package/core/lib/tracehouse/trace-processor.js +85 -19
  58. package/core/runner.js +3 -0
  59. package/core/scoring.d.ts +1 -0
  60. package/dist/report/bundle.esm.js +1 -1
  61. package/dist/report/flow.js +3 -3
  62. package/dist/report/standalone.js +1 -1
  63. package/flow-report/src/summary/category.tsx +1 -1
  64. package/package.json +11 -11
  65. package/report/renderer/category-renderer.js +1 -1
  66. package/report/renderer/report-utils.d.ts +2 -1
  67. package/report/renderer/report-utils.js +7 -2
  68. package/shared/localization/locales/ar-XB.json +72 -36
  69. package/shared/localization/locales/ar.json +72 -36
  70. package/shared/localization/locales/bg.json +72 -36
  71. package/shared/localization/locales/ca.json +72 -36
  72. package/shared/localization/locales/cs.json +72 -36
  73. package/shared/localization/locales/da.json +74 -38
  74. package/shared/localization/locales/de.json +72 -36
  75. package/shared/localization/locales/el.json +73 -37
  76. package/shared/localization/locales/en-GB.json +74 -38
  77. package/shared/localization/locales/en-US.json +263 -17
  78. package/shared/localization/locales/en-XL.json +263 -17
  79. package/shared/localization/locales/es-419.json +72 -36
  80. package/shared/localization/locales/es.json +73 -37
  81. package/shared/localization/locales/fi.json +72 -36
  82. package/shared/localization/locales/fil.json +74 -38
  83. package/shared/localization/locales/fr.json +162 -126
  84. package/shared/localization/locales/he.json +74 -38
  85. package/shared/localization/locales/hi.json +73 -37
  86. package/shared/localization/locales/hr.json +72 -36
  87. package/shared/localization/locales/hu.json +73 -37
  88. package/shared/localization/locales/id.json +74 -38
  89. package/shared/localization/locales/it.json +72 -36
  90. package/shared/localization/locales/ja.json +72 -36
  91. package/shared/localization/locales/ko.json +72 -36
  92. package/shared/localization/locales/lt.json +72 -36
  93. package/shared/localization/locales/lv.json +72 -36
  94. package/shared/localization/locales/nl.json +73 -37
  95. package/shared/localization/locales/no.json +72 -36
  96. package/shared/localization/locales/pl.json +72 -36
  97. package/shared/localization/locales/pt-PT.json +72 -36
  98. package/shared/localization/locales/pt.json +74 -38
  99. package/shared/localization/locales/ro.json +72 -36
  100. package/shared/localization/locales/ru.json +72 -36
  101. package/shared/localization/locales/sk.json +72 -36
  102. package/shared/localization/locales/sl.json +72 -36
  103. package/shared/localization/locales/sr-Latn.json +73 -37
  104. package/shared/localization/locales/sr.json +73 -37
  105. package/shared/localization/locales/sv.json +75 -39
  106. package/shared/localization/locales/ta.json +73 -37
  107. package/shared/localization/locales/te.json +72 -36
  108. package/shared/localization/locales/th.json +73 -37
  109. package/shared/localization/locales/tr.json +72 -36
  110. package/shared/localization/locales/uk.json +72 -36
  111. package/shared/localization/locales/vi.json +74 -38
  112. package/shared/localization/locales/zh-HK.json +72 -36
  113. package/shared/localization/locales/zh-TW.json +74 -38
  114. package/shared/localization/locales/zh.json +75 -39
  115. package/types/artifacts.d.ts +33 -0
  116. package/types/config.d.ts +1 -0
  117. package/types/internal/smokehouse.d.ts +7 -1
  118. package/types/lhr/lhr.d.ts +11 -0
@@ -50,6 +50,7 @@ export namespace UIStrings {
50
50
  let PrefixedVideoExitFullScreen: string;
51
51
  let PrefixedVideoExitFullscreen: string;
52
52
  let PrefixedVideoSupportsFullscreen: string;
53
+ let PreventSvgFilterPaint: string;
53
54
  let RangeExpand: string;
54
55
  let RelatedWebsiteSets: string;
55
56
  let RequestedSubresourceWithEmbeddedCredentials: string;
@@ -237,6 +238,11 @@ export namespace DEPRECATIONS_METADATA {
237
238
  export { milestone_12 as milestone };
238
239
  }
239
240
  export { PersistentQuotaType_1 as PersistentQuotaType };
241
+ export namespace PreventSvgFilterPaint_1 {
242
+ let chromeStatusFeature_25: number;
243
+ export { chromeStatusFeature_25 as chromeStatusFeature };
244
+ }
245
+ export { PreventSvgFilterPaint_1 as PreventSvgFilterPaint };
240
246
  export namespace RTCConstraintEnableDtlsSrtpFalse_1 {
241
247
  let milestone_13: number;
242
248
  export { milestone_13 as milestone };
@@ -248,25 +254,25 @@ export namespace DEPRECATIONS_METADATA {
248
254
  }
249
255
  export { RTCConstraintEnableDtlsSrtpTrue_1 as RTCConstraintEnableDtlsSrtpTrue };
250
256
  export namespace RTCPeerConnectionGetStatsLegacyNonCompliant_1 {
251
- let chromeStatusFeature_25: number;
252
- export { chromeStatusFeature_25 as chromeStatusFeature };
257
+ let chromeStatusFeature_26: number;
258
+ export { chromeStatusFeature_26 as chromeStatusFeature };
253
259
  let milestone_15: number;
254
260
  export { milestone_15 as milestone };
255
261
  }
256
262
  export { RTCPeerConnectionGetStatsLegacyNonCompliant_1 as RTCPeerConnectionGetStatsLegacyNonCompliant };
257
263
  export namespace RelatedWebsiteSets_1 {
258
- let chromeStatusFeature_26: number;
259
- export { chromeStatusFeature_26 as chromeStatusFeature };
264
+ let chromeStatusFeature_27: number;
265
+ export { chromeStatusFeature_27 as chromeStatusFeature };
260
266
  }
261
267
  export { RelatedWebsiteSets_1 as RelatedWebsiteSets };
262
268
  export namespace RequestedSubresourceWithEmbeddedCredentials_1 {
263
- let chromeStatusFeature_27: number;
264
- export { chromeStatusFeature_27 as chromeStatusFeature };
269
+ let chromeStatusFeature_28: number;
270
+ export { chromeStatusFeature_28 as chromeStatusFeature };
265
271
  }
266
272
  export { RequestedSubresourceWithEmbeddedCredentials_1 as RequestedSubresourceWithEmbeddedCredentials };
267
273
  export namespace RtcpMuxPolicyNegotiate_1 {
268
- let chromeStatusFeature_28: number;
269
- export { chromeStatusFeature_28 as chromeStatusFeature };
274
+ let chromeStatusFeature_29: number;
275
+ export { chromeStatusFeature_29 as chromeStatusFeature };
270
276
  let milestone_16: number;
271
277
  export { milestone_16 as milestone };
272
278
  }
@@ -277,25 +283,25 @@ export namespace DEPRECATIONS_METADATA {
277
283
  }
278
284
  export { SharedArrayBufferConstructedWithoutIsolation_1 as SharedArrayBufferConstructedWithoutIsolation };
279
285
  export namespace SharedStorage_1 {
280
- let chromeStatusFeature_29: number;
281
- export { chromeStatusFeature_29 as chromeStatusFeature };
286
+ let chromeStatusFeature_30: number;
287
+ export { chromeStatusFeature_30 as chromeStatusFeature };
282
288
  }
283
289
  export { SharedStorage_1 as SharedStorage };
284
290
  export namespace StorageAccessAPI_requestStorageAccessFor_Method_1 {
285
- let chromeStatusFeature_30: number;
286
- export { chromeStatusFeature_30 as chromeStatusFeature };
291
+ let chromeStatusFeature_31: number;
292
+ export { chromeStatusFeature_31 as chromeStatusFeature };
287
293
  }
288
294
  export { StorageAccessAPI_requestStorageAccessFor_Method_1 as StorageAccessAPI_requestStorageAccessFor_Method };
289
295
  export namespace TextToSpeech_DisallowedByAutoplay_1 {
290
- let chromeStatusFeature_31: number;
291
- export { chromeStatusFeature_31 as chromeStatusFeature };
296
+ let chromeStatusFeature_32: number;
297
+ export { chromeStatusFeature_32 as chromeStatusFeature };
292
298
  let milestone_18: number;
293
299
  export { milestone_18 as milestone };
294
300
  }
295
301
  export { TextToSpeech_DisallowedByAutoplay_1 as TextToSpeech_DisallowedByAutoplay };
296
302
  export namespace UnloadHandler_1 {
297
- let chromeStatusFeature_32: number;
298
- export { chromeStatusFeature_32 as chromeStatusFeature };
303
+ let chromeStatusFeature_33: number;
304
+ export { chromeStatusFeature_33 as chromeStatusFeature };
299
305
  }
300
306
  export { UnloadHandler_1 as UnloadHandler };
301
307
  export namespace V8SharedArrayBufferConstructedInExtensionWithoutIsolation_1 {
@@ -304,8 +310,8 @@ export namespace DEPRECATIONS_METADATA {
304
310
  }
305
311
  export { V8SharedArrayBufferConstructedInExtensionWithoutIsolation_1 as V8SharedArrayBufferConstructedInExtensionWithoutIsolation };
306
312
  export namespace WebBluetoothRemoteCharacteristicWriteValue_1 {
307
- let chromeStatusFeature_33: number;
308
- export { chromeStatusFeature_33 as chromeStatusFeature };
313
+ let chromeStatusFeature_34: number;
314
+ export { chromeStatusFeature_34 as chromeStatusFeature };
309
315
  }
310
316
  export { WebBluetoothRemoteCharacteristicWriteValue_1 as WebBluetoothRemoteCharacteristicWriteValue };
311
317
  export namespace XHRJSONEncodingDetection_1 {
@@ -314,8 +320,8 @@ export namespace DEPRECATIONS_METADATA {
314
320
  }
315
321
  export { XHRJSONEncodingDetection_1 as XHRJSONEncodingDetection };
316
322
  export namespace XSLT_1 {
317
- let chromeStatusFeature_34: number;
318
- export { chromeStatusFeature_34 as chromeStatusFeature };
323
+ let chromeStatusFeature_35: number;
324
+ export { chromeStatusFeature_35 as chromeStatusFeature };
319
325
  let milestone_21: number;
320
326
  export { milestone_21 as milestone };
321
327
  }
@@ -212,6 +212,10 @@ export const UIStrings = {
212
212
  * @description Standard message when one web API is deprecated in favor of another.
213
213
  */
214
214
  PrefixedVideoSupportsFullscreen: "HTMLVideoElement.webkitSupportsFullscreen is deprecated. Please use Document.fullscreenEnabled instead.",
215
+ /**
216
+ * @description Warning displayed to developers when an SVG filter is applied to a disallowed content type.
217
+ */
218
+ PreventSvgFilterPaint: "SVG filters cannot be applied to cross-origin iframes, restricted iframes (e.g., sandboxed), or plugins.",
215
219
  /**
216
220
  * @description Standard message when one web API is deprecated in favor of another.
217
221
  */
@@ -391,6 +395,9 @@ export const DEPRECATIONS_METADATA = {
391
395
  "chromeStatusFeature": 5176235376246784,
392
396
  "milestone": 106
393
397
  },
398
+ "PreventSvgFilterPaint": {
399
+ "chromeStatusFeature": 5117170452398080
400
+ },
394
401
  "RTCConstraintEnableDtlsSrtpFalse": {
395
402
  "milestone": 97
396
403
  },
@@ -131,7 +131,6 @@ function getNonHtmlError(finalRecord) {
131
131
  function getPageLoadError(navigationError, context) {
132
132
  const {url, networkRecords} = context;
133
133
  const mainRecordLantern = Lantern.Core.NetworkAnalyzer.findResourceForUrl(
134
- // @ts-expect-error - trace engine types for InitiatorType are outdated
135
134
  networkRecords,
136
135
  url
137
136
  );
@@ -144,7 +143,6 @@ function getPageLoadError(navigationError, context) {
144
143
  record.resourceType === NetworkRequest.TYPES.Document
145
144
  );
146
145
  if (documentRequests.length) {
147
- // @ts-expect-error - mainRecord is inferred as a Lantern request from findResourceForUrl, but we assign a raw record here.
148
146
  mainRecord = documentRequests.reduce((min, r) => {
149
147
  return r.networkRequestTime < min.networkRequestTime ? r : min;
150
148
  });
@@ -164,12 +162,8 @@ function getPageLoadError(navigationError, context) {
164
162
  context.warnings.push(str_(UIStrings.warningXhtml));
165
163
  }
166
164
 
167
- // @ts-expect-error - mainRecord may be typed as a Lantern request, but functions expect a raw record.
168
165
  const networkError = getNetworkError(mainRecord, context);
169
- // @ts-expect-error - mainRecord may be typed as a Lantern request, but functions expect a raw record.
170
166
  const interstitialError = getInterstitialError(mainRecord, networkRecords);
171
- // @ts-expect-error - finalRecord may be a Lantern request, which is compatible enough
172
- // for getNonHtmlError.
173
167
  const nonHtmlError = getNonHtmlError(finalRecord);
174
168
 
175
169
  // We want to special-case the interstitial beyond FAILED_DOCUMENT_REQUEST. See https://github.com/GoogleChrome/lighthouse/pull/8865#issuecomment-497507618
@@ -592,7 +592,6 @@ class NetworkRequest {
592
592
 
593
593
  record.fromWorker = record.sessionTargetType === 'worker';
594
594
 
595
- // @ts-expect-error - trace engine types for InitiatorType are outdated
596
595
  return {
597
596
  rawRequest: record,
598
597
  ...record,
@@ -88,10 +88,10 @@ declare namespace getOuterHTMLSnippet {
88
88
  */
89
89
  declare function computeBenchmarkIndex(): number;
90
90
  /**
91
- * @param {Element|ShadowRoot} element
92
- * @return {LH.Artifacts.NodeDetails}
91
+ * @param {Node|ShadowRoot} node
92
+ * @return {LH.Artifacts.NodeDetails | null}
93
93
  */
94
- declare function getNodeDetails(element: Element | ShadowRoot): LH.Artifacts.NodeDetails;
94
+ declare function getNodeDetails(node: Node | ShadowRoot): LH.Artifacts.NodeDetails | null;
95
95
  declare namespace getNodeDetails {
96
96
  function toString(): string;
97
97
  }
@@ -450,16 +450,23 @@ function wrapRequestIdleCallback(cpuSlowdownMultiplier) {
450
450
  }
451
451
 
452
452
  /**
453
- * @param {Element|ShadowRoot} element
454
- * @return {LH.Artifacts.NodeDetails}
453
+ * @param {Node|ShadowRoot} node
454
+ * @return {LH.Artifacts.NodeDetails | null}
455
455
  */
456
- function getNodeDetails(element) {
456
+ function getNodeDetails(node) {
457
457
  // This bookkeeping is for the FullPageScreenshot gatherer.
458
458
  if (!window.__lighthouseNodesDontTouchOrAllVarianceGoesAway) {
459
459
  window.__lighthouseNodesDontTouchOrAllVarianceGoesAway = new Map();
460
460
  }
461
461
 
462
- element = element instanceof ShadowRoot ? element.host : element;
462
+ let elem = node.nodeType === Node.ELEMENT_NODE ? node : node.parentElement;
463
+ if (!elem && node instanceof ShadowRoot) {
464
+ elem = node.host;
465
+ }
466
+
467
+ if (!elem) return null;
468
+
469
+ const element = /** @type {Element} */ (elem);
463
470
  const selector = getNodeSelector(element);
464
471
 
465
472
  // Create an id that will be unique across all execution contexts.
@@ -196,9 +196,10 @@ export class TraceProcessor {
196
196
  *
197
197
  * @param {LH.TraceEvent[]} events
198
198
  * @param {LH.TraceEvent} timeOriginEvent
199
+ * @param {string|undefined} mainFrameId
199
200
  * @return {{lcp: LCPEvent | undefined, invalidated: boolean}}
200
201
  */
201
- static computeValidLCPAllFrames(events: LH.TraceEvent[], timeOriginEvent: LH.TraceEvent): {
202
+ static computeValidLCPAllFrames(events: LH.TraceEvent[], timeOriginEvent: LH.TraceEvent, mainFrameId: string | undefined): {
202
203
  lcp: LCPEvent | undefined;
203
204
  invalidated: boolean;
204
205
  };
@@ -217,7 +218,7 @@ export class TraceProcessor {
217
218
  * @param {LH.Trace} trace
218
219
  * @param {{timeOriginDeterminationMethod?: TimeOriginDeterminationMethod}} [options]
219
220
  * @return {LH.Artifacts.ProcessedTrace}
220
- */
221
+ */
221
222
  static processTrace(trace: LH.Trace, options?: {
222
223
  timeOriginDeterminationMethod?: TimeOriginDeterminationMethod;
223
224
  }): LH.Artifacts.ProcessedTrace;
@@ -226,7 +227,7 @@ export class TraceProcessor {
226
227
  * origin in addition to the standard microsecond monotonic timestamps.
227
228
  * @param {LH.Artifacts.ProcessedTrace} processedTrace
228
229
  * @return {LH.Artifacts.ProcessedNavigation}
229
- */
230
+ */
230
231
  static processNavigation(processedTrace: LH.Artifacts.ProcessedTrace): LH.Artifacts.ProcessedNavigation;
231
232
  /**
232
233
  * Computes the last observable timestamp in a set of trace events.
@@ -272,7 +273,7 @@ export class TraceProcessor {
272
273
  * in addition to the standard microsecond monotonic timestamps.
273
274
  * @param {Array<LH.TraceEvent>} frameEvents
274
275
  * @param {{timeOriginEvt: LH.TraceEvent}} options
275
- */
276
+ */
276
277
  static computeNavigationTimingsForFrame(frameEvents: Array<LH.TraceEvent>, options: {
277
278
  timeOriginEvt: LH.TraceEvent;
278
279
  }): {
@@ -349,7 +349,7 @@ class TraceProcessor {
349
349
 
350
350
  const ret = this.getMainThreadTopLevelEventDurations(events, startTime, endTime);
351
351
  return this._riskPercentiles(ret.durations, totalTime, percentiles,
352
- ret.clippedLength);
352
+ ret.clippedLength);
353
353
  }
354
354
 
355
355
  /**
@@ -479,9 +479,9 @@ class TraceProcessor {
479
479
  const firstResourceSendEvt = events.find(e => e.name === 'ResourceSendRequest');
480
480
  // We know that these properties exist if we found the events, but TSC doesn't.
481
481
  if (navStartEvt?.args?.data &&
482
- firstResourceSendEvt &&
483
- firstResourceSendEvt.pid === navStartEvt.pid &&
484
- firstResourceSendEvt.tid === navStartEvt.tid) {
482
+ firstResourceSendEvt &&
483
+ firstResourceSendEvt.pid === navStartEvt.pid &&
484
+ firstResourceSendEvt.tid === navStartEvt.tid) {
485
485
  const frameId = navStartEvt.args.frame;
486
486
  if (frameId) {
487
487
  return {
@@ -552,9 +552,9 @@ class TraceProcessor {
552
552
  */
553
553
  static isScheduleableTask(evt) {
554
554
  return evt.name === SCHEDULABLE_TASK_TITLE_LH ||
555
- evt.name === SCHEDULABLE_TASK_TITLE_ALT1 ||
556
- evt.name === SCHEDULABLE_TASK_TITLE_ALT2 ||
557
- evt.name === SCHEDULABLE_TASK_TITLE_ALT3;
555
+ evt.name === SCHEDULABLE_TASK_TITLE_ALT1 ||
556
+ evt.name === SCHEDULABLE_TASK_TITLE_ALT2 ||
557
+ evt.name === SCHEDULABLE_TASK_TITLE_ALT3;
558
558
  }
559
559
 
560
560
  /**
@@ -563,7 +563,7 @@ class TraceProcessor {
563
563
  */
564
564
  static isLCPEvent(evt) {
565
565
  if (evt.name !== 'largestContentfulPaint::Invalidate' &&
566
- evt.name !== 'largestContentfulPaint::Candidate') return false;
566
+ evt.name !== 'largestContentfulPaint::Candidate') return false;
567
567
  return Boolean(evt.args?.frame);
568
568
  }
569
569
 
@@ -602,9 +602,10 @@ class TraceProcessor {
602
602
  *
603
603
  * @param {LH.TraceEvent[]} events
604
604
  * @param {LH.TraceEvent} timeOriginEvent
605
+ * @param {string|undefined} mainFrameId
605
606
  * @return {{lcp: LCPEvent | undefined, invalidated: boolean}}
606
607
  */
607
- static computeValidLCPAllFrames(events, timeOriginEvent) {
608
+ static computeValidLCPAllFrames(events, timeOriginEvent, mainFrameId) {
608
609
  const lcpEvents = events.filter(this.isLCPEvent).reverse();
609
610
 
610
611
  /** @type {Map<string, LCPEvent>} */
@@ -628,6 +629,53 @@ class TraceProcessor {
628
629
  }
629
630
  }
630
631
 
632
+ // If no standard LCP candidate is found, try the UKM AllFramesEvents.
633
+ if (!maxLcpAcrossFrames) {
634
+ const ukmEvents = events.filter(
635
+ (e) =>
636
+ e.name.includes('LargestContentfulPaint') && e.name.includes('UKM')
637
+ );
638
+
639
+ // In the rare cases this whole fallback is necessary, the
640
+ // NavStartToLargestContentfulPaint::Candidate::AllFrames::UKM events are missing too.
641
+ // As a result, the only useful signal left is the AllFrames invalidates.
642
+ // Not ideal since they are 1 paint behind, but.. better than the dreaded
643
+ // NO_LCP error
644
+ const targetEventName =
645
+ 'NavStartToLargestContentfulPaint::Invalidate::AllFrames::UKM';
646
+ const ukmInvalidates = ukmEvents.filter((e) => e.name === targetEventName);
647
+
648
+ if (ukmInvalidates.length > 0) {
649
+ ukmInvalidates.sort((a, b) => a.ts - b.ts);
650
+ const lastInvalidate = ukmInvalidates[ukmInvalidates.length - 1];
651
+
652
+ log.warn(
653
+ 'TraceProcessor',
654
+ 'LCP candidate missing, falling back to UKM Invalidate event.'
655
+ );
656
+
657
+ // Construct a mock LCP candidate event
658
+ maxLcpAcrossFrames = /** @type {LCPCandidateEvent} */ (
659
+ /** @type {unknown} */ ({
660
+ name: 'largestContentfulPaint::Candidate',
661
+ cat: 'loading',
662
+ ph: lastInvalidate.ph,
663
+ ts: lastInvalidate.ts,
664
+ pid: lastInvalidate.pid,
665
+ tid: lastInvalidate.tid,
666
+ args: {
667
+ frame: mainFrameId || 'main_frame', // Mocked frame ID
668
+ data: {
669
+ size: 1, // Don't know the actuall size so we assign it 1
670
+ isMainFrame: true,
671
+ isOutermostMainFrame: true,
672
+ },
673
+ },
674
+ })
675
+ );
676
+ }
677
+ }
678
+
631
679
  return {
632
680
  lcp: maxLcpAcrossFrames,
633
681
  // LCP events were found, but final LCP event of every frame was an invalidate event.
@@ -669,7 +717,7 @@ class TraceProcessor {
669
717
  * @param {LH.Trace} trace
670
718
  * @param {{timeOriginDeterminationMethod?: TimeOriginDeterminationMethod}} [options]
671
719
  * @return {LH.Artifacts.ProcessedTrace}
672
- */
720
+ */
673
721
  static processTrace(trace, options) {
674
722
  const {timeOriginDeterminationMethod = 'auto'} = options || {};
675
723
 
@@ -677,9 +725,9 @@ class TraceProcessor {
677
725
  // *must* be stable to keep events correctly nested.
678
726
  const keyEvents = this.filteredTraceSort(trace.traceEvents, e => {
679
727
  return e.cat.includes('blink.user_timing') ||
680
- e.cat.includes('loading') ||
681
- e.cat.includes('devtools.timeline') ||
682
- e.cat === '__metadata';
728
+ e.cat.includes('loading') ||
729
+ e.cat.includes('devtools.timeline') ||
730
+ e.cat === '__metadata';
683
731
  });
684
732
 
685
733
  // Find the inspected frame
@@ -699,7 +747,7 @@ class TraceProcessor {
699
747
  // Begin collection of frame tree information with TracingStartedInBrowser,
700
748
  // which should be present even without navigations.
701
749
  const tracingStartedFrames = keyEvents
702
- .find(e => e.name === 'TracingStartedInBrowser')?.args?.data?.frames;
750
+ .find(e => e.name === 'TracingStartedInBrowser')?.args?.data?.frames;
703
751
  if (tracingStartedFrames) {
704
752
  for (const frame of tracingStartedFrames) {
705
753
  framesById.set(frame.frame, {
@@ -738,12 +786,18 @@ class TraceProcessor {
738
786
  // Filter to just events matching the main frame ID, just to make sure.
739
787
  /** @param {LH.TraceEvent} e */
740
788
  function associatedToMainFrame(e) {
789
+ if (e.name === 'NavStartToLargestContentfulPaint::Invalidate::AllFrames::UKM') {
790
+ return true;
791
+ }
741
792
  const frameId = TraceProcessor.getFrameId(e);
742
793
  return frameId === mainFrameInfo.frameId;
743
794
  }
744
795
 
745
796
  /** @param {LH.TraceEvent} e */
746
797
  function associatedToAllFrames(e) {
798
+ if (e.name.includes('LargestContentfulPaint') && e.name.includes('UKM')) {
799
+ return true;
800
+ }
747
801
  const frameId = TraceProcessor.getFrameId(e);
748
802
  return frameId ? inspectedTreeFrameIds.includes(frameId) : false;
749
803
  }
@@ -805,9 +859,16 @@ class TraceProcessor {
805
859
  * origin in addition to the standard microsecond monotonic timestamps.
806
860
  * @param {LH.Artifacts.ProcessedTrace} processedTrace
807
861
  * @return {LH.Artifacts.ProcessedNavigation}
808
- */
862
+ */
809
863
  static processNavigation(processedTrace) {
810
- const {frameEvents, frameTreeEvents, timeOriginEvt, timings, timestamps} = processedTrace;
864
+ const {
865
+ frameEvents,
866
+ frameTreeEvents,
867
+ timeOriginEvt,
868
+ timings,
869
+ timestamps,
870
+ mainFrameInfo,
871
+ } = processedTrace;
811
872
 
812
873
  // Compute the key frame timings for the main frame.
813
874
  const frameTimings = this.computeNavigationTimingsForFrame(frameEvents, {timeOriginEvt});
@@ -822,7 +883,11 @@ class TraceProcessor {
822
883
  }
823
884
 
824
885
  // Compute LCP for all frames.
825
- const lcpAllFramesEvt = this.computeValidLCPAllFrames(frameTreeEvents, timeOriginEvt).lcp;
886
+ const lcpAllFramesEvt = this.computeValidLCPAllFrames(
887
+ frameTreeEvents,
888
+ timeOriginEvt,
889
+ mainFrameInfo.frameId
890
+ ).lcp;
826
891
 
827
892
  /** @param {number} ts */
828
893
  const getTiming = ts => (ts - timeOriginEvt.ts) / 1000;
@@ -950,7 +1015,7 @@ class TraceProcessor {
950
1015
  * in addition to the standard microsecond monotonic timestamps.
951
1016
  * @param {Array<LH.TraceEvent>} frameEvents
952
1017
  * @param {{timeOriginEvt: LH.TraceEvent}} options
953
- */
1018
+ */
954
1019
  static computeNavigationTimingsForFrame(frameEvents, options) {
955
1020
  const {timeOriginEvt} = options;
956
1021
 
@@ -967,7 +1032,8 @@ class TraceProcessor {
967
1032
  }
968
1033
 
969
1034
  // This function accepts events spanning multiple frames, but this usage will only provide events from the main frame.
970
- const lcpResult = this.computeValidLCPAllFrames(frameEvents, timeOriginEvt);
1035
+ const frameId = frameEvents.map(e => TraceProcessor.getFrameId(e)).find(Boolean);
1036
+ const lcpResult = this.computeValidLCPAllFrames(frameEvents, timeOriginEvt, frameId);
971
1037
 
972
1038
  const load = frameEvents.find(e => e.name === 'loadEventEnd' && e.ts > timeOriginEvt.ts);
973
1039
  const domContentLoaded = frameEvents.find(
package/core/runner.js CHANGED
@@ -498,6 +498,7 @@ vs: ${JSON.stringify(normalizedAuditSettings[k], null, 2)}`);
498
498
  .map(f => `byte-efficiency/${f}`),
499
499
  ...fs.readdirSync(path.join(moduleDir, './audits/manual')).map(f => `manual/${f}`),
500
500
  ...fs.readdirSync(path.join(moduleDir, './audits/insights')).map(f => `insights/${f}`),
501
+ ...fs.readdirSync(path.join(moduleDir, './audits/agentic')).map(f => `agentic/${f}`),
501
502
  ];
502
503
  return fileList.filter(f => {
503
504
  return /\.js$/.test(f) && !ignoredFiles.includes(f);
@@ -514,6 +515,8 @@ vs: ${JSON.stringify(normalizedAuditSettings[k], null, 2)}`);
514
515
  ...fs.readdirSync(path.join(moduleDir, './gather/gatherers/seo')).map(f => `seo/${f}`),
515
516
  ...fs.readdirSync(path.join(moduleDir, './gather/gatherers/dobetterweb'))
516
517
  .map(f => `dobetterweb/${f}`),
518
+ ...fs.readdirSync(path.join(moduleDir, './gather/gatherers/agentic'))
519
+ .map(f => `agentic/${f}`),
517
520
  ];
518
521
  return fileList.filter(f => /\.js$/.test(f) && f !== 'gatherer.js').sort();
519
522
  }
package/core/scoring.d.ts CHANGED
@@ -750,6 +750,7 @@ export class ReportScoring {
750
750
  acronym?: string | import("./index.js").IcuMessage | undefined;
751
751
  }[];
752
752
  supportedModes?: import("./index.js").Result.GatherMode[] | undefined;
753
+ categoryScoreDisplayMode?: import("./index.js").Result.CategoryScoreDisplayMode | undefined;
753
754
  };
754
755
  };
755
756
  }