chrome-devtools-mcp 0.6.0 → 0.7.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 (63) hide show
  1. package/README.md +13 -6
  2. package/build/node_modules/chrome-devtools-frontend/front_end/core/common/Color.js +13 -9
  3. package/build/node_modules/chrome-devtools-frontend/front_end/core/common/ColorConverter.js +9 -7
  4. package/build/node_modules/chrome-devtools-frontend/front_end/core/common/Gzip.js +1 -1
  5. package/build/node_modules/chrome-devtools-frontend/front_end/core/common/MapWithDefault.js +5 -3
  6. package/build/node_modules/chrome-devtools-frontend/front_end/core/common/ResourceType.js +0 -11
  7. package/build/node_modules/chrome-devtools-frontend/front_end/core/common/ReturnToPanel.js +6 -4
  8. package/build/node_modules/chrome-devtools-frontend/front_end/core/host/AidaClient.js +1 -1
  9. package/build/node_modules/chrome-devtools-frontend/front_end/core/host/GdpClient.js +116 -59
  10. package/build/node_modules/chrome-devtools-frontend/front_end/core/host/Platform.js +5 -3
  11. package/build/node_modules/chrome-devtools-frontend/front_end/core/host/UserMetrics.js +6 -4
  12. package/build/node_modules/chrome-devtools-frontend/front_end/core/platform/ArrayUtilities.js +1 -1
  13. package/build/node_modules/chrome-devtools-frontend/front_end/core/platform/StringUtilities.js +33 -31
  14. package/build/node_modules/chrome-devtools-frontend/front_end/core/sdk/CSSMetadata.js +4 -2
  15. package/build/node_modules/chrome-devtools-frontend/front_end/core/sdk/CSSPropertyParser.js +11 -9
  16. package/build/node_modules/chrome-devtools-frontend/front_end/core/sdk/CSSPropertyParserMatchers.js +19 -13
  17. package/build/node_modules/chrome-devtools-frontend/front_end/core/sdk/ChildTargetManager.js +30 -0
  18. package/build/node_modules/chrome-devtools-frontend/front_end/core/sdk/DOMModel.js +1 -1
  19. package/build/node_modules/chrome-devtools-frontend/front_end/core/sdk/EventBreakpointsModel.js +4 -2
  20. package/build/node_modules/chrome-devtools-frontend/front_end/core/sdk/HttpReasonPhraseStrings.js +4 -2
  21. package/build/node_modules/chrome-devtools-frontend/front_end/core/sdk/NetworkManager.js +9 -41
  22. package/build/node_modules/chrome-devtools-frontend/front_end/core/sdk/NetworkRequest.js +0 -14
  23. package/build/node_modules/chrome-devtools-frontend/front_end/core/sdk/PageResourceLoader.js +1 -1
  24. package/build/node_modules/chrome-devtools-frontend/front_end/core/sdk/PreloadingModel.js +7 -5
  25. package/build/node_modules/chrome-devtools-frontend/front_end/core/sdk/RehydratingConnection.js +1 -1
  26. package/build/node_modules/chrome-devtools-frontend/front_end/core/sdk/RemoteObject.js +1 -1
  27. package/build/node_modules/chrome-devtools-frontend/front_end/core/sdk/ResourceTreeModel.js +1 -0
  28. package/build/node_modules/chrome-devtools-frontend/front_end/core/sdk/ScreenCaptureModel.js +20 -18
  29. package/build/node_modules/chrome-devtools-frontend/front_end/core/sdk/Target.js +7 -1
  30. package/build/node_modules/chrome-devtools-frontend/front_end/core/sdk/TraceObject.js +2 -2
  31. package/build/node_modules/chrome-devtools-frontend/front_end/generated/Deprecation.js +4 -4
  32. package/build/node_modules/chrome-devtools-frontend/front_end/generated/InspectorBackendCommands.js +2 -2
  33. package/build/node_modules/chrome-devtools-frontend/front_end/models/ai_assistance/data_formatters/NetworkRequestFormatter.js +30 -3
  34. package/build/node_modules/chrome-devtools-frontend/front_end/models/ai_assistance/performance/AIContext.js +18 -4
  35. package/build/node_modules/chrome-devtools-frontend/front_end/models/crux-manager/CrUXManager.js +1 -1
  36. package/build/node_modules/chrome-devtools-frontend/front_end/models/network_time_calculator/RequestTimeRanges.js +6 -4
  37. package/build/node_modules/chrome-devtools-frontend/front_end/models/source_map_scopes/NamesResolver.js +7 -5
  38. package/build/node_modules/chrome-devtools-frontend/front_end/models/trace/LanternComputationData.js +1 -0
  39. package/build/node_modules/chrome-devtools-frontend/front_end/models/trace/extras/TraceTree.js +1 -1
  40. package/build/node_modules/chrome-devtools-frontend/front_end/models/trace/handlers/FramesHandler.js +7 -5
  41. package/build/node_modules/chrome-devtools-frontend/front_end/models/trace/handlers/LayoutShiftsHandler.js +8 -4
  42. package/build/node_modules/chrome-devtools-frontend/front_end/models/trace/handlers/NetworkRequestsHandler.js +17 -0
  43. package/build/node_modules/chrome-devtools-frontend/front_end/models/trace/handlers/helpers.js +1 -1
  44. package/build/node_modules/chrome-devtools-frontend/front_end/models/trace/helpers/Timing.js +4 -2
  45. package/build/node_modules/chrome-devtools-frontend/front_end/models/trace/helpers/Trace.js +8 -4
  46. package/build/node_modules/chrome-devtools-frontend/front_end/models/trace/insights/DocumentLatency.js +10 -10
  47. package/build/node_modules/chrome-devtools-frontend/front_end/models/trace/insights/INPBreakdown.js +12 -1
  48. package/build/node_modules/chrome-devtools-frontend/front_end/models/trace/insights/LCPBreakdown.js +11 -1
  49. package/build/node_modules/chrome-devtools-frontend/front_end/models/trace/insights/NetworkDependencyTree.js +2 -2
  50. package/build/node_modules/chrome-devtools-frontend/front_end/models/trace/types/TraceEvents.js +5 -3
  51. package/build/node_modules/chrome-devtools-frontend/front_end/models/trace_source_maps_resolver/SourceMapsResolver.js +1 -1
  52. package/build/src/McpContext.js +24 -7
  53. package/build/src/McpResponse.js +49 -20
  54. package/build/src/Mutex.js +3 -6
  55. package/build/src/browser.js +6 -5
  56. package/build/src/cli.js +10 -2
  57. package/build/src/formatters/consoleFormatter.js +1 -1
  58. package/build/src/formatters/networkFormatter.js +44 -0
  59. package/build/src/tools/emulation.js +13 -2
  60. package/build/src/tools/performance.js +3 -4
  61. package/build/src/tools/screenshot.js +2 -3
  62. package/build/src/trace-processing/parse.js +7 -6
  63. package/package.json +7 -6
@@ -3,11 +3,25 @@
3
3
  // found in the LICENSE file.
4
4
  import * as Trace from '../../../models/trace/trace.js';
5
5
  import { AICallTree } from './AICallTree.js';
6
+ /**
7
+ * Gets the first, most relevant InsightSet to use, following the logic of:
8
+ * 1. If there is only one InsightSet, use that.
9
+ * 2. If there are more, prefer the first we find that has a navigation associated with it.
10
+ * 3. If none with a navigation are found, fallback to the first one.
11
+ * 4. Otherwise, return null.
12
+ *
13
+ * TODO(cjamcl): we should just give the agent the entire insight set, and give
14
+ * summary detail about all of them + the ability to query each.
15
+ */
6
16
  function getFirstInsightSet(insights) {
7
- // Currently only support a single insight set. Pick the first one with a navigation.
8
- // TODO(cjamcl): we should just give the agent the entire insight set, and give
9
- // summary detail about all of them + the ability to query each.
10
- return [...insights.values()].filter(insightSet => insightSet.navigation).at(0) ?? null;
17
+ const insightSets = Array.from(insights.values());
18
+ if (insightSets.length === 0) {
19
+ return null;
20
+ }
21
+ if (insightSets.length === 1) {
22
+ return insightSets[0];
23
+ }
24
+ return insightSets.filter(set => set.navigation).at(0) ?? insightSets.at(0) ?? null;
11
25
  }
12
26
  export class AgentFocus {
13
27
  static fromParsedTrace(parsedTrace) {
@@ -19,7 +19,7 @@ const i18nString = i18n.i18n.getLocalizedString.bind(undefined, str_);
19
19
  const CRUX_API_KEY = 'AIzaSyCCSOx25vrb5z0tbedCB3_JRzzbVW6Uwgw';
20
20
  const DEFAULT_ENDPOINT = `https://chromeuxreport.googleapis.com/v1/records:queryRecord?key=${CRUX_API_KEY}`;
21
21
  let cruxManagerInstance;
22
- // TODO: Potentially support `TABLET`. Tablet field data will always be `null` until then.
22
+ /** TODO: Potentially support `TABLET`. Tablet field data will always be `null` until then. **/
23
23
  export const DEVICE_SCOPE_LIST = ['ALL', 'DESKTOP', 'PHONE'];
24
24
  const pageScopeList = ['origin', 'url'];
25
25
  const metrics = [
@@ -36,10 +36,12 @@ export function calculateRequestTimeRanges(request, navigationStart) {
36
36
  addRange(name, startTime + (start / 1000), startTime + (end / 1000));
37
37
  }
38
38
  }
39
- // In some situations, argument `start` may come before `startTime` (`timing.requestStart`). This is especially true
40
- // in cases such as SW static routing API where fields like `workerRouterEvaluationStart` or `workerCacheLookupStart`
41
- // is set before setting `timing.requestStart`. If the `start` and `end` is known to be a valid value (i.e. not default
42
- // invalid value -1 or undefined), we allow adding the range.
39
+ /**
40
+ * In some situations, argument `start` may come before `startTime` (`timing.requestStart`). This is especially true
41
+ * in cases such as SW static routing API where fields like `workerRouterEvaluationStart` or `workerCacheLookupStart`
42
+ * is set before setting `timing.requestStart`. If the `start` and `end` is known to be a valid value (i.e. not default
43
+ * invalid value -1 or undefined), we allow adding the range.
44
+ **/
43
45
  function addMaybeNegativeOffsetRange(name, start, end) {
44
46
  addRange(name, startTime + (start / 1000), startTime + (end / 1000));
45
47
  }
@@ -182,7 +182,7 @@ const resolveScope = async (script, scopeChain) => {
182
182
  return;
183
183
  }
184
184
  }
185
- // If there is no entry with the name field, try to infer the name from the source positions.
185
+ /** If there is no entry with the name field, try to infer the name from the source positions. **/
186
186
  async function resolvePosition() {
187
187
  if (!sourceMap) {
188
188
  return;
@@ -556,10 +556,12 @@ export class RemoteObject extends SDK.RemoteObject.RemoteObject {
556
556
  return this.object.isNode();
557
557
  }
558
558
  }
559
- // Resolve the frame's function name using the name associated with the opening
560
- // paren that starts the scope. If there is no name associated with the scope
561
- // start or if the function scope does not start with a left paren (e.g., arrow
562
- // function with one parameter), the resolution returns null.
559
+ /**
560
+ * Resolve the frame's function name using the name associated with the opening
561
+ * paren that starts the scope. If there is no name associated with the scope
562
+ * start or if the function scope does not start with a left paren (e.g., arrow
563
+ * function with one parameter), the resolution returns null.
564
+ **/
563
565
  async function getFunctionNameFromScopeStart(script, lineNumber, columnNumber) {
564
566
  // To reduce the overhead of resolving function names,
565
567
  // we check for source maps first and immediately leave
@@ -167,6 +167,7 @@ function createLanternRequest(parsedTrace, workerThreads, request) {
167
167
  priority: request.args.data.priority,
168
168
  frameId: request.args.data.frame,
169
169
  fromWorker,
170
+ serverResponseTime: request.args.data.lrServerResponseTime ?? undefined,
170
171
  // Set later.
171
172
  redirects: undefined,
172
173
  redirectSource: undefined,
@@ -573,7 +573,7 @@ export function eventStackFrame(event) {
573
573
  }
574
574
  return { ...topFrame, scriptId: String(topFrame.scriptId) };
575
575
  }
576
- // TODO(paulirish): rename to generateNodeId
576
+ /** TODO(paulirish): rename to generateNodeId **/
577
577
  export function generateEventID(event) {
578
578
  if (Types.Events.isProfileCall(event)) {
579
579
  const name = SamplesIntegrator.isNativeRuntimeFrame(event.callFrame) ?
@@ -410,7 +410,7 @@ export class PendingFrame {
410
410
  this.triggerTime = triggerTime;
411
411
  }
412
412
  }
413
- // The parameters of an impl-side BeginFrame.
413
+ /** The parameters of an impl-side BeginFrame. **/
414
414
  class BeginFrameInfo {
415
415
  seqId;
416
416
  startTime;
@@ -423,10 +423,12 @@ class BeginFrameInfo {
423
423
  this.isPartial = isPartial;
424
424
  }
425
425
  }
426
- // A queue of BeginFrames pending visualization.
427
- // BeginFrames are added into this queue as they occur; later when their
428
- // corresponding DrawFrames occur (or lack thereof), the BeginFrames are removed
429
- // from the queue and their timestamps are used for visualization.
426
+ /**
427
+ * A queue of BeginFrames pending visualization.
428
+ * BeginFrames are added into this queue as they occur; later when their
429
+ * corresponding DrawFrames occur (or lack thereof), the BeginFrames are removed
430
+ * from the queue and their timestamps are used for visualization.
431
+ **/
430
432
  export class TimelineFrameBeginFrameQueue {
431
433
  queueFrames = [];
432
434
  // Maps frameSeqId to BeginFrameInfo.
@@ -6,11 +6,15 @@ import * as Helpers from '../helpers/helpers.js';
6
6
  import * as Types from '../types/types.js';
7
7
  import { data as metaHandlerData } from './MetaHandler.js';
8
8
  import { data as screenshotsHandlerData } from './ScreenshotsHandler.js';
9
- // This represents the maximum #time we will allow a cluster to go before we
10
- // reset it.
9
+ /**
10
+ * This represents the maximum #time we will allow a cluster to go before we
11
+ * reset it.
12
+ **/
11
13
  export const MAX_CLUSTER_DURATION = Helpers.Timing.milliToMicro(Types.Timing.Milli(5000));
12
- // This represents the maximum #time we will allow between layout shift events
13
- // before considering it to be the start of a new cluster.
14
+ /**
15
+ * This represents the maximum #time we will allow between layout shift events
16
+ * before considering it to be the start of a new cluster.
17
+ **/
14
18
  export const MAX_SHIFT_TIME_DELTA = Helpers.Timing.milliToMicro(Types.Timing.Milli(1000));
15
19
  // Layout shifts are reported globally to the developer, irrespective of which
16
20
  // frame they originated in. However, each process does have its own individual
@@ -221,6 +221,7 @@ export async function finalize() {
221
221
  *
222
222
  * See `_updateTimingsForLightrider` in Lighthouse for more detail.
223
223
  */
224
+ let lrServerResponseTime;
224
225
  if (isLightrider && request.receiveResponse?.args.data.headers) {
225
226
  timing = {
226
227
  requestTime: Helpers.Timing.microToSeconds(request.sendRequests.at(0)?.ts ?? 0),
@@ -254,6 +255,13 @@ export async function finalize() {
254
255
  timing.connectEnd = TCPMs;
255
256
  timing.sslEnd = TCPMs;
256
257
  }
258
+ // Lightrider does not have any equivalent for `sendEnd` timing values. The
259
+ // closest we can get to the server response time is from a header that
260
+ // Lightrider sets.
261
+ const ResponseMsHeader = request.receiveResponse.args.data.headers.find(h => h.name === 'X-ResponseMs');
262
+ if (ResponseMsHeader) {
263
+ lrServerResponseTime = Math.max(0, parseInt(ResponseMsHeader.value, 10));
264
+ }
257
265
  }
258
266
  // TODO: consider allowing chrome / about.
259
267
  const allowedProtocols = [
@@ -346,6 +354,13 @@ export async function finalize() {
346
354
  const waiting = timing ?
347
355
  Types.Timing.Micro((timing.receiveHeadersEnd - timing.sendEnd) * MILLISECONDS_TO_MICROSECONDS) :
348
356
  Types.Timing.Micro(0);
357
+ // Server Response Time
358
+ // =======================
359
+ // Time from when the send finished going to when the first byte of headers were received.
360
+ const serverResponseTime = timing ?
361
+ Types.Timing.Micro(((timing.receiveHeadersStart ?? timing.receiveHeadersEnd) - timing.sendEnd) *
362
+ MILLISECONDS_TO_MICROSECONDS) :
363
+ Types.Timing.Micro(0);
349
364
  // Download
350
365
  // =======================
351
366
  // Time from receipt of headers to the finish time.
@@ -404,6 +419,7 @@ export async function finalize() {
404
419
  stalled,
405
420
  totalTime,
406
421
  waiting,
422
+ serverResponseTime,
407
423
  },
408
424
  // All fields below are from TraceEventsForNetworkRequest.
409
425
  decodedBodyLength,
@@ -428,6 +444,7 @@ export async function finalize() {
428
444
  initiator: finalSendRequest.args.data.initiator,
429
445
  stackTrace: finalSendRequest.args.data.stackTrace,
430
446
  timing,
447
+ lrServerResponseTime,
431
448
  url,
432
449
  failed: request.resourceFinish?.args.data.didFail ?? false,
433
450
  finished: Boolean(request.resourceFinish),
@@ -142,7 +142,7 @@ export function addEventToEntityMapping(event, entityMappings) {
142
142
  }
143
143
  entityMappings.entityByEvent.set(event, entity);
144
144
  }
145
- // A slight upgrade of addEventToEntityMapping to handle the sub-events of a network request.
145
+ /** A slight upgrade of addEventToEntityMapping to handle the sub-events of a network request. **/
146
146
  export function addNetworkRequestToEntityMapping(networkRequest, entityMappings, requestTraceEvents) {
147
147
  const entity = getEntityForEvent(networkRequest, entityMappings);
148
148
  if (!entity) {
@@ -25,8 +25,10 @@ export function timeStampForEventAdjustedByClosestNavigation(event, traceBounds,
25
25
  }
26
26
  return Types.Timing.Micro(eventTimeStamp);
27
27
  }
28
- // Expands the trace window by a provided percentage or, if it the expanded window is smaller than 1 millisecond, expands it to 1 millisecond.
29
- // If the expanded window is outside of the max trace window, cut the overflowing bound to the max trace window bound.
28
+ /**
29
+ * Expands the trace window by a provided percentage or, if it the expanded window is smaller than 1 millisecond, expands it to 1 millisecond.
30
+ * If the expanded window is outside of the max trace window, cut the overflowing bound to the max trace window bound.
31
+ **/
30
32
  export function expandWindowByPercentOrToOneMillisecond(annotationWindow, maxTraceWindow, percentage) {
31
33
  // Expand min and max of the window by half of the provided percentage. That way, in total, the window will be expanded by the provided percentage.
32
34
  let newMin = annotationWindow.min - annotationWindow.range * (percentage / 100) / 2;
@@ -70,8 +70,10 @@ export function extractOriginFromTrace(firstNavigationURL) {
70
70
  }
71
71
  return null;
72
72
  }
73
- // Each thread contains events. Events indicate the thread and process IDs, which are
74
- // used to store the event in the correct process thread entry below.
73
+ /**
74
+ * Each thread contains events. Events indicate the thread and process IDs, which are
75
+ * used to store the event in the correct process thread entry below.
76
+ **/
75
77
  export function addEventToProcessThread(event, eventsInProcessThread) {
76
78
  const { tid, pid } = event;
77
79
  let eventsInThread = eventsInProcessThread.get(pid);
@@ -704,8 +706,10 @@ export function extractSampleTraceId(event) {
704
706
  }
705
707
  return event.args?.sampleTraceId ?? event.args?.data?.sampleTraceId ?? null;
706
708
  }
707
- // This exactly matches Trace.Styles.visibleTypes. See the runtime verification in maybeInitStylesMap.
708
- // TODO(crbug.com/410884528)
709
+ /**
710
+ * This exactly matches Trace.Styles.visibleTypes. See the runtime verification in maybeInitStylesMap.
711
+ * TODO(crbug.com/410884528)
712
+ **/
709
713
  export const VISIBLE_TRACE_EVENT_TYPES = new Set([
710
714
  "AbortPostTaskCallback" /* Types.Events.Name.ABORT_POST_TASK_CALLBACK */,
711
715
  "Animation" /* Types.Events.Name.ANIMATION */,
@@ -67,20 +67,20 @@ const IGNORE_THRESHOLD_IN_BYTES = 1400;
67
67
  export function isDocumentLatencyInsight(x) {
68
68
  return x.insightKey === 'DocumentLatency';
69
69
  }
70
- function getServerResponseTime(request, context) {
71
- // Prefer the value as given by the Lantern provider.
72
- // For PSI, Lighthouse uses this to set a better value for the server response
73
- // time. For technical reasons, in Lightrider we do not have `sendEnd` timing
74
- // values. See Lighthouse's `asLanternNetworkRequest` function for more.
75
- const lanternRequest = context.navigation && context.lantern?.requests.find(r => r.rawRequest === request);
76
- if (lanternRequest?.serverResponseTime !== undefined) {
77
- return lanternRequest.serverResponseTime;
70
+ function getServerResponseTime(request) {
71
+ // For technical reasons, Lightrider does not have `sendEnd` timing values. The
72
+ // closest we can get to the server response time is from a header that Lightrider
73
+ // sets.
74
+ // @ts-expect-error
75
+ const isLightrider = globalThis.isLightrider;
76
+ if (isLightrider) {
77
+ return request.args.data.lrServerResponseTime ?? null;
78
78
  }
79
79
  const timing = request.args.data.timing;
80
80
  if (!timing) {
81
81
  return null;
82
82
  }
83
- const ms = Helpers.Timing.microToMilli(request.args.data.syntheticData.waiting);
83
+ const ms = Helpers.Timing.microToMilli(request.args.data.syntheticData.serverResponseTime);
84
84
  return Math.round(ms);
85
85
  }
86
86
  function getCompressionSavings(request) {
@@ -162,7 +162,7 @@ export function generateInsight(data, context) {
162
162
  if (!documentRequest) {
163
163
  return finalize({ warnings: [InsightWarning.NO_DOCUMENT_REQUEST] });
164
164
  }
165
- const serverResponseTime = getServerResponseTime(documentRequest, context);
165
+ const serverResponseTime = getServerResponseTime(documentRequest);
166
166
  if (serverResponseTime === null) {
167
167
  throw new Error('missing document request timing');
168
168
  }
@@ -2,6 +2,7 @@
2
2
  // Use of this source code is governed by a BSD-style license that can be
3
3
  // found in the LICENSE file.
4
4
  import * as i18n from '../../../core/i18n/i18n.js';
5
+ import * as Handlers from '../handlers/handlers.js';
5
6
  import * as Helpers from '../helpers/helpers.js';
6
7
  import { InsightCategory, } from './types.js';
7
8
  export const UIStrings = {
@@ -45,13 +46,23 @@ export function isINPBreakdownInsight(insight) {
45
46
  return insight.insightKey === "INPBreakdown" /* InsightKeys.INP_BREAKDOWN */;
46
47
  }
47
48
  function finalize(partialModel) {
49
+ let state = 'pass';
50
+ if (partialModel.longestInteractionEvent) {
51
+ const classification = Handlers.ModelHandlers.UserInteractions.scoreClassificationForInteractionToNextPaint(partialModel.longestInteractionEvent.dur);
52
+ if (classification === "good" /* Handlers.ModelHandlers.PageLoadMetrics.ScoreClassification.GOOD */) {
53
+ state = 'informative';
54
+ }
55
+ else {
56
+ state = 'fail';
57
+ }
58
+ }
48
59
  return {
49
60
  insightKey: "INPBreakdown" /* InsightKeys.INP_BREAKDOWN */,
50
61
  strings: UIStrings,
51
62
  title: i18nString(UIStrings.title),
52
63
  description: i18nString(UIStrings.description),
53
64
  category: InsightCategory.INP,
54
- state: partialModel.longestInteractionEvent ? 'informative' : 'pass',
65
+ state,
55
66
  ...partialModel,
56
67
  };
57
68
  }
@@ -124,13 +124,23 @@ function finalize(partialModel) {
124
124
  if (partialModel.lcpRequest) {
125
125
  relatedEvents.push(partialModel.lcpRequest);
126
126
  }
127
+ let state = 'pass';
128
+ if (partialModel.lcpMs !== undefined) {
129
+ const classification = Handlers.ModelHandlers.PageLoadMetrics.scoreClassificationForLargestContentfulPaint(Helpers.Timing.milliToMicro(partialModel.lcpMs));
130
+ if (classification === "good" /* Handlers.ModelHandlers.PageLoadMetrics.ScoreClassification.GOOD */) {
131
+ state = 'informative';
132
+ }
133
+ else {
134
+ state = 'fail';
135
+ }
136
+ }
127
137
  return {
128
138
  insightKey: "LCPBreakdown" /* InsightKeys.LCP_BREAKDOWN */,
129
139
  strings: UIStrings,
130
140
  title: i18nString(UIStrings.title),
131
141
  description: i18nString(UIStrings.description),
132
142
  category: InsightCategory.LCP,
133
- state: partialModel.lcpEvent || partialModel.lcpRequest ? 'informative' : 'pass',
143
+ state,
134
144
  ...partialModel,
135
145
  relatedEvents,
136
146
  };
@@ -337,7 +337,7 @@ export function handleLinkResponseHeader(linkHeaderValue) {
337
337
  }
338
338
  return preconnectedOrigins;
339
339
  }
340
- // Export the function for test purpose.
340
+ /** Export the function for test purpose. **/
341
341
  export function generatePreconnectedOrigins(data, context, contextRequests, preconnectCandidates) {
342
342
  const preconnectedOrigins = [];
343
343
  for (const event of data.NetworkRequests.linkPreconnectEvents) {
@@ -436,7 +436,7 @@ function candidateRequestsByOrigin(data, mainResource, contextRequests, lcpGraph
436
436
  });
437
437
  return origins;
438
438
  }
439
- // Export the function for test purpose.
439
+ /** Export the function for test purpose. **/
440
440
  export function generatePreconnectCandidates(data, context, contextRequests) {
441
441
  if (!context.lantern) {
442
442
  return [];
@@ -349,7 +349,7 @@ export function isResourceWillSendRequest(event) {
349
349
  export function isResourceReceivedData(event) {
350
350
  return event.name === 'ResourceReceivedData';
351
351
  }
352
- // Any event where we receive data (and get an encodedDataLength)
352
+ /** Any event where we receive data (and get an encodedDataLength) **/
353
353
  export function isReceivedDataEvent(event) {
354
354
  return event.name === 'ResourceReceivedData' || event.name === 'ResourceFinish' ||
355
355
  event.name === 'ResourceReceiveResponse';
@@ -547,8 +547,10 @@ export function isFlowPhaseEvent(event) {
547
547
  export function isParseAuthorStyleSheetEvent(event) {
548
548
  return event.name === "ParseAuthorStyleSheet" /* Name.PARSE_AUTHOR_STYLE_SHEET */ && event.ph === "X" /* Phase.COMPLETE */;
549
549
  }
550
- // NOT AN EXHAUSTIVE LIST: just some categories we use and refer
551
- // to in multiple places.
550
+ /**
551
+ * NOT AN EXHAUSTIVE LIST: just some categories we use and refer
552
+ * to in multiple places.
553
+ **/
552
554
  export const Categories = {
553
555
  Console: 'blink.console',
554
556
  UserTiming: 'blink.user_timing',
@@ -13,7 +13,7 @@ export class SourceMappingsUpdated extends Event {
13
13
  super(SourceMappingsUpdated.eventName, { composed: true, bubbles: true });
14
14
  }
15
15
  }
16
- // The code location key is created as a concatenation of its fields.
16
+ /** The code location key is created as a concatenation of its fields. **/
17
17
  export const resolvedCodeLocationDataNames = new Map();
18
18
  export class SourceMapsResolver extends EventTarget {
19
19
  executionContextNamesByOrigin = new Map();
@@ -27,6 +27,17 @@ function getNetworkMultiplierFromString(condition) {
27
27
  }
28
28
  return 1;
29
29
  }
30
+ function getExtensionFromMimeType(mimeType) {
31
+ switch (mimeType) {
32
+ case 'image/png':
33
+ return 'png';
34
+ case 'image/jpeg':
35
+ return 'jpeg';
36
+ case 'image/webp':
37
+ return 'webp';
38
+ }
39
+ throw new Error(`No mapping for Mime type ${mimeType}.`);
40
+ }
30
41
  export class McpContext {
31
42
  browser;
32
43
  logger;
@@ -258,18 +269,24 @@ export class McpContext {
258
269
  async saveTemporaryFile(data, mimeType) {
259
270
  try {
260
271
  const dir = await fs.mkdtemp(path.join(os.tmpdir(), 'chrome-devtools-mcp-'));
261
- const ext = mimeType === 'image/png'
262
- ? 'png'
263
- : mimeType === 'image/jpeg'
264
- ? 'jpg'
265
- : 'webp';
266
- const filename = path.join(dir, `screenshot.${ext}`);
272
+ const filename = path.join(dir, `screenshot.${getExtensionFromMimeType(mimeType)}`);
267
273
  await fs.writeFile(filename, data);
268
274
  return { filename };
269
275
  }
270
276
  catch (err) {
271
277
  this.logger(err);
272
- throw new Error('Could not save a screenshot to a file');
278
+ throw new Error('Could not save a screenshot to a file', { cause: err });
279
+ }
280
+ }
281
+ async saveFile(data, filename) {
282
+ try {
283
+ const filePath = path.resolve(filename);
284
+ await fs.writeFile(filePath, data);
285
+ return { filename };
286
+ }
287
+ catch (err) {
288
+ this.logger(err);
289
+ throw new Error('Could not save a screenshot to a file', { cause: err });
273
290
  }
274
291
  }
275
292
  storeTraceRecording(result) {
@@ -1,12 +1,12 @@
1
1
  import { formatConsoleEvent } from './formatters/consoleFormatter.js';
2
- import { getFormattedHeaderValue, getShortDescriptionForRequest, getStatusFromRequest, } from './formatters/networkFormatter.js';
2
+ import { getFormattedHeaderValue, getFormattedResponseBody, getFormattedRequestBody, getShortDescriptionForRequest, getStatusFromRequest, } from './formatters/networkFormatter.js';
3
3
  import { formatA11ySnapshot } from './formatters/snapshotFormatter.js';
4
4
  import { handleDialog } from './tools/pages.js';
5
5
  import { paginate } from './utils/pagination.js';
6
6
  export class McpResponse {
7
7
  #includePages = false;
8
8
  #includeSnapshot = false;
9
- #attachedNetworkRequestUrl;
9
+ #attachedNetworkRequestData;
10
10
  #includeConsoleData = false;
11
11
  #textResponseLines = [];
12
12
  #formattedConsoleData;
@@ -38,7 +38,9 @@ export class McpResponse {
38
38
  this.#includeConsoleData = value;
39
39
  }
40
40
  attachNetworkRequest(url) {
41
- this.#attachedNetworkRequestUrl = url;
41
+ this.#attachedNetworkRequestData = {
42
+ networkRequestUrl: url,
43
+ };
42
44
  }
43
45
  get includePages() {
44
46
  return this.#includePages;
@@ -50,7 +52,7 @@ export class McpResponse {
50
52
  return this.#includeConsoleData;
51
53
  }
52
54
  get attachedNetworkRequestUrl() {
53
- return this.#attachedNetworkRequestUrl;
55
+ return this.#attachedNetworkRequestData?.networkRequestUrl;
54
56
  }
55
57
  get networkRequestsPageIdx() {
56
58
  return this.#networkRequestsOptions?.pagination?.pageIdx;
@@ -78,6 +80,16 @@ export class McpResponse {
78
80
  await context.createTextSnapshot();
79
81
  }
80
82
  let formattedConsoleMessages;
83
+ if (this.#attachedNetworkRequestData?.networkRequestUrl) {
84
+ const request = context.getNetworkRequestByUrl(this.#attachedNetworkRequestData.networkRequestUrl);
85
+ this.#attachedNetworkRequestData.requestBody =
86
+ await getFormattedRequestBody(request);
87
+ const response = request.response();
88
+ if (response) {
89
+ this.#attachedNetworkRequestData.responseBody =
90
+ await getFormattedResponseBody(response);
91
+ }
92
+ }
81
93
  if (this.#includeConsoleData) {
82
94
  const consoleMessages = context.getConsoleData();
83
95
  if (consoleMessages) {
@@ -139,21 +151,9 @@ Call ${handleDialog.name} to handle it before continuing.`);
139
151
  }
140
152
  response.push('## Network requests');
141
153
  if (requests.length) {
142
- const paginationResult = paginate(requests, this.#networkRequestsOptions.pagination);
143
- if (paginationResult.invalidPage) {
144
- response.push('Invalid page number provided. Showing first page.');
145
- }
146
- const { startIndex, endIndex, currentPage, totalPages } = paginationResult;
147
- response.push(`Showing ${startIndex + 1}-${endIndex} of ${requests.length} (Page ${currentPage + 1} of ${totalPages}).`);
148
- if (this.#networkRequestsOptions.pagination) {
149
- if (paginationResult.hasNextPage) {
150
- response.push(`Next page: ${currentPage + 1}`);
151
- }
152
- if (paginationResult.hasPreviousPage) {
153
- response.push(`Previous page: ${currentPage - 1}`);
154
- }
155
- }
156
- for (const request of paginationResult.items) {
154
+ const data = this.#dataWithPagination(requests, this.#networkRequestsOptions.pagination);
155
+ response.push(...data.info);
156
+ for (const request of data.items) {
157
157
  response.push(getShortDescriptionForRequest(request));
158
158
  }
159
159
  }
@@ -182,9 +182,30 @@ Call ${handleDialog.name} to handle it before continuing.`);
182
182
  });
183
183
  return [text, ...images];
184
184
  }
185
+ #dataWithPagination(data, pagination) {
186
+ const response = [];
187
+ const paginationResult = paginate(data, pagination);
188
+ if (paginationResult.invalidPage) {
189
+ response.push('Invalid page number provided. Showing first page.');
190
+ }
191
+ const { startIndex, endIndex, currentPage, totalPages } = paginationResult;
192
+ response.push(`Showing ${startIndex + 1}-${endIndex} of ${data.length} (Page ${currentPage + 1} of ${totalPages}).`);
193
+ if (pagination) {
194
+ if (paginationResult.hasNextPage) {
195
+ response.push(`Next page: ${currentPage + 1}`);
196
+ }
197
+ if (paginationResult.hasPreviousPage) {
198
+ response.push(`Previous page: ${currentPage - 1}`);
199
+ }
200
+ }
201
+ return {
202
+ info: response,
203
+ items: paginationResult.items,
204
+ };
205
+ }
185
206
  #getIncludeNetworkRequestsData(context) {
186
207
  const response = [];
187
- const url = this.#attachedNetworkRequestUrl;
208
+ const url = this.#attachedNetworkRequestData?.networkRequestUrl;
188
209
  if (!url) {
189
210
  return response;
190
211
  }
@@ -195,6 +216,10 @@ Call ${handleDialog.name} to handle it before continuing.`);
195
216
  for (const line of getFormattedHeaderValue(httpRequest.headers())) {
196
217
  response.push(line);
197
218
  }
219
+ if (this.#attachedNetworkRequestData?.requestBody) {
220
+ response.push(`### Request Body`);
221
+ response.push(this.#attachedNetworkRequestData.requestBody);
222
+ }
198
223
  const httpResponse = httpRequest.response();
199
224
  if (httpResponse) {
200
225
  response.push(`### Response Headers`);
@@ -202,6 +227,10 @@ Call ${handleDialog.name} to handle it before continuing.`);
202
227
  response.push(line);
203
228
  }
204
229
  }
230
+ if (this.#attachedNetworkRequestData?.responseBody) {
231
+ response.push(`### Response Body`);
232
+ response.push(this.#attachedNetworkRequestData.responseBody);
233
+ }
205
234
  const httpFailure = httpRequest.failure();
206
235
  if (httpFailure) {
207
236
  response.push(`### Request failed with`);
@@ -6,20 +6,17 @@
6
6
  export class Mutex {
7
7
  static Guard = class Guard {
8
8
  #mutex;
9
- #onRelease;
10
- constructor(mutex, onRelease) {
9
+ constructor(mutex) {
11
10
  this.#mutex = mutex;
12
- this.#onRelease = onRelease;
13
11
  }
14
12
  dispose() {
15
- this.#onRelease?.();
16
13
  return this.#mutex.release();
17
14
  }
18
15
  };
19
16
  #locked = false;
20
17
  #acquirers = [];
21
18
  // This is FIFO.
22
- async acquire(onRelease) {
19
+ async acquire() {
23
20
  if (!this.#locked) {
24
21
  this.#locked = true;
25
22
  return new Mutex.Guard(this);
@@ -27,7 +24,7 @@ export class Mutex {
27
24
  const { resolve, promise } = Promise.withResolvers();
28
25
  this.#acquirers.push(resolve);
29
26
  await promise;
30
- return new Mutex.Guard(this, onRelease);
27
+ return new Mutex.Guard(this);
31
28
  }
32
29
  release() {
33
30
  const resolve = this.#acquirers.shift();