chrome-devtools-mcp 0.2.7 → 0.4.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 (44) hide show
  1. package/README.md +30 -4
  2. package/build/node_modules/chrome-devtools-frontend/front_end/core/host/GdpClient.js +30 -5
  3. package/build/node_modules/chrome-devtools-frontend/front_end/core/host/UserMetrics.js +3 -2
  4. package/build/node_modules/chrome-devtools-frontend/front_end/core/i18n/i18n.js +24 -0
  5. package/build/node_modules/chrome-devtools-frontend/front_end/core/sdk/CSSMatchedStyles.js +5 -1
  6. package/build/node_modules/chrome-devtools-frontend/front_end/core/sdk/EnhancedTracesParser.js +4 -5
  7. package/build/node_modules/chrome-devtools-frontend/front_end/core/sdk/NetworkManager.js +1 -0
  8. package/build/node_modules/chrome-devtools-frontend/front_end/core/sdk/NetworkRequest.js +8 -0
  9. package/build/node_modules/chrome-devtools-frontend/front_end/core/sdk/TargetManager.js +4 -0
  10. package/build/node_modules/chrome-devtools-frontend/front_end/generated/InspectorBackendCommands.js +10 -7
  11. package/build/node_modules/chrome-devtools-frontend/front_end/generated/SupportedCSSProperties.js +40 -11
  12. package/build/node_modules/chrome-devtools-frontend/front_end/models/ai_assistance/data_formatters/PerformanceInsightFormatter.js +86 -283
  13. package/build/node_modules/chrome-devtools-frontend/front_end/models/ai_assistance/data_formatters/PerformanceTraceFormatter.js +267 -20
  14. package/build/node_modules/chrome-devtools-frontend/front_end/models/ai_assistance/performance/AICallTree.js +12 -6
  15. package/build/node_modules/chrome-devtools-frontend/front_end/models/ai_assistance/performance/AIContext.js +64 -7
  16. package/build/node_modules/chrome-devtools-frontend/front_end/models/trace/EventsSerializer.js +3 -3
  17. package/build/node_modules/chrome-devtools-frontend/front_end/models/trace/handlers/UserInteractionsHandler.js +91 -63
  18. package/build/node_modules/chrome-devtools-frontend/front_end/models/trace/handlers/UserTimingsHandler.js +1 -1
  19. package/build/node_modules/chrome-devtools-frontend/front_end/models/trace/helpers/Timing.js +1 -1
  20. package/build/node_modules/chrome-devtools-frontend/front_end/models/trace/helpers/Trace.js +98 -36
  21. package/build/node_modules/chrome-devtools-frontend/front_end/models/trace/types/TraceEvents.js +9 -0
  22. package/build/node_modules/chrome-devtools-frontend/front_end/third_party/legacy-javascript/legacy-javascript.js +2 -38
  23. package/build/node_modules/chrome-devtools-frontend/front_end/third_party/legacy-javascript/lib/legacy-javascript.js +1 -5
  24. package/build/node_modules/chrome-devtools-frontend/front_end/third_party/third-party-web/lib/nostats-subset.js +1 -3
  25. package/build/node_modules/chrome-devtools-frontend/front_end/third_party/third-party-web/third-party-web.js +2 -8
  26. package/build/src/McpContext.js +8 -2
  27. package/build/src/McpResponse.js +47 -8
  28. package/build/src/browser.js +2 -2
  29. package/build/src/cli.js +81 -0
  30. package/build/src/formatters/consoleFormatter.js +3 -1
  31. package/build/src/index.js +3 -3
  32. package/build/src/main.js +12 -85
  33. package/build/src/tools/ToolDefinition.js +1 -0
  34. package/build/src/tools/emulation.js +2 -2
  35. package/build/src/tools/input.js +8 -8
  36. package/build/src/tools/network.js +46 -4
  37. package/build/src/tools/pages.js +12 -2
  38. package/build/src/tools/performance.js +4 -4
  39. package/build/src/tools/screenshot.js +1 -1
  40. package/build/src/tools/script.js +4 -7
  41. package/build/src/tools/snapshot.js +2 -2
  42. package/build/src/trace-processing/parse.js +14 -7
  43. package/build/src/utils/pagination.js +49 -0
  44. package/package.json +9 -6
@@ -3,8 +3,8 @@
3
3
  // found in the LICENSE file.
4
4
  import * as Common from '../../../core/common/common.js';
5
5
  import * as Trace from '../../trace/trace.js';
6
- import { NetworkRequestFormatter, } from './NetworkRequestFormatter.js';
7
- import { bytes, micros, millis } from './UnitFormatters.js';
6
+ import { PerformanceTraceFormatter } from './PerformanceTraceFormatter.js';
7
+ import { bytes, millis } from './UnitFormatters.js';
8
8
  /**
9
9
  * For a given frame ID and navigation ID, returns the LCP Event and the LCP Request, if the resource was an image.
10
10
  */
@@ -28,11 +28,13 @@ function getLCPData(parsedTrace, frameId, navigationId) {
28
28
  };
29
29
  }
30
30
  export class PerformanceInsightFormatter {
31
+ #traceFormatter;
31
32
  #insight;
32
33
  #parsedTrace;
33
- constructor(parsedTrace, insight) {
34
+ constructor(focus, insight) {
35
+ this.#traceFormatter = new PerformanceTraceFormatter(focus);
34
36
  this.#insight = insight;
35
- this.#parsedTrace = parsedTrace;
37
+ this.#parsedTrace = focus.parsedTrace;
36
38
  }
37
39
  #formatMilli(x) {
38
40
  if (x === undefined) {
@@ -46,6 +48,22 @@ export class PerformanceInsightFormatter {
46
48
  }
47
49
  return this.#formatMilli(Trace.Helpers.Timing.microToMilli(x));
48
50
  }
51
+ #formatRequestUrl(request) {
52
+ return `${request.args.data.url} ${this.#traceFormatter.serializeEvent(request)}`;
53
+ }
54
+ #formatScriptUrl(script) {
55
+ if (script.request) {
56
+ return this.#formatRequestUrl(script.request);
57
+ }
58
+ return script.url ?? script.sourceUrl ?? script.scriptId;
59
+ }
60
+ #formatUrl(url) {
61
+ const request = this.#parsedTrace.data.NetworkRequests.byTime.find(request => request.args.data.url === url);
62
+ if (request) {
63
+ return this.#formatRequestUrl(request);
64
+ }
65
+ return url;
66
+ }
49
67
  /**
50
68
  * Information about LCP which we pass to the LLM for all insights that relate to LCP.
51
69
  */
@@ -62,13 +80,15 @@ export class PerformanceInsightFormatter {
62
80
  return '';
63
81
  }
64
82
  const { metricScore, lcpRequest, lcpEvent } = data;
65
- const theLcpElement = lcpEvent.args.data?.nodeName ? `The LCP element (${lcpEvent.args.data.nodeName})` : 'The LCP element';
83
+ const theLcpElement = lcpEvent.args.data?.nodeName ?
84
+ `The LCP element (${lcpEvent.args.data.nodeName}, nodeId: ${lcpEvent.args.data.nodeId})` :
85
+ 'The LCP element';
66
86
  const parts = [
67
87
  `The Largest Contentful Paint (LCP) time for this navigation was ${this.#formatMicro(metricScore.timing)}.`,
68
88
  ];
69
89
  if (lcpRequest) {
70
- parts.push(`${theLcpElement} is an image fetched from \`${lcpRequest.args.data.url}\`.`);
71
- const request = TraceEventFormatter.networkRequests([lcpRequest], this.#parsedTrace, { verbose: true, customTitle: 'LCP resource network request' });
90
+ parts.push(`${theLcpElement} is an image fetched from ${this.#formatRequestUrl(lcpRequest)}.`);
91
+ const request = this.#traceFormatter.formatNetworkRequests([lcpRequest], { verbose: true, customTitle: 'LCP resource network request' });
72
92
  parts.push(request);
73
93
  }
74
94
  else {
@@ -77,6 +97,12 @@ export class PerformanceInsightFormatter {
77
97
  return parts.join('\n');
78
98
  }
79
99
  insightIsSupported() {
100
+ // Although our types don't show it, Insights can end up as Errors if there
101
+ // is an issue in the processing stage. In this case we should gracefully
102
+ // ignore this error.
103
+ if (this.#insight instanceof Error) {
104
+ return false;
105
+ }
80
106
  return this.#description().length > 0;
81
107
  }
82
108
  getSuggestions() {
@@ -170,13 +196,54 @@ export class PerformanceInsightFormatter {
170
196
  }
171
197
  let output = 'The following resources were associated with ineffficient cache policies:\n';
172
198
  for (const entry of insight.requests) {
173
- output += `\n- ${entry.request.args.data.url}`;
199
+ output += `\n- ${this.#formatRequestUrl(entry.request)}`;
174
200
  output += `\n - Cache Time to Live (TTL): ${entry.ttl} seconds`;
175
201
  output += `\n - Wasted bytes: ${bytes(entry.wastedBytes)}`;
176
202
  }
177
203
  output += '\n\n' + Trace.Insights.Models.Cache.UIStrings.description;
178
204
  return output;
179
205
  }
206
+ #formatLayoutShift(shift, index, rootCauses) {
207
+ const baseTime = this.#parsedTrace.data.Meta.traceBounds.min;
208
+ const potentialRootCauses = [];
209
+ if (rootCauses) {
210
+ rootCauses.iframes.forEach(iframe => potentialRootCauses.push(`- An iframe (id: ${iframe.frame}, url: ${iframe.url ?? 'unknown'} was injected into the page)`));
211
+ rootCauses.webFonts.forEach(req => {
212
+ potentialRootCauses.push(`- A font that was loaded over the network: ${this.#formatRequestUrl(req)}.`);
213
+ });
214
+ rootCauses.nonCompositedAnimations.forEach(nonCompositedFailure => {
215
+ potentialRootCauses.push('- A non-composited animation:');
216
+ const animationInfoOutput = [];
217
+ potentialRootCauses.push(`- non-composited animation: \`${nonCompositedFailure.name || '(unnamed)'}\``);
218
+ if (nonCompositedFailure.name) {
219
+ animationInfoOutput.push(`Animation name: ${nonCompositedFailure.name}`);
220
+ }
221
+ if (nonCompositedFailure.unsupportedProperties) {
222
+ animationInfoOutput.push('Unsupported CSS properties:');
223
+ animationInfoOutput.push('- ' + nonCompositedFailure.unsupportedProperties.join(', '));
224
+ }
225
+ animationInfoOutput.push('Failure reasons:');
226
+ animationInfoOutput.push(' - ' + nonCompositedFailure.failureReasons.join(', '));
227
+ // Extra padding to the detail to not mess up the indentation.
228
+ potentialRootCauses.push(animationInfoOutput.map(l => ' '.repeat(4) + l).join('\n'));
229
+ });
230
+ rootCauses.unsizedImages.forEach(img => {
231
+ // TODO(b/413284569): if we store a nice human readable name for this
232
+ // image in the trace metadata, we can do something much nicer here.
233
+ const url = img.paintImageEvent.args.data.url;
234
+ const nodeName = img.paintImageEvent.args.data.nodeName;
235
+ const extraText = url ? `url: ${this.#formatUrl(url)}` : `id: ${img.backendNodeId}`;
236
+ potentialRootCauses.push(`- An unsized image (${nodeName}) (${extraText}).`);
237
+ });
238
+ }
239
+ const rootCauseText = potentialRootCauses.length ? `- Potential root causes:\n ${potentialRootCauses.join('\n')}` :
240
+ '- No potential root causes identified';
241
+ const startTime = Trace.Helpers.Timing.microToMilli(Trace.Types.Timing.Micro(shift.ts - baseTime));
242
+ return `### Layout shift ${index + 1}:
243
+ - Start time: ${millis(startTime)}
244
+ - Score: ${shift.args.data?.weighted_score_delta.toFixed(4)}
245
+ ${rootCauseText}`;
246
+ }
180
247
  /**
181
248
  * Create an AI prompt string out of the CLS Culprits Insight model to use with Ask AI.
182
249
  * @param insight The CLS Culprits Model to query.
@@ -185,7 +252,7 @@ export class PerformanceInsightFormatter {
185
252
  formatClsCulpritsInsight(insight) {
186
253
  const { worstCluster, shifts } = insight;
187
254
  if (!worstCluster) {
188
- return '';
255
+ return 'No layout shifts were found.';
189
256
  }
190
257
  const baseTime = this.#parsedTrace.data.Meta.traceBounds.min;
191
258
  const clusterTimes = {
@@ -193,7 +260,7 @@ export class PerformanceInsightFormatter {
193
260
  end: worstCluster.ts + worstCluster.dur - baseTime,
194
261
  };
195
262
  const shiftsFormatted = worstCluster.events.map((layoutShift, index) => {
196
- return TraceEventFormatter.layoutShift(layoutShift, index, this.#parsedTrace, shifts.get(layoutShift));
263
+ return this.#formatLayoutShift(layoutShift, index, shifts.get(layoutShift));
197
264
  });
198
265
  return `The worst layout shift cluster was the cluster that started at ${this.#formatMicro(clusterTimes.start)} and ended at ${this.#formatMicro(clusterTimes.end)}, with a duration of ${this.#formatMicro(worstCluster.dur)}.
199
266
  The score for this cluster is ${worstCluster.clusterCumulativeScore.toFixed(4)}.
@@ -229,7 +296,7 @@ ${shiftsFormatted.join('\n')}`;
229
296
  });
230
297
  return `${this.#lcpMetricSharedContext()}
231
298
 
232
- ${TraceEventFormatter.networkRequests([documentRequest], this.#parsedTrace, {
299
+ ${this.#traceFormatter.formatNetworkRequests([documentRequest], {
233
300
  verbose: true,
234
301
  customTitle: 'Document network request'
235
302
  })}
@@ -321,7 +388,7 @@ Duplication grouped by Node modules: ${filesFormatted}`;
321
388
  const url = new Common.ParsedURL.ParsedURL(font.request.args.data.url);
322
389
  fontName = url.isValid ? url.lastPathComponent : '(not available)';
323
390
  }
324
- output += `\n - Font name: ${fontName}, URL: ${font.request.args.data.url}, Property 'font-display' set to: '${font.display}', Wasted time: ${this.#formatMilli(font.wastedTime)}.`;
391
+ output += `\n - Font name: ${fontName}, URL: ${this.#formatRequestUrl(font.request)}, Property 'font-display' set to: '${font.display}', Wasted time: ${this.#formatMilli(font.wastedTime)}.`;
325
392
  }
326
393
  output += '\n\n' + Trace.Insights.Models.FontDisplay.UIStrings.description;
327
394
  return output;
@@ -395,7 +462,7 @@ Duplication grouped by Node modules: ${filesFormatted}`;
395
462
  return `${message} (Est ${byteSavings})`;
396
463
  })
397
464
  .join('\n');
398
- return `### ${image.request.args.data.url}
465
+ return `### ${this.#formatRequestUrl(image.request)}
399
466
  - Potential savings: ${bytes(image.byteSavings)}
400
467
  - Optimizations:\n${optimizations}`;
401
468
  })
@@ -488,7 +555,7 @@ ${checklistBulletPoints.map(point => `- ${point.name}: ${point.passed ? 'PASSED'
488
555
  return 'There is no significant amount of legacy JavaScript on the page.';
489
556
  }
490
557
  const filesFormatted = Array.from(legacyJavaScriptResults)
491
- .map(([script, result]) => `\n- Script: ${script.url} - Wasted bytes: ${result.estimatedByteSavings} bytes
558
+ .map(([script, result]) => `\n- Script: ${this.#formatScriptUrl(script)} - Wasted bytes: ${result.estimatedByteSavings} bytes
492
559
  Matches:
493
560
  ${result.matches.map(match => `Line: ${match.line}, Column: ${match.column}, Name: ${match.name}`).join('\n')}`)
494
561
  .join('\n');
@@ -504,8 +571,8 @@ ${filesFormatted}`;
504
571
  */
505
572
  formatModernHttpInsight(insight) {
506
573
  const requestSummary = (insight.http1Requests.length === 1) ?
507
- TraceEventFormatter.networkRequests(insight.http1Requests, this.#parsedTrace, { verbose: true }) :
508
- TraceEventFormatter.networkRequests(insight.http1Requests, this.#parsedTrace);
574
+ this.#traceFormatter.formatNetworkRequests(insight.http1Requests, { verbose: true }) :
575
+ this.#traceFormatter.formatNetworkRequests(insight.http1Requests);
509
576
  if (requestSummary.length === 0) {
510
577
  return 'There are no requests that were served over a legacy HTTP protocol.';
511
578
  }
@@ -529,7 +596,7 @@ ${requestSummary}`;
529
596
  output += `Max critical path latency is ${this.#formatMicro(insight.maxTime)}\n\n`;
530
597
  output += 'The following is the critical request chain:\n';
531
598
  function formatNode(node, indent) {
532
- const url = node.request.args.data.url;
599
+ const url = this.#formatRequestUrl(node.request);
533
600
  const time = this.#formatMicro(node.timeFromInitialRequest);
534
601
  const isLongest = node.isLongest ? ' (longest chain)' : '';
535
602
  let nodeString = `${indent}- ${url} (${time})${isLongest}\n`;
@@ -589,7 +656,7 @@ ${requestSummary}`;
589
656
  * @returns a string formatted for sending to Ask AI.
590
657
  */
591
658
  formatRenderBlockingInsight(insight) {
592
- const requestSummary = TraceEventFormatter.networkRequests(insight.renderBlockingRequests, this.#parsedTrace);
659
+ const requestSummary = this.#traceFormatter.formatNetworkRequests(insight.renderBlockingRequests);
593
660
  if (requestSummary.length === 0) {
594
661
  return 'There are no network requests that are render blocking.';
595
662
  }
@@ -780,7 +847,7 @@ ${this.#links()}`;
780
847
  #links() {
781
848
  switch (this.#insight.insightKey) {
782
849
  case 'CLSCulprits':
783
- return `- https://wdeb.dev/articles/cls
850
+ return `- https://web.dev/articles/cls
784
851
  - https://web.dev/articles/optimize-cls`;
785
852
  case 'DocumentLatency':
786
853
  return '- https://web.dev/articles/optimize-ttfb';
@@ -910,267 +977,3 @@ Polyfills and transforms enable older browsers to use new JavaScript features. H
910
977
  }
911
978
  }
912
979
  }
913
- export class TraceEventFormatter {
914
- static layoutShift(shift, index, parsedTrace, rootCauses) {
915
- const baseTime = parsedTrace.data.Meta.traceBounds.min;
916
- const potentialRootCauses = [];
917
- if (rootCauses) {
918
- rootCauses.iframes.forEach(iframe => potentialRootCauses.push(`An iframe (id: ${iframe.frame}, url: ${iframe.url ?? 'unknown'} was injected into the page)`));
919
- rootCauses.webFonts.forEach(req => {
920
- potentialRootCauses.push(`A font that was loaded over the network (${req.args.data.url}).`);
921
- });
922
- // TODO(b/413285103): use the nice strings for non-composited animations.
923
- // The code for this lives in TimelineUIUtils but that cannot be used
924
- // within models. We should move it and then expose the animations info
925
- // more nicely.
926
- rootCauses.nonCompositedAnimations.forEach(_ => {
927
- potentialRootCauses.push('A non composited animation.');
928
- });
929
- rootCauses.unsizedImages.forEach(img => {
930
- // TODO(b/413284569): if we store a nice human readable name for this
931
- // image in the trace metadata, we can do something much nicer here.
932
- const url = img.paintImageEvent.args.data.url;
933
- const nodeName = img.paintImageEvent.args.data.nodeName;
934
- const extraText = url ? `url: ${url}` : `id: ${img.backendNodeId}`;
935
- potentialRootCauses.push(`An unsized image (${nodeName}) (${extraText}).`);
936
- });
937
- }
938
- const rootCauseText = potentialRootCauses.length ?
939
- `- Potential root causes:\n - ${potentialRootCauses.join('\n - ')}` :
940
- '- No potential root causes identified';
941
- const startTime = Trace.Helpers.Timing.microToMilli(Trace.Types.Timing.Micro(shift.ts - baseTime));
942
- return `### Layout shift ${index + 1}:
943
- - Start time: ${millis(startTime)}
944
- - Score: ${shift.args.data?.weighted_score_delta.toFixed(4)}
945
- ${rootCauseText}`;
946
- }
947
- // Stringify network requests for the LLM model.
948
- static networkRequests(requests, parsedTrace, options) {
949
- if (requests.length === 0) {
950
- return '';
951
- }
952
- let verbose;
953
- if (options?.verbose !== undefined) {
954
- verbose = options.verbose;
955
- }
956
- else {
957
- verbose = requests.length === 1;
958
- }
959
- // Use verbose format for a single network request. With the compressed format, a format description
960
- // needs to be provided, which is not worth sending if only one network request is being stringified.
961
- // For a single request, use `formatRequestVerbosely`, which formats with all fields specified and does not require a
962
- // format description.
963
- if (verbose) {
964
- return requests.map(request => this.#networkRequestVerbosely(request, parsedTrace, options?.customTitle))
965
- .join('\n');
966
- }
967
- return this.#networkRequestsArrayCompressed(requests, parsedTrace);
968
- }
969
- /**
970
- * This is the data passed to a network request when the Performance Insights
971
- * agent is asking for information. It is a slimmed down version of the
972
- * request's data to avoid using up too much of the context window.
973
- * IMPORTANT: these set of fields have been reviewed by Chrome Privacy &
974
- * Security; be careful about adding new data here. If you are in doubt please
975
- * talk to jacktfranklin@.
976
- */
977
- static #networkRequestVerbosely(request, parsedTrace, customTitle) {
978
- const { url, statusCode, initialPriority, priority, fromServiceWorker, mimeType, responseHeaders, syntheticData, protocol } = request.args.data;
979
- const titlePrefix = `## ${customTitle ?? 'Network request'}`;
980
- // Note: unlike other agents, we do have the ability to include
981
- // cross-origins, hence why we do not sanitize the URLs here.
982
- const navigationForEvent = Trace.Helpers.Trace.getNavigationForTraceEvent(request, request.args.data.frame, parsedTrace.data.Meta.navigationsByFrameId);
983
- const baseTime = navigationForEvent?.ts ?? parsedTrace.data.Meta.traceBounds.min;
984
- // Gets all the timings for this request, relative to the base time.
985
- // Note that this is the start time, not total time. E.g. "queuedAt: X"
986
- // means that the request was queued at Xms, not that it queued for Xms.
987
- const startTimesForLifecycle = {
988
- queuedAt: request.ts - baseTime,
989
- requestSentAt: syntheticData.sendStartTime - baseTime,
990
- downloadCompletedAt: syntheticData.finishTime - baseTime,
991
- processingCompletedAt: request.ts + request.dur - baseTime,
992
- };
993
- const mainThreadProcessingDuration = startTimesForLifecycle.processingCompletedAt - startTimesForLifecycle.downloadCompletedAt;
994
- const downloadTime = syntheticData.finishTime - syntheticData.downloadStart;
995
- const renderBlocking = Trace.Helpers.Network.isSyntheticNetworkRequestEventRenderBlocking(request);
996
- const initiator = parsedTrace.data.NetworkRequests.eventToInitiator.get(request);
997
- const priorityLines = [];
998
- if (initialPriority === priority) {
999
- priorityLines.push(`Priority: ${priority}`);
1000
- }
1001
- else {
1002
- priorityLines.push(`Initial priority: ${initialPriority}`);
1003
- priorityLines.push(`Final priority: ${priority}`);
1004
- }
1005
- const redirects = request.args.data.redirects.map((redirect, index) => {
1006
- const startTime = redirect.ts - baseTime;
1007
- return `#### Redirect ${index + 1}: ${redirect.url}
1008
- - Start time: ${micros(startTime)}
1009
- - Duration: ${micros(redirect.dur)}`;
1010
- });
1011
- const initiators = this.#getInitiatorChain(parsedTrace, request);
1012
- const initiatorUrls = initiators.map(initiator => initiator.args.data.url);
1013
- return `${titlePrefix}: ${url}
1014
- Timings:
1015
- - Queued at: ${micros(startTimesForLifecycle.queuedAt)}
1016
- - Request sent at: ${micros(startTimesForLifecycle.requestSentAt)}
1017
- - Download complete at: ${micros(startTimesForLifecycle.downloadCompletedAt)}
1018
- - Main thread processing completed at: ${micros(startTimesForLifecycle.processingCompletedAt)}
1019
- Durations:
1020
- - Download time: ${micros(downloadTime)}
1021
- - Main thread processing time: ${micros(mainThreadProcessingDuration)}
1022
- - Total duration: ${micros(request.dur)}${initiator ? `\nInitiator: ${initiator.args.data.url}` : ''}
1023
- Redirects:${redirects.length ? '\n' + redirects.join('\n') : ' no redirects'}
1024
- Status code: ${statusCode}
1025
- MIME Type: ${mimeType}
1026
- Protocol: ${protocol}
1027
- ${priorityLines.join('\n')}
1028
- Render blocking: ${renderBlocking ? 'Yes' : 'No'}
1029
- From a service worker: ${fromServiceWorker ? 'Yes' : 'No'}
1030
- Initiators (root request to the request that directly loaded this one): ${initiatorUrls.join(', ') || 'none'}
1031
- ${NetworkRequestFormatter.formatHeaders('Response headers', responseHeaders ?? [], true)}`;
1032
- }
1033
- static #getOrAssignUrlIndex(urlIdToIndex, url) {
1034
- let index = urlIdToIndex.get(url);
1035
- if (index !== undefined) {
1036
- return index;
1037
- }
1038
- index = urlIdToIndex.size;
1039
- urlIdToIndex.set(url, index);
1040
- return index;
1041
- }
1042
- // A compact network requests format designed to save tokens when sending multiple network requests to the model.
1043
- // It creates a map that maps request URLs to IDs and references the IDs in the compressed format.
1044
- //
1045
- // Important: Do not use this method for stringifying a single network request. With this format, a format description
1046
- // needs to be provided, which is not worth sending if only one network request is being stringified.
1047
- // For a single request, use `formatRequestVerbosely`, which formats with all fields specified and does not require a
1048
- // format description.
1049
- static #networkRequestsArrayCompressed(requests, parsedTrace) {
1050
- const networkDataString = `
1051
- Network requests data:
1052
-
1053
- `;
1054
- const urlIdToIndex = new Map();
1055
- const allRequestsText = requests
1056
- .map(request => {
1057
- const urlIndex = TraceEventFormatter.#getOrAssignUrlIndex(urlIdToIndex, request.args.data.url);
1058
- return this.#networkRequestCompressedFormat(urlIndex, request, parsedTrace, urlIdToIndex);
1059
- })
1060
- .join('\n');
1061
- const urlsMapString = 'allUrls = ' +
1062
- `[${Array.from(urlIdToIndex.entries())
1063
- .map(([url, index]) => {
1064
- return `${index}: ${url}`;
1065
- })
1066
- .join(', ')}]`;
1067
- return networkDataString + '\n\n' + urlsMapString + '\n\n' + allRequestsText;
1068
- }
1069
- /**
1070
- * Network requests format description that is sent to the model as a fact.
1071
- */
1072
- static networkDataFormatDescription = `Network requests are formatted like this:
1073
- \`urlIndex;queuedTime;requestSentTime;downloadCompleteTime;processingCompleteTime;totalDuration;downloadDuration;mainThreadProcessingDuration;statusCode;mimeType;priority;initialPriority;finalPriority;renderBlocking;protocol;fromServiceWorker;initiators;redirects:[[redirectUrlIndex|startTime|duration]];responseHeaders:[header1Value|header2Value|...]\`
1074
-
1075
- - \`urlIndex\`: Numerical index for the request's URL, referencing the "All URLs" list.
1076
- Timings (all in milliseconds, relative to navigation start):
1077
- - \`queuedTime\`: When the request was queued.
1078
- - \`requestSentTime\`: When the request was sent.
1079
- - \`downloadCompleteTime\`: When the download completed.
1080
- - \`processingCompleteTime\`: When main thread processing finished.
1081
- Durations (all in milliseconds):
1082
- - \`totalDuration\`: Total time from the request being queued until its main thread processing completed.
1083
- - \`downloadDuration\`: Time spent actively downloading the resource.
1084
- - \`mainThreadProcessingDuration\`: Time spent on the main thread after the download completed.
1085
- - \`statusCode\`: The HTTP status code of the response (e.g., 200, 404).
1086
- - \`mimeType\`: The MIME type of the resource (e.g., "text/html", "application/javascript").
1087
- - \`priority\`: The final network request priority (e.g., "VeryHigh", "Low").
1088
- - \`initialPriority\`: The initial network request priority.
1089
- - \`finalPriority\`: The final network request priority (redundant if \`priority\` is always final, but kept for clarity if \`initialPriority\` and \`priority\` differ).
1090
- - \`renderBlocking\`: 't' if the request was render-blocking, 'f' otherwise.
1091
- - \`protocol\`: The network protocol used (e.g., "h2", "http/1.1").
1092
- - \`fromServiceWorker\`: 't' if the request was served from a service worker, 'f' otherwise.
1093
- - \`initiators\`: A list (separated by ,) of URL indices for the initiator chain of this request. Listed in order starting from the root request to the request that directly loaded this one. This represents the network dependencies necessary to load this request. If there is no initiator, this is empty.
1094
- - \`redirects\`: A comma-separated list of redirects, enclosed in square brackets. Each redirect is formatted as
1095
- \`[redirectUrlIndex|startTime|duration]\`, where: \`redirectUrlIndex\`: Numerical index for the redirect's URL. \`startTime\`: The start time of the redirect in milliseconds, relative to navigation start. \`duration\`: The duration of the redirect in milliseconds.
1096
- - \`responseHeaders\`: A list (separated by '|') of values for specific, pre-defined response headers, enclosed in square brackets.
1097
- The order of headers corresponds to an internal fixed list. If a header is not present, its value will be empty.
1098
- `;
1099
- /**
1100
- *
1101
- * This is the network request data passed to the Performance agent.
1102
- *
1103
- * The `urlIdToIndex` Map is used to map URLs to numerical indices in order to not need to pass whole url every time it's mentioned.
1104
- * The map content is passed in the response together will all the requests data.
1105
- *
1106
- * See `networkDataFormatDescription` above for specifics.
1107
- */
1108
- static #networkRequestCompressedFormat(urlIndex, request, parsedTrace, urlIdToIndex) {
1109
- const { statusCode, initialPriority, priority, fromServiceWorker, mimeType, responseHeaders, syntheticData, protocol, } = request.args.data;
1110
- const navigationForEvent = Trace.Helpers.Trace.getNavigationForTraceEvent(request, request.args.data.frame, parsedTrace.data.Meta.navigationsByFrameId);
1111
- const baseTime = navigationForEvent?.ts ?? parsedTrace.data.Meta.traceBounds.min;
1112
- const queuedTime = micros(request.ts - baseTime);
1113
- const requestSentTime = micros(syntheticData.sendStartTime - baseTime);
1114
- const downloadCompleteTime = micros(syntheticData.finishTime - baseTime);
1115
- const processingCompleteTime = micros(request.ts + request.dur - baseTime);
1116
- const totalDuration = micros(request.dur);
1117
- const downloadDuration = micros(syntheticData.finishTime - syntheticData.downloadStart);
1118
- const mainThreadProcessingDuration = micros(request.ts + request.dur - syntheticData.finishTime);
1119
- const renderBlocking = Trace.Helpers.Network.isSyntheticNetworkRequestEventRenderBlocking(request) ? 't' : 'f';
1120
- const finalPriority = priority;
1121
- const headerValues = responseHeaders
1122
- ?.map(header => {
1123
- const value = NetworkRequestFormatter.allowHeader(header.name) ? header.value : '<redacted>';
1124
- return `${header.name}: ${value}`;
1125
- })
1126
- .join('|');
1127
- const redirects = request.args.data.redirects
1128
- .map(redirect => {
1129
- const urlIndex = TraceEventFormatter.#getOrAssignUrlIndex(urlIdToIndex, redirect.url);
1130
- const redirectStartTime = micros(redirect.ts - baseTime);
1131
- const redirectDuration = micros(redirect.dur);
1132
- return `[${urlIndex}|${redirectStartTime}|${redirectDuration}]`;
1133
- })
1134
- .join(',');
1135
- const initiators = this.#getInitiatorChain(parsedTrace, request);
1136
- const initiatorUrlIndices = initiators.map(initiator => TraceEventFormatter.#getOrAssignUrlIndex(urlIdToIndex, initiator.args.data.url));
1137
- const parts = [
1138
- urlIndex,
1139
- queuedTime,
1140
- requestSentTime,
1141
- downloadCompleteTime,
1142
- processingCompleteTime,
1143
- totalDuration,
1144
- downloadDuration,
1145
- mainThreadProcessingDuration,
1146
- statusCode,
1147
- mimeType,
1148
- priority,
1149
- initialPriority,
1150
- finalPriority,
1151
- renderBlocking,
1152
- protocol,
1153
- fromServiceWorker ? 't' : 'f',
1154
- initiatorUrlIndices.join(','),
1155
- `[${redirects}]`,
1156
- `[${headerValues ?? ''}]`,
1157
- ];
1158
- return parts.join(';');
1159
- }
1160
- static #getInitiatorChain(parsedTrace, request) {
1161
- const initiators = [];
1162
- let cur = request;
1163
- while (cur) {
1164
- const initiator = parsedTrace.data.NetworkRequests.eventToInitiator.get(cur);
1165
- if (initiator) {
1166
- // Should never happen, but if it did that would be an infinite loop.
1167
- if (initiators.includes(initiator)) {
1168
- return [];
1169
- }
1170
- initiators.unshift(initiator);
1171
- }
1172
- cur = initiator;
1173
- }
1174
- return initiators;
1175
- }
1176
- }