chrome-devtools-frontend 1.0.1595925 → 1.0.1596535

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 (129) hide show
  1. package/.stylelintrc.json +3 -1
  2. package/docs/ui_engineering.md +0 -36
  3. package/front_end/core/common/Console.ts +6 -6
  4. package/front_end/core/common/Settings.ts +12 -8
  5. package/front_end/core/host/UserMetrics.ts +0 -1
  6. package/front_end/core/root/DevToolsContext.ts +13 -7
  7. package/front_end/core/root/ExperimentNames.ts +0 -1
  8. package/front_end/core/sdk/CPUThrottlingManager.ts +12 -9
  9. package/front_end/core/sdk/DOMModel.ts +206 -0
  10. package/front_end/core/sdk/FrameManager.ts +7 -8
  11. package/front_end/core/sdk/SourceMapScopesInfo.ts +1 -1
  12. package/front_end/entrypoints/greendev_floaty/FloatyEntrypoint.ts +1 -1
  13. package/front_end/entrypoints/greendev_floaty/floaty.css +2 -2
  14. package/front_end/entrypoints/main/MainImpl.ts +1 -7
  15. package/front_end/foundation/Universe.ts +22 -11
  16. package/front_end/generated/InspectorBackendCommands.ts +7 -0
  17. package/front_end/generated/protocol-mapping.d.ts +16 -0
  18. package/front_end/generated/protocol-proxy-api.d.ts +25 -0
  19. package/front_end/generated/protocol.ts +71 -0
  20. package/front_end/models/ai_assistance/AiConversation.ts +6 -95
  21. package/front_end/models/ai_assistance/ConversationHandler.ts +1 -1
  22. package/front_end/models/ai_assistance/agents/AiAgent.ts +11 -1
  23. package/front_end/models/ai_assistance/agents/PerformanceAgent.ts +14 -0
  24. package/front_end/models/ai_code_completion/AiCodeCompletion.ts +1 -1
  25. package/front_end/panels/ai_assistance/AiAssistancePanel.ts +3 -0
  26. package/front_end/panels/ai_assistance/components/ChatMessage.ts +73 -35
  27. package/front_end/panels/ai_assistance/components/WalkthroughView.ts +30 -6
  28. package/front_end/panels/ai_assistance/components/chatMessage.css +4 -2
  29. package/front_end/panels/application/IndexedDBModel.ts +2 -4
  30. package/front_end/panels/application/ServiceWorkersView.ts +3 -11
  31. package/front_end/panels/developer_resources/DeveloperResourcesListView.ts +1 -1
  32. package/front_end/panels/elements/ComputedStyleWidget.ts +25 -16
  33. package/front_end/panels/elements/ElementsPanel.ts +0 -1
  34. package/front_end/panels/elements/StandaloneStylesContainer.ts +48 -12
  35. package/front_end/panels/elements/StylePropertiesSection.ts +97 -1
  36. package/front_end/panels/elements/StylePropertyTreeElement.ts +27 -5
  37. package/front_end/panels/elements/StylesContainer.ts +1 -0
  38. package/front_end/panels/elements/StylesSidebarPane.ts +48 -32
  39. package/front_end/panels/elements/nodeStackTraceWidget.css +1 -1
  40. package/front_end/panels/elements/stylePropertiesTreeOutline.css +1 -1
  41. package/front_end/panels/emulation/DeviceModeToolbar.ts +171 -89
  42. package/front_end/panels/greendev/GreenDevPanel.css +2 -2
  43. package/front_end/panels/issues/AffectedPermissionElementsView.ts +9 -6
  44. package/front_end/panels/lighthouse/LighthouseController.ts +13 -5
  45. package/front_end/panels/lighthouse/LighthousePanel.ts +22 -5
  46. package/front_end/panels/profiler/HeapSnapshotGridNodes.ts +11 -3
  47. package/front_end/panels/recorder/components/StepView.ts +0 -2
  48. package/front_end/panels/recorder/components/stepView.css +13 -13
  49. package/front_end/panels/recorder/components/timelineSection.css +6 -7
  50. package/front_end/panels/settings/components/SyncSection.ts +4 -13
  51. package/front_end/panels/timeline/CompatibilityTracksAppender.ts +2 -15
  52. package/front_end/panels/timeline/RecordingMetadata.ts +1 -1
  53. package/front_end/panels/timeline/TimelineDetailsView.ts +2 -2
  54. package/front_end/panels/timeline/TimelinePanel.ts +18 -0
  55. package/front_end/panels/timeline/TimelineUIUtils.ts +7 -4
  56. package/front_end/panels/timeline/components/CWVMetrics.ts +339 -0
  57. package/front_end/panels/timeline/components/Sidebar.ts +17 -0
  58. package/front_end/panels/timeline/components/SidebarInsightsTab.ts +10 -0
  59. package/front_end/panels/timeline/components/SidebarSingleInsightSet.ts +25 -267
  60. package/front_end/panels/timeline/components/Utils.ts +2 -2
  61. package/front_end/panels/timeline/components/components.ts +2 -0
  62. package/front_end/panels/timeline/components/cwvMetrics.css +107 -0
  63. package/front_end/panels/timeline/components/sidebarSingleInsightSet.css +0 -102
  64. package/front_end/panels/timeline/timeline-meta.ts +11 -0
  65. package/front_end/panels/timeline/utils/Helpers.ts +5 -1
  66. package/front_end/panels/webauthn/webauthnPane.css +1 -1
  67. package/front_end/third_party/chromium/README.chromium +1 -1
  68. package/front_end/third_party/puppeteer/README.chromium +2 -2
  69. package/front_end/third_party/puppeteer/package/lib/cjs/puppeteer/api/Page.d.ts +7 -0
  70. package/front_end/third_party/puppeteer/package/lib/cjs/puppeteer/api/Page.d.ts.map +1 -1
  71. package/front_end/third_party/puppeteer/package/lib/cjs/puppeteer/api/Page.js.map +1 -1
  72. package/front_end/third_party/puppeteer/package/lib/cjs/puppeteer/bidi/Page.d.ts +1 -0
  73. package/front_end/third_party/puppeteer/package/lib/cjs/puppeteer/bidi/Page.d.ts.map +1 -1
  74. package/front_end/third_party/puppeteer/package/lib/cjs/puppeteer/bidi/Page.js +3 -0
  75. package/front_end/third_party/puppeteer/package/lib/cjs/puppeteer/bidi/Page.js.map +1 -1
  76. package/front_end/third_party/puppeteer/package/lib/cjs/puppeteer/cdp/Browser.d.ts +1 -0
  77. package/front_end/third_party/puppeteer/package/lib/cjs/puppeteer/cdp/Browser.d.ts.map +1 -1
  78. package/front_end/third_party/puppeteer/package/lib/cjs/puppeteer/cdp/Browser.js +6 -0
  79. package/front_end/third_party/puppeteer/package/lib/cjs/puppeteer/cdp/Browser.js.map +1 -1
  80. package/front_end/third_party/puppeteer/package/lib/cjs/puppeteer/cdp/Page.d.ts +1 -0
  81. package/front_end/third_party/puppeteer/package/lib/cjs/puppeteer/cdp/Page.d.ts.map +1 -1
  82. package/front_end/third_party/puppeteer/package/lib/cjs/puppeteer/cdp/Page.js +5 -0
  83. package/front_end/third_party/puppeteer/package/lib/cjs/puppeteer/cdp/Page.js.map +1 -1
  84. package/front_end/third_party/puppeteer/package/lib/cjs/puppeteer/injected/injected.d.ts +1 -1
  85. package/front_end/third_party/puppeteer/package/lib/cjs/puppeteer/revisions.d.ts +2 -2
  86. package/front_end/third_party/puppeteer/package/lib/cjs/puppeteer/revisions.js +2 -2
  87. package/front_end/third_party/puppeteer/package/lib/cjs/puppeteer/util/Mutex.d.ts +2 -2
  88. package/front_end/third_party/puppeteer/package/lib/cjs/puppeteer/util/version.d.ts +1 -1
  89. package/front_end/third_party/puppeteer/package/lib/cjs/puppeteer/util/version.js +1 -1
  90. package/front_end/third_party/puppeteer/package/lib/es5-iife/puppeteer-core-browser.d.ts +7 -0
  91. package/front_end/third_party/puppeteer/package/lib/es5-iife/puppeteer-core-browser.js +14 -3
  92. package/front_end/third_party/puppeteer/package/lib/esm/puppeteer/api/Page.d.ts +7 -0
  93. package/front_end/third_party/puppeteer/package/lib/esm/puppeteer/api/Page.d.ts.map +1 -1
  94. package/front_end/third_party/puppeteer/package/lib/esm/puppeteer/api/Page.js.map +1 -1
  95. package/front_end/third_party/puppeteer/package/lib/esm/puppeteer/bidi/Page.d.ts +1 -0
  96. package/front_end/third_party/puppeteer/package/lib/esm/puppeteer/bidi/Page.d.ts.map +1 -1
  97. package/front_end/third_party/puppeteer/package/lib/esm/puppeteer/bidi/Page.js +3 -0
  98. package/front_end/third_party/puppeteer/package/lib/esm/puppeteer/bidi/Page.js.map +1 -1
  99. package/front_end/third_party/puppeteer/package/lib/esm/puppeteer/cdp/Browser.d.ts +1 -0
  100. package/front_end/third_party/puppeteer/package/lib/esm/puppeteer/cdp/Browser.d.ts.map +1 -1
  101. package/front_end/third_party/puppeteer/package/lib/esm/puppeteer/cdp/Browser.js +6 -0
  102. package/front_end/third_party/puppeteer/package/lib/esm/puppeteer/cdp/Browser.js.map +1 -1
  103. package/front_end/third_party/puppeteer/package/lib/esm/puppeteer/cdp/Page.d.ts +1 -0
  104. package/front_end/third_party/puppeteer/package/lib/esm/puppeteer/cdp/Page.d.ts.map +1 -1
  105. package/front_end/third_party/puppeteer/package/lib/esm/puppeteer/cdp/Page.js +5 -0
  106. package/front_end/third_party/puppeteer/package/lib/esm/puppeteer/cdp/Page.js.map +1 -1
  107. package/front_end/third_party/puppeteer/package/lib/esm/puppeteer/revisions.d.ts +2 -2
  108. package/front_end/third_party/puppeteer/package/lib/esm/puppeteer/revisions.js +2 -2
  109. package/front_end/third_party/puppeteer/package/lib/esm/puppeteer/util/version.d.ts +1 -1
  110. package/front_end/third_party/puppeteer/package/lib/esm/puppeteer/util/version.js +1 -1
  111. package/front_end/third_party/puppeteer/package/lib/types.d.ts +7 -0
  112. package/front_end/third_party/puppeteer/package/package.json +1 -1
  113. package/front_end/third_party/puppeteer/package/src/api/Page.ts +8 -0
  114. package/front_end/third_party/puppeteer/package/src/bidi/Page.ts +4 -0
  115. package/front_end/third_party/puppeteer/package/src/cdp/Browser.ts +7 -0
  116. package/front_end/third_party/puppeteer/package/src/cdp/Page.ts +6 -0
  117. package/front_end/third_party/puppeteer/package/src/revisions.ts +2 -2
  118. package/front_end/third_party/puppeteer/package/src/util/version.ts +1 -1
  119. package/front_end/ui/components/text_editor/AutocompleteHistory.ts +89 -5
  120. package/front_end/ui/components/tree_outline/TreeOutline.ts +1 -1
  121. package/front_end/ui/legacy/ProgressIndicator.ts +1 -1
  122. package/front_end/ui/legacy/Toolbar.ts +1 -1
  123. package/front_end/ui/legacy/components/object_ui/ObjectPropertiesSection.ts +14 -7
  124. package/front_end/ui/legacy/legacy.ts +0 -2
  125. package/front_end/ui/visual_logging/KnownContextValues.ts +5 -0
  126. package/inspector_overlay/loadCSS.rollup.js +2 -2
  127. package/inspector_overlay/tool_source_order.css +1 -0
  128. package/package.json +1 -1
  129. package/front_end/ui/legacy/Fragment.ts +0 -234
@@ -13,18 +13,18 @@
13
13
  .title-container {
14
14
  /* Vertically center items with min-width: 0; */
15
15
  min-width: 0;
16
- font-size: 13px;
17
- line-height: 16px;
18
- letter-spacing: 0.03em;
16
+ font-size: var(--sys-size-7);
19
17
  display: flex;
20
18
  flex-direction: row;
21
- gap: 3px;
22
- outline-offset: 3px;
19
+ gap: var(--sys-size-2);
20
+ outline-offset: var(--sys-size-2);
21
+ flex-grow: 1;
22
+ align-items: center;
23
23
  }
24
24
 
25
25
  .action {
26
26
  display: flex;
27
- align-items: flex-start;
27
+ align-items: center;
28
28
  }
29
29
 
30
30
  .title {
@@ -58,7 +58,7 @@
58
58
  cy: 8px;
59
59
  }
60
60
 
61
- .is-start-of-group .circle-icon {
61
+ .is-start-of-group:not(:first-of-type) .circle-icon {
62
62
  r: 7px;
63
63
  fill: var(--sys-color-cdt-base-container);
64
64
  stroke: var(--sys-color-primary);
@@ -143,7 +143,7 @@
143
143
  height: 14px;
144
144
  transition: 200ms;
145
145
  position: absolute;
146
- top: 18px;
146
+ top: 14px;
147
147
  left: 24px;
148
148
  transform: rotate(-90deg);
149
149
  color: var(--sys-color-on-surface);
@@ -183,7 +183,11 @@ devtools-recorder-step-editor:hover {
183
183
  }
184
184
 
185
185
  devtools-recorder-step-editor.is-selected {
186
- background-color: color-mix(in srgb, var(--sys-color-tonal-container), var(--sys-color-cdt-base-container) 50%);
186
+ background-color: color-mix(
187
+ in srgb,
188
+ var(--sys-color-tonal-container),
189
+ var(--sys-color-cdt-base-container) 50%
190
+ );
187
191
  border: 1px solid var(--sys-color-tonal-outline);
188
192
  }
189
193
 
@@ -192,10 +196,6 @@ devtools-recorder-step-editor.is-selected {
192
196
  flex-flow: row nowrap;
193
197
  }
194
198
 
195
- .filler {
196
- flex-grow: 1;
197
- }
198
-
199
199
  .subtitle {
200
200
  font-weight: normal;
201
201
  color: var(--sys-color-on-surface-subtle);
@@ -13,7 +13,7 @@
13
13
 
14
14
  .timeline-section {
15
15
  position: relative;
16
- padding: 16px 0 0 40px;
16
+ padding: 8px 0 8px 40px;
17
17
  margin-left: 8px;
18
18
 
19
19
  --override-color-recording-successful-text: #36a854;
@@ -52,12 +52,12 @@
52
52
  z-index: 4;
53
53
  }
54
54
 
55
- .is-start-of-group {
56
- padding-top: 28px;
55
+ .is-start-of-group:not(:first-of-type) {
56
+ padding-top: 16px;
57
57
  }
58
58
 
59
59
  .is-end-of-group {
60
- padding-bottom: 24px;
60
+ padding-bottom: 16px;
61
61
  }
62
62
 
63
63
  .icon {
@@ -73,7 +73,7 @@
73
73
  display: block;
74
74
  transform: translateX(-50%);
75
75
  top: 18px;
76
- height: calc(100% + 8px);
76
+ height: 100%;
77
77
  z-index: 1; /* We want this to be below of `.overlay` for stopped case */
78
78
  }
79
79
 
@@ -86,8 +86,7 @@
86
86
  }
87
87
 
88
88
  .is-first-section .bar {
89
- top: 32px;
90
- height: calc(100% - 8px);
89
+ height: 100%;
91
90
  display: none;
92
91
  }
93
92
 
@@ -10,10 +10,10 @@ import * as Common from '../../../core/common/common.js';
10
10
  import * as Host from '../../../core/host/host.js';
11
11
  import * as i18n from '../../../core/i18n/i18n.js';
12
12
  import type * as Platform from '../../../core/platform/platform.js';
13
- import * as SDK from '../../../core/sdk/sdk.js';
14
13
  import * as Badges from '../../../models/badges/badges.js';
15
14
  import * as Buttons from '../../../ui/components/buttons/buttons.js';
16
15
  import type * as SettingsComponents from '../../../ui/components/settings/settings.js';
16
+ import * as UIHelpers from '../../../ui/helpers/helpers.js';
17
17
  import * as UI from '../../../ui/legacy/legacy.js';
18
18
  import * as Lit from '../../../ui/lit/lit.js';
19
19
  import * as VisualLogging from '../../../ui/visual_logging/visual_logging.js';
@@ -343,19 +343,10 @@ export class SyncSection extends UI.Widget.Widget {
343
343
  }
344
344
 
345
345
  #onWarningClick(event: Event): void {
346
- const rootTarget = SDK.TargetManager.TargetManager.instance().rootTarget();
347
- if (rootTarget === null) {
348
- return;
349
- }
350
346
  // TODO: investigate if /advance link is alive
351
- const warningLink = !this.#syncInfo.isSyncActive ?
352
- ('chrome://settings/syncSetup' as Platform.DevToolsPath.UrlString) :
353
- ('chrome://settings/syncSetup/advanced' as Platform.DevToolsPath.UrlString);
354
- void rootTarget.targetAgent().invoke_createTarget({url: warningLink}).then(result => {
355
- if (result.getError()) {
356
- Host.InspectorFrontendHost.InspectorFrontendHostInstance.openInNewTab(warningLink);
357
- }
358
- });
347
+ const warningLink =
348
+ this.#syncInfo.isSyncActive ? 'chrome://settings/syncSetup/advanced' : 'chrome://settings/syncSetup';
349
+ UIHelpers.openInNewTab(warningLink);
359
350
  event.consume();
360
351
  }
361
352
 
@@ -36,17 +36,6 @@ export interface PopoverInfo {
36
36
  additionalElements: HTMLElement[];
37
37
  }
38
38
 
39
- let showPostMessageEvents: boolean|undefined;
40
- function isShowPostMessageEventsEnabled(): boolean {
41
- // Everytime the experiment is toggled devtools is reloaded so the
42
- // cache is updated automatically.
43
- if (showPostMessageEvents === undefined) {
44
- showPostMessageEvents =
45
- Root.Runtime.experiments.isEnabled(Root.ExperimentNames.ExperimentName.TIMELINE_SHOW_POST_MESSAGE_EVENTS);
46
- }
47
- return showPostMessageEvents;
48
- }
49
-
50
39
  export function entryIsVisibleInTimeline(
51
40
  entry: Trace.Types.Events.Event, parsedTrace?: Trace.TraceModel.ParsedTrace): boolean {
52
41
  if (parsedTrace?.data.Meta.traceIsGeneric) {
@@ -64,10 +53,8 @@ export function entryIsVisibleInTimeline(
64
53
  return true;
65
54
  }
66
55
 
67
- if (isShowPostMessageEventsEnabled()) {
68
- if (Trace.Types.Events.isSchedulePostMessage(entry) || Trace.Types.Events.isHandlePostMessage(entry)) {
69
- return true;
70
- }
56
+ if (Trace.Types.Events.isSchedulePostMessage(entry) || Trace.Types.Events.isHandlePostMessage(entry)) {
57
+ return true;
71
58
  }
72
59
 
73
60
  if (Trace.Types.Extensions.isSyntheticExtensionEntry(entry)) {
@@ -39,7 +39,7 @@ async function innerForTraceCalculate({recordingStartTime, cruxFieldData}: DataF
39
39
  const deviceModeModel = EmulationModel.DeviceModeModel.DeviceModeModel.tryInstance();
40
40
  let emulatedDeviceTitle;
41
41
  if (deviceModeModel?.type() === EmulationModel.DeviceModeModel.Type.Device) {
42
- emulatedDeviceTitle = deviceModeModel.device()?.title ?? undefined;
42
+ emulatedDeviceTitle = deviceModeModel.device()?.title;
43
43
  } else if (deviceModeModel?.type() === EmulationModel.DeviceModeModel.Type.Responsive) {
44
44
  emulatedDeviceTitle = 'Responsive';
45
45
  }
@@ -149,7 +149,7 @@ export class TimelineDetailsPane extends
149
149
  this.#thirdPartyTree.addEventListener(TimelineTreeView.Events.TREE_ROW_HOVERED, node => {
150
150
  // Re-dispatch through 3P event to get 3P dimmer.
151
151
  this.dispatchEventToListeners(
152
- TimelineTreeView.Events.TREE_ROW_HOVERED, {node: node.data.node, events: node.data.events ?? undefined});
152
+ TimelineTreeView.Events.TREE_ROW_HOVERED, {node: node.data.node, events: node.data.events});
153
153
  });
154
154
 
155
155
  this.#thirdPartyTree.addEventListener(TimelineTreeView.Events.BOTTOM_UP_BUTTON_CLICKED, node => {
@@ -158,7 +158,7 @@ export class TimelineDetailsPane extends
158
158
  this.#thirdPartyTree.addEventListener(TimelineTreeView.Events.TREE_ROW_CLICKED, node => {
159
159
  // Re-dispatch through 3P event to get 3P dimmer.
160
160
  this.dispatchEventToListeners(
161
- TimelineTreeView.Events.TREE_ROW_CLICKED, {node: node.data.node, events: node.data.events ?? undefined});
161
+ TimelineTreeView.Events.TREE_ROW_CLICKED, {node: node.data.node, events: node.data.events});
162
162
  });
163
163
 
164
164
  this.tabbedPane.addEventListener(UI.TabbedPane.Events.TabSelected, this.tabSelected, this);
@@ -3054,6 +3054,17 @@ export class TimelinePanel extends Common.ObjectWrapper.eventMixin<EventTypes, t
3054
3054
  this.#setActiveInsight({model: insightModel, insightSetKey}, {highlightInsight: true});
3055
3055
  }
3056
3056
 
3057
+ revealCoreVitals(revealable: Utils.Helpers.RevealableCoreVitals): void {
3058
+ if (this.#splitWidget.showMode() !== UI.SplitWidget.ShowMode.BOTH) {
3059
+ this.#splitWidget.showBoth();
3060
+ }
3061
+ this.#sideBar.openInsightsTab();
3062
+ if (revealable.insightSetKey) {
3063
+ this.#sideBar.setActiveInsightSet(revealable.insightSetKey);
3064
+ this.#setActiveInsight(null);
3065
+ }
3066
+ }
3067
+
3057
3068
  static async executeRecordAndReload(): Promise<Trace.TraceModel.ParsedTrace> {
3058
3069
  await UI.ViewManager.ViewManager.instance().showView('timeline');
3059
3070
  const panelInstance = TimelinePanel.instance();
@@ -3238,6 +3249,13 @@ export class InsightRevealer implements Common.Revealer.Revealer<Utils.Helpers.R
3238
3249
  }
3239
3250
  }
3240
3251
 
3252
+ export class CoreVitalsRevealer implements Common.Revealer.Revealer<Utils.Helpers.RevealableCoreVitals> {
3253
+ async reveal(revealable: Utils.Helpers.RevealableCoreVitals): Promise<void> {
3254
+ await UI.ViewManager.ViewManager.instance().showView('timeline');
3255
+ TimelinePanel.instance().revealCoreVitals(revealable);
3256
+ }
3257
+ }
3258
+
3241
3259
  export class ActionDelegate implements UI.ActionRegistration.ActionDelegate {
3242
3260
  handleAction(context: UI.Context.Context, actionId: string): boolean {
3243
3261
  const panel = context.flavor(TimelinePanel);
@@ -34,6 +34,8 @@
34
34
  * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
35
35
  */
36
36
 
37
+ import '../../ui/kit/kit.js';
38
+
37
39
  import * as Common from '../../core/common/common.js';
38
40
  import * as i18n from '../../core/i18n/i18n.js';
39
41
  import * as Platform from '../../core/platform/platform.js';
@@ -50,13 +52,13 @@ import * as CodeHighlighter from '../../ui/components/code_highlighter/code_high
50
52
  // eslint-disable-next-line @devtools/es-modules-import
51
53
  import codeHighlighterStyles from '../../ui/components/code_highlighter/codeHighlighter.css.js';
52
54
  import * as uiI18n from '../../ui/i18n/i18n.js';
53
- import {Link} from '../../ui/kit/kit.js';
54
55
  import * as PerfUI from '../../ui/legacy/components/perf_ui/perf_ui.js';
55
56
  // eslint-disable-next-line @devtools/es-modules-import
56
57
  import imagePreviewStyles from '../../ui/legacy/components/utils/imagePreview.css.js';
57
58
  import * as LegacyComponents from '../../ui/legacy/components/utils/utils.js';
58
59
  import * as UI from '../../ui/legacy/legacy.js';
59
60
  import * as ThemeSupport from '../../ui/legacy/theme_support/theme_support.js';
61
+ import {html, render} from '../../ui/lit/lit.js';
60
62
  import * as PanelsCommon from '../common/common.js';
61
63
 
62
64
  import {getDurationString} from './AppenderUtils.js';
@@ -833,9 +835,10 @@ export class TimelineUIUtils {
833
835
  break;
834
836
  }
835
837
 
836
- const html = UI.Fragment.html`<div>${
837
- Link.create(link, i18nString(UIStrings.learnMore), undefined, 'learn-more')} about ${name}.</div>`;
838
- return html as HTMLElement;
838
+ const div = document.createElement('div');
839
+ // eslint-disable-next-line @devtools/no-lit-render-outside-of-view
840
+ render(html`<devtools-link href=${link}>${i18nString(UIStrings.learnMore)}</devtools-link> about ${name}.`, div);
841
+ return div;
839
842
  }
840
843
 
841
844
  static buildConsumeCacheDetails(
@@ -0,0 +1,339 @@
1
+ // Copyright 2026 The Chromium Authors
2
+ // Use of this source code is governed by a BSD-style license that can be
3
+ // found in the LICENSE file.
4
+
5
+ import * as i18n from '../../../core/i18n/i18n.js';
6
+ import * as Platform from '../../../core/platform/platform.js';
7
+ import * as CrUXManager from '../../../models/crux-manager/crux-manager.js';
8
+ import * as Trace from '../../../models/trace/trace.js';
9
+ import * as Buttons from '../../../ui/components/buttons/buttons.js';
10
+ import * as UI from '../../../ui/legacy/legacy.js';
11
+ import * as Lit from '../../../ui/lit/lit.js';
12
+ import * as VisualLogging from '../../../ui/visual_logging/visual_logging.js';
13
+
14
+ import cwvMetricsStyles from './cwvMetrics.css.js';
15
+ import {md} from './insights/Helpers.js';
16
+ import * as Insights from './insights/insights.js';
17
+ import {isFieldWorseThanLocal, NumberWithUnit} from './Utils.js';
18
+
19
+ const {html} = Lit.StaticHtml;
20
+
21
+ const UIStrings = {
22
+ /**
23
+ * @description title used for a metric value to tell the user about its score classification
24
+ * @example {INP} PH1
25
+ * @example {1.2s} PH2
26
+ * @example {poor} PH3
27
+ */
28
+ metricScore: '{PH1}: {PH2} {PH3} score',
29
+ /**
30
+ * @description title used for a metric value to tell the user that the data is unavailable
31
+ * @example {INP} PH1
32
+ */
33
+ metricScoreUnavailable: '{PH1}: unavailable',
34
+ /**
35
+ * @description Label denoting that metrics were observed in the field, from real use data (CrUX). Also denotes if from URL or Origin dataset.
36
+ * @example {URL} PH1
37
+ */
38
+ fieldScoreLabel: 'Field ({PH1})',
39
+ /**
40
+ * @description Label for an option that selects the page's specific URL as opposed to it's entire origin/domain.
41
+ */
42
+ urlOption: 'URL',
43
+ /**
44
+ * @description Label for an option that selects the page's entire origin/domain as opposed to it's specific URL.
45
+ */
46
+ originOption: 'Origin',
47
+ /**
48
+ * @description Title for button that closes a warning popup.
49
+ */
50
+ dismissTitle: 'Dismiss',
51
+ /**
52
+ * @description Title shown in a warning dialog when field metrics (collected from real users) is worse than the locally observed metrics.
53
+ */
54
+ fieldMismatchTitle: 'Field & local metrics mismatch',
55
+ /**
56
+ * @description Text shown in a warning dialog when field metrics (collected from real users) is worse than the locally observed metrics.
57
+ * Asks user to use features such as throttling and device emulation.
58
+ */
59
+ fieldMismatchNotice:
60
+ 'There are many reasons why local and field metrics [may not match](https://web.dev/articles/lab-and-field-data-differences). ' +
61
+ 'Adjust [throttling settings and device emulation](https://developer.chrome.com/docs/devtools/device-mode) to analyze traces more similar to the average user\'s environment.',
62
+ } as const;
63
+
64
+ const str_ = i18n.i18n.registerUIStrings('panels/timeline/components/CWVMetrics.ts', UIStrings);
65
+ const i18nString = i18n.i18n.getLocalizedString.bind(undefined, str_);
66
+
67
+ interface LocalMetrics {
68
+ lcp: {value: Trace.Types.Timing.Micro, event: Trace.Types.Events.AnyLargestContentfulPaintCandidate}|null;
69
+ cls: {value: number, worstClusterEvent: Trace.Types.Events.Event|null};
70
+ inp: {value: Trace.Types.Timing.Micro, event: Trace.Types.Events.SyntheticInteractionPair}|null;
71
+ }
72
+
73
+ function getLocalMetrics(parsedTrace: Trace.TraceModel.ParsedTrace|null, insightSetKey: string|null): LocalMetrics|
74
+ null {
75
+ if (!parsedTrace || !insightSetKey) {
76
+ return null;
77
+ }
78
+
79
+ const insightSet = parsedTrace.insights?.get(insightSetKey);
80
+ if (!insightSet) {
81
+ return null;
82
+ }
83
+
84
+ const lcp = Trace.Insights.Common.getLCP(insightSet);
85
+ const cls = Trace.Insights.Common.getCLS(insightSet);
86
+ const inp = Trace.Insights.Common.getINP(insightSet);
87
+
88
+ return {lcp, cls, inp};
89
+ }
90
+
91
+ export function getFieldMetrics(parsedTrace: Trace.TraceModel.ParsedTrace|null, insightSetKey: string|null):
92
+ Trace.Insights.Common.CrUXFieldMetricResults|null {
93
+ if (!parsedTrace || !parsedTrace.metadata?.cruxFieldData || !insightSetKey) {
94
+ return null;
95
+ }
96
+
97
+ const insightSet = parsedTrace.insights?.get(insightSetKey);
98
+ if (!insightSet) {
99
+ return null;
100
+ }
101
+
102
+ let scope: CrUXManager.Scope|null = null;
103
+ try {
104
+ scope = CrUXManager.CrUXManager.instance().getSelectedScope();
105
+ } catch {
106
+ // test environment
107
+ }
108
+
109
+ const fieldMetricsResults =
110
+ Trace.Insights.Common.getFieldMetricsForInsightSet(insightSet, parsedTrace.metadata, scope);
111
+ if (!fieldMetricsResults) {
112
+ return null;
113
+ }
114
+
115
+ return fieldMetricsResults;
116
+ }
117
+
118
+ interface MetricsViewInput {
119
+ parsedTrace: Trace.TraceModel.ParsedTrace|null;
120
+ insightSetKey: string|null;
121
+ didDismissFieldMismatchNotice: boolean;
122
+ onDismisFieldMismatchNotice: () => void;
123
+ onClickMetric: (traceEvent: Trace.Types.Events.Event) => void;
124
+ }
125
+
126
+ type MetricsView = (input: MetricsViewInput, output: undefined, target: HTMLElement) => void;
127
+
128
+ const CWV_METRICS_VIEW: MetricsView = (input, _output, target) => {
129
+ const {
130
+ parsedTrace,
131
+ insightSetKey,
132
+ didDismissFieldMismatchNotice,
133
+ onDismisFieldMismatchNotice,
134
+ onClickMetric,
135
+ } = input;
136
+
137
+ const local = getLocalMetrics(parsedTrace, insightSetKey);
138
+ const field = getFieldMetrics(parsedTrace, insightSetKey);
139
+
140
+ const localValues = {
141
+ lcp: local?.lcp?.value !== undefined ? Trace.Helpers.Timing.microToMilli(local?.lcp.value) : undefined,
142
+ inp: local?.inp?.value !== undefined ? Trace.Helpers.Timing.microToMilli(local?.inp.value) : undefined,
143
+ };
144
+ const fieldValues = field && {
145
+ lcp: field.lcp?.value !== undefined ? Trace.Helpers.Timing.microToMilli(field.lcp.value) : undefined,
146
+ inp: field.inp?.value !== undefined ? Trace.Helpers.Timing.microToMilli(field.inp.value) : undefined,
147
+ };
148
+ const showFieldMismatchNotice =
149
+ !didDismissFieldMismatchNotice && !!fieldValues && isFieldWorseThanLocal(localValues, fieldValues);
150
+
151
+ function renderMetricValue(
152
+ metric: 'LCP'|'CLS'|'INP', value: number|null, relevantEvent: Trace.Types.Events.Event|null): Lit.LitTemplate {
153
+ let valueText: string;
154
+ let valueDisplay: HTMLElement|string;
155
+ let classification;
156
+ if (value === null) {
157
+ valueText = valueDisplay = '-';
158
+ classification = Trace.Handlers.ModelHandlers.PageLoadMetrics.ScoreClassification.UNCLASSIFIED;
159
+ } else if (metric === 'LCP') {
160
+ const micros = value as Trace.Types.Timing.Micro;
161
+ const {text, element} = NumberWithUnit.formatMicroSecondsAsSeconds(micros);
162
+ valueText = text;
163
+ valueDisplay = element;
164
+ classification =
165
+ Trace.Handlers.ModelHandlers.PageLoadMetrics.scoreClassificationForLargestContentfulPaint(micros);
166
+ } else if (metric === 'CLS') {
167
+ valueText = valueDisplay = value ? value.toFixed(2) : '0';
168
+ classification = Trace.Handlers.ModelHandlers.LayoutShifts.scoreClassificationForLayoutShift(value);
169
+ } else if (metric === 'INP') {
170
+ const micros = value as Trace.Types.Timing.Micro;
171
+ const {text, element} = NumberWithUnit.formatMicroSecondsAsMillisFixed(micros);
172
+ valueText = text;
173
+ valueDisplay = element;
174
+ classification =
175
+ Trace.Handlers.ModelHandlers.UserInteractions.scoreClassificationForInteractionToNextPaint(micros);
176
+ } else {
177
+ Platform.TypeScriptUtilities.assertNever(metric, `Unexpected metric ${metric}`);
178
+ }
179
+
180
+ // NOTE: it is deliberate to use the same value for the title and
181
+ // aria-label; the aria-label is used to give more context to
182
+ // screen-readers, and the title is to aid users who may not know what
183
+ // the red/orange/green classification is, or those who are unable to
184
+ // easily distinguish the visual colour differences.
185
+ // clang-format off
186
+ const title = value !== null ?
187
+ i18nString(UIStrings.metricScore, {PH1: metric, PH2: valueText, PH3: classification}) :
188
+ i18nString(UIStrings.metricScoreUnavailable, {PH1: metric});
189
+
190
+ return html`
191
+ <button class="metric"
192
+ @click=${relevantEvent ? onClickMetric.bind(relevantEvent) : null}
193
+ title=${title}
194
+ aria-label=${title}
195
+ >
196
+ <div class="metric-value metric-value-${classification}">${valueDisplay}</div>
197
+ </button>
198
+ `;
199
+ // clang-format on
200
+ }
201
+
202
+ const lcpEl = renderMetricValue('LCP', local?.lcp?.value ?? null, local?.lcp?.event ?? null);
203
+ const inpEl = renderMetricValue('INP', local?.inp?.value ?? null, local?.inp?.event ?? null);
204
+ const clsEl = renderMetricValue('CLS', local?.cls?.value ?? null, local?.cls?.worstClusterEvent ?? null);
205
+
206
+ const localMetricsTemplateResult = html`
207
+ <div class="metrics-row">
208
+ <span>${lcpEl}</span>
209
+ <span>${inpEl}</span>
210
+ <span>${clsEl}</span>
211
+ <span class="row-label">Local</span>
212
+ </div>
213
+ <span class="row-border"></span>
214
+ `;
215
+
216
+ let fieldMetricsTemplateResult;
217
+ if (field) {
218
+ const {lcp, inp, cls} = field;
219
+
220
+ const lcpEl = renderMetricValue('LCP', lcp?.value ?? null, null);
221
+ const inpEl = renderMetricValue('INP', inp?.value ?? null, null);
222
+ const clsEl = renderMetricValue('CLS', cls?.value ?? null, null);
223
+
224
+ let scope = i18nString(UIStrings.originOption);
225
+ if (lcp?.pageScope === 'url' || inp?.pageScope === 'url') {
226
+ scope = i18nString(UIStrings.urlOption);
227
+ }
228
+
229
+ // clang-format off
230
+ fieldMetricsTemplateResult = html`
231
+ <div class="metrics-row">
232
+ <span>${lcpEl}</span>
233
+ <span>${inpEl}</span>
234
+ <span>${clsEl}</span>
235
+ <span class="row-label">${i18nString(UIStrings.fieldScoreLabel, {PH1: scope})}</span>
236
+ </div>
237
+ <span class="row-border"></span>
238
+ `;
239
+ // clang-format on
240
+ }
241
+
242
+ let fieldIsDifferentEl;
243
+ if (showFieldMismatchNotice) {
244
+ // clang-format off
245
+ fieldIsDifferentEl = html`
246
+ <div class="field-mismatch-notice" jslog=${VisualLogging.section('timeline.insights.field-mismatch')}>
247
+ <h3>${i18nString(UIStrings.fieldMismatchTitle)}</h3>
248
+ <devtools-button
249
+ title=${i18nString(UIStrings.dismissTitle)}
250
+ .iconName=${'cross'}
251
+ .variant=${Buttons.Button.Variant.ICON}
252
+ .jslogContext=${'timeline.insights.dismiss-field-mismatch'}
253
+ @click=${onDismisFieldMismatchNotice}
254
+ ></devtools-button>
255
+ <div class="field-mismatch-notice__body">${md(i18nString(UIStrings.fieldMismatchNotice))}</div>
256
+ </div>
257
+ `;
258
+ // clang-format on
259
+ }
260
+
261
+ const classes = {metrics: true, 'metrics--field': Boolean(fieldMetricsTemplateResult)};
262
+ const metricsTableEl = html`<div class=${Lit.Directives.classMap(classes)}>
263
+ <div class="metrics-row">
264
+ <span class="metric-label">LCP</span>
265
+ <span class="metric-label">INP</span>
266
+ <span class="metric-label">CLS</span>
267
+ <span class="row-label"></span>
268
+ </div>
269
+ ${localMetricsTemplateResult}
270
+ ${fieldMetricsTemplateResult}
271
+ </div>`;
272
+
273
+ Lit.render(
274
+ html`
275
+ <style>${cwvMetricsStyles}</style>
276
+ ${metricsTableEl}
277
+ ${fieldIsDifferentEl}
278
+ `,
279
+ target);
280
+ };
281
+
282
+ export interface CWVMetricsData {
283
+ insightSetKey: Trace.Types.Events.NavigationId|null;
284
+ parsedTrace: Trace.TraceModel.ParsedTrace|null;
285
+ }
286
+
287
+ export class CWVMetrics extends UI.Widget.Widget {
288
+ #view: MetricsView;
289
+ #data: CWVMetricsData = {
290
+ insightSetKey: null,
291
+ parsedTrace: null,
292
+ };
293
+
294
+ #didDismissFieldMismatchNotice = false;
295
+
296
+ constructor(element?: HTMLElement, view: MetricsView = CWV_METRICS_VIEW) {
297
+ super(element, {useShadowDom: true});
298
+ this.#view = view;
299
+ }
300
+
301
+ set data(data: CWVMetricsData) {
302
+ this.#data = data;
303
+ this.requestUpdate();
304
+ }
305
+
306
+ #onClickMetric(traceEvent: Trace.Types.Events.Event): void {
307
+ this.element.dispatchEvent(new Insights.EventRef.EventReferenceClick(traceEvent));
308
+ }
309
+
310
+ #onDismisFieldMismatchNotice(): void {
311
+ this.#didDismissFieldMismatchNotice = true;
312
+ this.requestUpdate();
313
+ }
314
+
315
+ override performUpdate(): void {
316
+ const {
317
+ parsedTrace,
318
+ insightSetKey,
319
+ } = this.#data;
320
+
321
+ if (!parsedTrace?.insights || !insightSetKey || !(parsedTrace.insights instanceof Map)) {
322
+ return;
323
+ }
324
+
325
+ const insightSet = parsedTrace.insights.get(insightSetKey);
326
+ if (!insightSet) {
327
+ return;
328
+ }
329
+
330
+ const input: MetricsViewInput = {
331
+ parsedTrace,
332
+ insightSetKey,
333
+ didDismissFieldMismatchNotice: this.#didDismissFieldMismatchNotice,
334
+ onDismisFieldMismatchNotice: this.#onDismisFieldMismatchNotice.bind(this),
335
+ onClickMetric: this.#onClickMetric.bind(this),
336
+ };
337
+ this.#view(input, undefined, this.contentElement);
338
+ }
339
+ }
@@ -163,6 +163,14 @@ export class SidebarWidget extends UI.Widget.VBox {
163
163
  }
164
164
  }
165
165
 
166
+ openInsightsTab(): void {
167
+ this.#tabbedPane.selectTab(SidebarTabs.INSIGHTS);
168
+ }
169
+
170
+ setActiveInsightSet(insightSetKey: string): void {
171
+ this.#insightsView.setActiveInsightSet(insightSetKey);
172
+ }
173
+
166
174
  /**
167
175
  * True if the sidebar has been visible at least one time. This is persisted
168
176
  * to the user settings so it persists across sessions. This is used because
@@ -213,6 +221,15 @@ class InsightsView extends UI.Widget.VBox {
213
221
  });
214
222
  }
215
223
  }
224
+
225
+ setActiveInsightSet(insightSetKey: string): void {
226
+ const widget = this.#component.getWidget();
227
+ if (!widget) {
228
+ return;
229
+ }
230
+
231
+ widget.setActiveInsightSet(insightSetKey);
232
+ }
216
233
  }
217
234
 
218
235
  class AnnotationsView extends UI.Widget.VBox {
@@ -200,6 +200,16 @@ export class SidebarInsightsTab extends UI.Widget.Widget {
200
200
  this.requestUpdate();
201
201
  }
202
202
 
203
+ setActiveInsightSet(insightSetKey: string): void {
204
+ if (this.#parsedTrace?.insights) {
205
+ const insightSet = this.#parsedTrace.insights.get(insightSetKey);
206
+ if (insightSet) {
207
+ this.#selectedInsightSet = insightSet;
208
+ this.requestUpdate();
209
+ }
210
+ }
211
+ }
212
+
203
213
  #onInsightSetToggled(insightSet: Trace.Insights.Types.InsightSet): void {
204
214
  this.#selectedInsightSet = this.#selectedInsightSet === insightSet ? null : insightSet;
205
215
  // Update the active insight set.