chrome-devtools-mcp 0.2.7 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +10 -2
- package/build/node_modules/chrome-devtools-frontend/front_end/core/host/GdpClient.js +18 -3
- package/build/node_modules/chrome-devtools-frontend/front_end/core/host/UserMetrics.js +3 -2
- package/build/node_modules/chrome-devtools-frontend/front_end/core/i18n/i18n.js +24 -0
- package/build/node_modules/chrome-devtools-frontend/front_end/core/sdk/CSSMatchedStyles.js +5 -1
- package/build/node_modules/chrome-devtools-frontend/front_end/core/sdk/EnhancedTracesParser.js +4 -5
- package/build/node_modules/chrome-devtools-frontend/front_end/core/sdk/NetworkManager.js +1 -0
- package/build/node_modules/chrome-devtools-frontend/front_end/core/sdk/NetworkRequest.js +8 -0
- package/build/node_modules/chrome-devtools-frontend/front_end/core/sdk/TargetManager.js +4 -0
- package/build/node_modules/chrome-devtools-frontend/front_end/generated/InspectorBackendCommands.js +10 -7
- package/build/node_modules/chrome-devtools-frontend/front_end/generated/SupportedCSSProperties.js +40 -11
- package/build/node_modules/chrome-devtools-frontend/front_end/models/ai_assistance/data_formatters/PerformanceInsightFormatter.js +95 -283
- package/build/node_modules/chrome-devtools-frontend/front_end/models/ai_assistance/data_formatters/PerformanceTraceFormatter.js +256 -22
- package/build/node_modules/chrome-devtools-frontend/front_end/models/ai_assistance/performance/AICallTree.js +12 -6
- package/build/node_modules/chrome-devtools-frontend/front_end/models/ai_assistance/performance/AIContext.js +64 -7
- package/build/node_modules/chrome-devtools-frontend/front_end/models/trace/EventsSerializer.js +3 -3
- package/build/node_modules/chrome-devtools-frontend/front_end/models/trace/handlers/UserInteractionsHandler.js +91 -63
- package/build/node_modules/chrome-devtools-frontend/front_end/models/trace/handlers/UserTimingsHandler.js +1 -1
- package/build/node_modules/chrome-devtools-frontend/front_end/models/trace/helpers/Timing.js +1 -1
- package/build/node_modules/chrome-devtools-frontend/front_end/models/trace/helpers/Trace.js +98 -36
- package/build/node_modules/chrome-devtools-frontend/front_end/models/trace/types/TraceEvents.js +9 -0
- package/build/src/McpContext.js +8 -2
- package/build/src/McpResponse.js +30 -3
- package/build/src/browser.js +2 -2
- package/build/src/cli.js +82 -0
- package/build/src/formatters/consoleFormatter.js +3 -1
- package/build/src/index.js +1 -1
- package/build/src/main.js +11 -84
- package/build/src/tools/ToolDefinition.js +1 -0
- package/build/src/tools/emulation.js +2 -2
- package/build/src/tools/input.js +8 -8
- package/build/src/tools/network.js +20 -4
- package/build/src/tools/pages.js +12 -2
- package/build/src/tools/performance.js +2 -2
- package/build/src/tools/screenshot.js +1 -1
- package/build/src/tools/script.js +4 -7
- package/build/src/tools/snapshot.js +2 -2
- package/build/src/trace-processing/parse.js +5 -6
- package/build/src/utils/pagination.js +49 -0
- package/package.json +9 -6
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
// Copyright 2022 The Chromium Authors
|
|
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
|
+
import * as Platform from '../../../core/platform/platform.js';
|
|
4
5
|
import * as Helpers from '../helpers/helpers.js';
|
|
5
6
|
import * as Types from '../types/types.js';
|
|
6
7
|
import { data as metaHandlerData } from './MetaHandler.js';
|
|
@@ -14,14 +15,14 @@ const INP_MEDIUM_TIMING = Helpers.Timing.milliToMicro(Types.Timing.Milli(500));
|
|
|
14
15
|
let longestInteractionEvent = null;
|
|
15
16
|
let interactionEvents = [];
|
|
16
17
|
let interactionEventsWithNoNesting = [];
|
|
17
|
-
let eventTimingEndEventsById = new Map();
|
|
18
18
|
let eventTimingStartEventsForInteractions = [];
|
|
19
|
+
let eventTimingEndEventsForInteractions = [];
|
|
19
20
|
export function reset() {
|
|
20
21
|
beginCommitCompositorFrameEvents = [];
|
|
21
22
|
parseMetaViewportEvents = [];
|
|
22
23
|
interactionEvents = [];
|
|
23
24
|
eventTimingStartEventsForInteractions = [];
|
|
24
|
-
|
|
25
|
+
eventTimingEndEventsForInteractions = [];
|
|
25
26
|
interactionEventsWithNoNesting = [];
|
|
26
27
|
longestInteractionEvent = null;
|
|
27
28
|
}
|
|
@@ -39,7 +40,7 @@ export function handleEvent(event) {
|
|
|
39
40
|
}
|
|
40
41
|
if (Types.Events.isEventTimingEnd(event)) {
|
|
41
42
|
// Store the end event; for each start event that is an interaction, we need the matching end event to calculate the duration correctly.
|
|
42
|
-
|
|
43
|
+
eventTimingEndEventsForInteractions.push(event);
|
|
43
44
|
}
|
|
44
45
|
// From this point on we want to find events that represent interactions.
|
|
45
46
|
// These events are always start events - those are the ones that contain all
|
|
@@ -111,8 +112,17 @@ export function categoryOfInteraction(interaction) {
|
|
|
111
112
|
* =======B=[keyup]=====
|
|
112
113
|
* ====C=[pointerdown]=
|
|
113
114
|
* =D=[pointerup]=
|
|
115
|
+
*
|
|
116
|
+
* Additionally, this method will also maximise the processing duration of the
|
|
117
|
+
* events that we keep as non-nested. We want to make sure we give an accurate
|
|
118
|
+
* representation of main thread activity, so if we keep an event + hide its
|
|
119
|
+
* nested children, we set the top level event's processing start &
|
|
120
|
+
* processing end to be the earliest processing start & the latest processing
|
|
121
|
+
* end of its children. This ensures we report a more accurate main thread
|
|
122
|
+
* activity time which is important as we want developers to focus on fixing
|
|
123
|
+
* this.
|
|
114
124
|
**/
|
|
115
|
-
export function
|
|
125
|
+
export function removeNestedInteractionsAndSetProcessingTime(interactions) {
|
|
116
126
|
/**
|
|
117
127
|
* Because we nest events only that are in the same category, we store the
|
|
118
128
|
* longest event for a given end time by category.
|
|
@@ -189,68 +199,86 @@ function writeSyntheticTimespans(event) {
|
|
|
189
199
|
}
|
|
190
200
|
export async function finalize() {
|
|
191
201
|
const { navigationsByFrameId } = metaHandlerData();
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
if (!type || !interactionId || !timeStamp || !processingStart || !processingEnd) {
|
|
201
|
-
// A valid interaction event that we care about has to have a type (e.g. pointerdown, keyup).
|
|
202
|
-
// We also need to ensure it has an interactionId and various timings. There are edge cases where these aren't included in the trace event.
|
|
203
|
-
continue;
|
|
202
|
+
const beginAndEndEvents = Platform.ArrayUtilities.mergeOrdered(eventTimingStartEventsForInteractions, eventTimingEndEventsForInteractions, Helpers.Trace.eventTimeComparator);
|
|
203
|
+
// Pair up the begin & end events and create synthetic user timing events.
|
|
204
|
+
const beginEventById = new Map();
|
|
205
|
+
for (const event of beginAndEndEvents) {
|
|
206
|
+
if (Types.Events.isEventTimingStart(event)) {
|
|
207
|
+
const forId = beginEventById.get(event.id) ?? [];
|
|
208
|
+
forId.push(event);
|
|
209
|
+
beginEventById.set(event.id, forId);
|
|
204
210
|
}
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
//
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
211
|
+
else if (Types.Events.isEventTimingEnd(event)) {
|
|
212
|
+
const beginEvents = beginEventById.get(event.id) ?? [];
|
|
213
|
+
const beginEvent = beginEvents.pop();
|
|
214
|
+
if (!beginEvent) {
|
|
215
|
+
continue;
|
|
216
|
+
}
|
|
217
|
+
const { type, interactionId, timeStamp, processingStart, processingEnd } = beginEvent.args.data;
|
|
218
|
+
if (!type || !interactionId || !timeStamp || !processingStart || !processingEnd) {
|
|
219
|
+
// A valid interaction event that we care about has to have a type (e.g. pointerdown, keyup).
|
|
220
|
+
// We also need to ensure it has an interactionId and various timings. There are edge cases where these aren't included in the trace event.
|
|
221
|
+
continue;
|
|
222
|
+
}
|
|
223
|
+
// In the future we will add microsecond timestamps to the trace events…
|
|
224
|
+
// (See https://source.chromium.org/chromium/chromium/src/+/main:third_party/blink/renderer/core/timing/window_performance.cc;l=900-901;drc=b503c262e425eae59ced4a80d59d176ed07152c7 )
|
|
225
|
+
// …but until then we can use the millisecond precision values that are in
|
|
226
|
+
// the trace event. To adjust them to be relative to the event.ts and the
|
|
227
|
+
// trace timestamps, for both processingStart and processingEnd we subtract
|
|
228
|
+
// the event timestamp (NOT event.ts, but the timeStamp millisecond value
|
|
229
|
+
// emitted in args.data), and then add that value to the event.ts. This
|
|
230
|
+
// will give us a processingStart and processingEnd time in microseconds
|
|
231
|
+
// that is relative to event.ts, and can be used when drawing boxes.
|
|
232
|
+
// There is some inaccuracy here as we are converting milliseconds to
|
|
233
|
+
// microseconds, but it is good enough until the backend emits more
|
|
234
|
+
// accurate numbers.
|
|
235
|
+
const processingStartRelativeToTraceTime = Types.Timing.Micro(Helpers.Timing.milliToMicro(processingStart) - Helpers.Timing.milliToMicro(timeStamp) + beginEvent.ts);
|
|
236
|
+
const processingEndRelativeToTraceTime = Types.Timing.Micro((Helpers.Timing.milliToMicro(processingEnd) - Helpers.Timing.milliToMicro(timeStamp)) + beginEvent.ts);
|
|
237
|
+
// Ultimate frameId fallback only needed for TSC, see comments in the type.
|
|
238
|
+
const frameId = beginEvent.args.frame ?? beginEvent.args.data.frame ?? '';
|
|
239
|
+
const navigation = Helpers.Trace.getNavigationForTraceEvent(beginEvent, frameId, navigationsByFrameId);
|
|
240
|
+
const navigationId = navigation?.args.data?.navigationId;
|
|
241
|
+
const interactionEvent = Helpers.SyntheticEvents.SyntheticEventsManager.registerSyntheticEvent({
|
|
242
|
+
// Use the start event to define the common fields.
|
|
243
|
+
rawSourceEvent: beginEvent,
|
|
244
|
+
cat: beginEvent.cat,
|
|
245
|
+
name: beginEvent.name,
|
|
246
|
+
pid: beginEvent.pid,
|
|
247
|
+
tid: beginEvent.tid,
|
|
248
|
+
ph: beginEvent.ph,
|
|
249
|
+
processingStart: processingStartRelativeToTraceTime,
|
|
250
|
+
processingEnd: processingEndRelativeToTraceTime,
|
|
251
|
+
// These will be set in writeSyntheticTimespans()
|
|
252
|
+
inputDelay: Types.Timing.Micro(-1),
|
|
253
|
+
mainThreadHandling: Types.Timing.Micro(-1),
|
|
254
|
+
presentationDelay: Types.Timing.Micro(-1),
|
|
255
|
+
args: {
|
|
256
|
+
data: {
|
|
257
|
+
beginEvent,
|
|
258
|
+
endEvent: event,
|
|
259
|
+
frame: frameId,
|
|
260
|
+
navigationId,
|
|
261
|
+
},
|
|
243
262
|
},
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
263
|
+
ts: beginEvent.ts,
|
|
264
|
+
dur: Types.Timing.Micro(event.ts - beginEvent.ts),
|
|
265
|
+
type: beginEvent.args.data.type,
|
|
266
|
+
interactionId: beginEvent.args.data.interactionId,
|
|
267
|
+
});
|
|
268
|
+
writeSyntheticTimespans(interactionEvent);
|
|
269
|
+
interactionEvents.push(interactionEvent);
|
|
270
|
+
}
|
|
252
271
|
}
|
|
253
|
-
|
|
272
|
+
// Once we gather up all the interactions, we want to remove nested
|
|
273
|
+
// interactions. Interactions can be nested because one user action (e.g. a
|
|
274
|
+
// click) will cause a pointerdown, pointerup and click. But we don't want to
|
|
275
|
+
// fill the interactions track with lots of noise. To fix this, we go through
|
|
276
|
+
// all the events and remove any nested ones so on the timeline we focus the
|
|
277
|
+
// user on the most important events, which we define as the longest one. But
|
|
278
|
+
// this algorithm assumes the events are in ASC order, so we first sort the
|
|
279
|
+
// set of interactions.
|
|
280
|
+
Helpers.Trace.sortTraceEventsInPlace(interactionEvents);
|
|
281
|
+
interactionEventsWithNoNesting.push(...removeNestedInteractionsAndSetProcessingTime(interactionEvents));
|
|
254
282
|
// Pick the longest interactions from the set that were not nested, as we
|
|
255
283
|
// know those are the set of the largest interactions.
|
|
256
284
|
for (const interactionEvent of interactionEventsWithNoNesting) {
|
|
@@ -174,8 +174,8 @@ export async function finalize() {
|
|
|
174
174
|
}
|
|
175
175
|
export function data() {
|
|
176
176
|
return {
|
|
177
|
-
performanceMeasures: syntheticEvents.filter(e => e.cat === 'blink.user_timing'),
|
|
178
177
|
consoleTimings: syntheticEvents.filter(e => e.cat === 'blink.console'),
|
|
178
|
+
performanceMeasures: syntheticEvents.filter(e => e.cat === 'blink.user_timing'),
|
|
179
179
|
performanceMarks: performanceMarkEvents,
|
|
180
180
|
timestampEvents,
|
|
181
181
|
measureTraceByTraceId,
|
package/build/node_modules/chrome-devtools-frontend/front_end/models/trace/helpers/Timing.js
CHANGED
|
@@ -167,7 +167,7 @@ export function boundsIncludeTimeRange(data) {
|
|
|
167
167
|
/** Checks to see if the event is within or overlaps the bounds */
|
|
168
168
|
export function eventIsInBounds(event, bounds) {
|
|
169
169
|
const startTime = event.ts;
|
|
170
|
-
return startTime <= bounds.max && bounds.min
|
|
170
|
+
return startTime <= bounds.max && bounds.min < (startTime + (event.dur ?? 0));
|
|
171
171
|
}
|
|
172
172
|
export function timestampIsInBounds(bounds, timestamp) {
|
|
173
173
|
return timestamp >= bounds.min && timestamp <= bounds.max;
|
|
@@ -242,52 +242,109 @@ export function makeProfileCall(node, profileId, sampleIndex, ts, pid, tid) {
|
|
|
242
242
|
};
|
|
243
243
|
}
|
|
244
244
|
/**
|
|
245
|
-
* Matches beginning events with PairableAsyncEnd and PairableAsyncInstant
|
|
246
|
-
* if provided
|
|
247
|
-
* account for that.
|
|
245
|
+
* Matches beginning events with PairableAsyncEnd and PairableAsyncInstant
|
|
246
|
+
* if provided. Traces may contain multiple instant events so we need to
|
|
247
|
+
* account for that. Additionally we have seen cases where we might only have a
|
|
248
|
+
* begin event & instant event(s), with no end event. So we account for that
|
|
249
|
+
* situation also.
|
|
248
250
|
*
|
|
249
|
-
*
|
|
251
|
+
* You might also like to read the models/trace/README.md which has some
|
|
252
|
+
* documentation on trace IDs. This is important as Perfetto will reuse trace
|
|
253
|
+
* IDs when emitting events (if they do not overlap). This means it's not as
|
|
254
|
+
* simple as grouping events by IDs. Instead, we group begin & instant events
|
|
255
|
+
* by ID as we find them. When we find end events, we then pop any matching
|
|
256
|
+
* begin/instant events off the stack and group those. That way, if we meet the
|
|
257
|
+
* same ID later on it doesn't cause us collisions.
|
|
258
|
+
*
|
|
259
|
+
* @returns An array of all the matched event groups, along with their ID. Note
|
|
260
|
+
* that two event groups can have the same ID if they were non-overlapping
|
|
261
|
+
* events. You cannot rely on ID being unique across a trace. The returned set
|
|
262
|
+
* of groups are NOT SORTED in any order.
|
|
250
263
|
*/
|
|
251
|
-
|
|
264
|
+
function matchEvents(unpairedEvents) {
|
|
265
|
+
sortTraceEventsInPlace(unpairedEvents);
|
|
252
266
|
// map to store begin and end of the event
|
|
253
|
-
const
|
|
254
|
-
|
|
267
|
+
const matches = [];
|
|
268
|
+
const beginEventsById = new Map();
|
|
269
|
+
const instantEventsById = new Map();
|
|
255
270
|
for (const event of unpairedEvents) {
|
|
256
|
-
const
|
|
257
|
-
if (
|
|
271
|
+
const id = getSyntheticId(event);
|
|
272
|
+
if (id === undefined) {
|
|
258
273
|
continue;
|
|
259
274
|
}
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
if (!otherEventsWithID.instant) {
|
|
277
|
-
otherEventsWithID.instant = [];
|
|
275
|
+
if (Types.Events.isPairableAsyncBegin(event)) {
|
|
276
|
+
const existingEvents = beginEventsById.get(id) ?? [];
|
|
277
|
+
existingEvents.push(event);
|
|
278
|
+
beginEventsById.set(id, existingEvents);
|
|
279
|
+
}
|
|
280
|
+
else if (Types.Events.isPairableAsyncInstant(event)) {
|
|
281
|
+
const existingEvents = instantEventsById.get(id) ?? [];
|
|
282
|
+
existingEvents.push(event);
|
|
283
|
+
instantEventsById.set(id, existingEvents);
|
|
284
|
+
}
|
|
285
|
+
else if (Types.Events.isPairableAsyncEnd(event)) {
|
|
286
|
+
// Find matching begin event by ID
|
|
287
|
+
const beginEventsWithMatchingId = beginEventsById.get(id) ?? [];
|
|
288
|
+
const beginEvent = beginEventsWithMatchingId.pop();
|
|
289
|
+
if (!beginEvent) {
|
|
290
|
+
continue;
|
|
278
291
|
}
|
|
279
|
-
|
|
292
|
+
const instantEventsWithMatchingId = instantEventsById.get(id) ?? [];
|
|
293
|
+
// Find all instant events after the begin event ts.
|
|
294
|
+
const instantEventsForThisGroup = [];
|
|
295
|
+
while (instantEventsWithMatchingId.length > 0) {
|
|
296
|
+
if (instantEventsWithMatchingId[0].ts >= beginEvent.ts) {
|
|
297
|
+
const event = instantEventsWithMatchingId.pop();
|
|
298
|
+
if (event) {
|
|
299
|
+
instantEventsForThisGroup.push(event);
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
else {
|
|
303
|
+
break;
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
const matchingGroup = {
|
|
307
|
+
begin: beginEvent,
|
|
308
|
+
end: event,
|
|
309
|
+
instant: instantEventsForThisGroup,
|
|
310
|
+
syntheticId: id,
|
|
311
|
+
};
|
|
312
|
+
matches.push(matchingGroup);
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
// At this point we know we have paired up all the Begin & End & Instant
|
|
316
|
+
// events. But it is possible to see only begin & instant events with the
|
|
317
|
+
// same ID, and no end event. So now we do a second pass through our begin
|
|
318
|
+
// events to find any that did not have an end event. If we find some
|
|
319
|
+
// instant events for the begin event, we create a new group.
|
|
320
|
+
// Also, because there were no end events, we know that the IDs will be
|
|
321
|
+
// unique now; e.g. each key in the map should have no more than one item in
|
|
322
|
+
// it.
|
|
323
|
+
for (const [id, beginEvents] of beginEventsById) {
|
|
324
|
+
const beginEvent = beginEvents.pop();
|
|
325
|
+
if (!beginEvent) {
|
|
326
|
+
continue;
|
|
327
|
+
}
|
|
328
|
+
const matchingInstantEvents = instantEventsById.get(id);
|
|
329
|
+
if (matchingInstantEvents?.length) {
|
|
330
|
+
matches.push({
|
|
331
|
+
syntheticId: id,
|
|
332
|
+
begin: beginEvent,
|
|
333
|
+
end: null,
|
|
334
|
+
instant: matchingInstantEvents,
|
|
335
|
+
});
|
|
280
336
|
}
|
|
281
337
|
}
|
|
282
|
-
return
|
|
338
|
+
return matches;
|
|
283
339
|
}
|
|
284
|
-
function getSyntheticId(event) {
|
|
340
|
+
export function getSyntheticId(event) {
|
|
285
341
|
const id = extractId(event);
|
|
286
342
|
return id && `${event.cat}:${id}:${event.name}`;
|
|
287
343
|
}
|
|
288
|
-
|
|
344
|
+
function createSortedSyntheticEvents(matchedPairs) {
|
|
289
345
|
const syntheticEvents = [];
|
|
290
|
-
for (const
|
|
346
|
+
for (const eventsTriplet of matchedPairs) {
|
|
347
|
+
const id = eventsTriplet.syntheticId;
|
|
291
348
|
const beginEvent = eventsTriplet.begin;
|
|
292
349
|
const endEvent = eventsTriplet.end;
|
|
293
350
|
const instantEvents = eventsTriplet.instant;
|
|
@@ -335,14 +392,19 @@ export function createSortedSyntheticEvents(matchedPairs, syntheticEventCallback
|
|
|
335
392
|
// crbug.com/1472375
|
|
336
393
|
continue;
|
|
337
394
|
}
|
|
338
|
-
syntheticEventCallback?.(event);
|
|
339
395
|
syntheticEvents.push(event);
|
|
340
396
|
}
|
|
341
|
-
|
|
397
|
+
sortTraceEventsInPlace(syntheticEvents);
|
|
398
|
+
return syntheticEvents;
|
|
342
399
|
}
|
|
343
|
-
|
|
400
|
+
/**
|
|
401
|
+
* Groups up sets of async events into synthetic events.
|
|
402
|
+
* @param unpairedAsyncEvents the raw array of begin, end and async instant
|
|
403
|
+
* events. These MUST be sorted in timestamp ASC order.
|
|
404
|
+
*/
|
|
405
|
+
export function createMatchedSortedSyntheticEvents(unpairedAsyncEvents) {
|
|
344
406
|
const matchedPairs = matchEvents(unpairedAsyncEvents);
|
|
345
|
-
const syntheticEvents = createSortedSyntheticEvents(matchedPairs
|
|
407
|
+
const syntheticEvents = createSortedSyntheticEvents(matchedPairs);
|
|
346
408
|
return syntheticEvents;
|
|
347
409
|
}
|
|
348
410
|
/**
|
package/build/node_modules/chrome-devtools-frontend/front_end/models/trace/types/TraceEvents.js
CHANGED
|
@@ -93,6 +93,15 @@ export function isRenderFrameImplCreateChildFrame(event) {
|
|
|
93
93
|
export function isLayoutImageUnsized(event) {
|
|
94
94
|
return event.name === "LayoutImageUnsized" /* Name.LAYOUT_IMAGE_UNSIZED */;
|
|
95
95
|
}
|
|
96
|
+
export function isPairableAsyncBegin(e) {
|
|
97
|
+
return e.ph === "b" /* Phase.ASYNC_NESTABLE_START */;
|
|
98
|
+
}
|
|
99
|
+
export function isPairableAsyncEnd(e) {
|
|
100
|
+
return e.ph === "e" /* Phase.ASYNC_NESTABLE_END */;
|
|
101
|
+
}
|
|
102
|
+
export function isPairableAsyncInstant(e) {
|
|
103
|
+
return e.ph === "n" /* Phase.ASYNC_NESTABLE_INSTANT */;
|
|
104
|
+
}
|
|
96
105
|
export function isAnimationFrameAsyncStart(data) {
|
|
97
106
|
return data.name === "AnimationFrame" /* Name.ANIMATION_FRAME */ && data.ph === "b" /* Phase.ASYNC_NESTABLE_START */;
|
|
98
107
|
}
|
package/build/src/McpContext.js
CHANGED
|
@@ -1,8 +1,14 @@
|
|
|
1
|
-
|
|
1
|
+
/**
|
|
2
|
+
* @license
|
|
3
|
+
* Copyright 2025 Google LLC
|
|
4
|
+
* SPDX-License-Identifier: Apache-2.0
|
|
5
|
+
*/
|
|
2
6
|
import fs from 'node:fs/promises';
|
|
3
7
|
import os from 'node:os';
|
|
4
8
|
import path from 'node:path';
|
|
9
|
+
import { NetworkCollector, PageCollector } from './PageCollector.js';
|
|
5
10
|
import { listPages } from './tools/pages.js';
|
|
11
|
+
import { CLOSE_PAGE_ERROR } from './tools/ToolDefinition.js';
|
|
6
12
|
import { WaitForHelper } from './WaitForHelper.js';
|
|
7
13
|
const DEFAULT_TIMEOUT = 5_000;
|
|
8
14
|
const NAVIGATION_TIMEOUT = 10_000;
|
|
@@ -82,7 +88,7 @@ export class McpContext {
|
|
|
82
88
|
}
|
|
83
89
|
async closePage(pageIdx) {
|
|
84
90
|
if (this.#pages.length === 1) {
|
|
85
|
-
throw new Error(
|
|
91
|
+
throw new Error(CLOSE_PAGE_ERROR);
|
|
86
92
|
}
|
|
87
93
|
const page = this.getPageByIdx(pageIdx);
|
|
88
94
|
this.setSelectedPageIdx(0);
|
package/build/src/McpResponse.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
|
+
import { formatConsoleEvent } from './formatters/consoleFormatter.js';
|
|
1
2
|
import { getFormattedHeaderValue, getShortDescriptionForRequest, getStatusFromRequest, } from './formatters/networkFormatter.js';
|
|
2
3
|
import { formatA11ySnapshot } from './formatters/snapshotFormatter.js';
|
|
3
|
-
import {
|
|
4
|
+
import { paginate } from './utils/pagination.js';
|
|
4
5
|
export class McpResponse {
|
|
5
6
|
#includePages = false;
|
|
6
7
|
#includeSnapshot = false;
|
|
@@ -10,14 +11,23 @@ export class McpResponse {
|
|
|
10
11
|
#textResponseLines = [];
|
|
11
12
|
#formattedConsoleData;
|
|
12
13
|
#images = [];
|
|
14
|
+
#networkRequestsPaginationOptions;
|
|
13
15
|
setIncludePages(value) {
|
|
14
16
|
this.#includePages = value;
|
|
15
17
|
}
|
|
16
18
|
setIncludeSnapshot(value) {
|
|
17
19
|
this.#includeSnapshot = value;
|
|
18
20
|
}
|
|
19
|
-
setIncludeNetworkRequests(value) {
|
|
21
|
+
setIncludeNetworkRequests(value, options) {
|
|
20
22
|
this.#includeNetworkRequests = value;
|
|
23
|
+
if (!value || !options) {
|
|
24
|
+
this.#networkRequestsPaginationOptions = undefined;
|
|
25
|
+
return;
|
|
26
|
+
}
|
|
27
|
+
this.#networkRequestsPaginationOptions = {
|
|
28
|
+
pageSize: options.pageSize,
|
|
29
|
+
pageIdx: options.pageIdx,
|
|
30
|
+
};
|
|
21
31
|
}
|
|
22
32
|
setIncludeConsoleData(value) {
|
|
23
33
|
this.#includeConsoleData = value;
|
|
@@ -37,6 +47,9 @@ export class McpResponse {
|
|
|
37
47
|
get attachedNetworkRequestUrl() {
|
|
38
48
|
return this.#attachedNetworkRequestUrl;
|
|
39
49
|
}
|
|
50
|
+
get networkRequestsPageIdx() {
|
|
51
|
+
return this.#networkRequestsPaginationOptions?.pageIdx;
|
|
52
|
+
}
|
|
40
53
|
appendResponseLine(value) {
|
|
41
54
|
this.#textResponseLines.push(value);
|
|
42
55
|
}
|
|
@@ -113,7 +126,21 @@ Call browser_handle_dialog to handle it before continuing.`);
|
|
|
113
126
|
const requests = context.getNetworkRequests();
|
|
114
127
|
response.push('## Network requests');
|
|
115
128
|
if (requests.length) {
|
|
116
|
-
|
|
129
|
+
const paginationResult = paginate(requests, this.#networkRequestsPaginationOptions);
|
|
130
|
+
if (paginationResult.invalidPage) {
|
|
131
|
+
response.push('Invalid page number provided. Showing first page.');
|
|
132
|
+
}
|
|
133
|
+
const { startIndex, endIndex, currentPage, totalPages } = paginationResult;
|
|
134
|
+
response.push(`Showing ${startIndex + 1}-${endIndex} of ${requests.length} (Page ${currentPage + 1} of ${totalPages}).`);
|
|
135
|
+
if (this.#networkRequestsPaginationOptions) {
|
|
136
|
+
if (paginationResult.hasNextPage) {
|
|
137
|
+
response.push(`Next page: ${currentPage + 1}`);
|
|
138
|
+
}
|
|
139
|
+
if (paginationResult.hasPreviousPage) {
|
|
140
|
+
response.push(`Previous page: ${currentPage - 1}`);
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
for (const request of paginationResult.items) {
|
|
117
144
|
response.push(getShortDescriptionForRequest(request));
|
|
118
145
|
}
|
|
119
146
|
}
|
package/build/src/browser.js
CHANGED
|
@@ -3,10 +3,10 @@
|
|
|
3
3
|
* Copyright 2025 Google LLC
|
|
4
4
|
* SPDX-License-Identifier: Apache-2.0
|
|
5
5
|
*/
|
|
6
|
-
import
|
|
6
|
+
import fs from 'node:fs';
|
|
7
7
|
import os from 'node:os';
|
|
8
8
|
import path from 'node:path';
|
|
9
|
-
import
|
|
9
|
+
import puppeteer from 'puppeteer-core';
|
|
10
10
|
let browser;
|
|
11
11
|
const ignoredPrefixes = new Set([
|
|
12
12
|
'chrome://',
|
package/build/src/cli.js
ADDED
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @license
|
|
3
|
+
* Copyright 2025 Google LLC
|
|
4
|
+
* SPDX-License-Identifier: Apache-2.0
|
|
5
|
+
*/
|
|
6
|
+
import yargs from 'yargs';
|
|
7
|
+
import { hideBin } from 'yargs/helpers';
|
|
8
|
+
export const cliOptions = {
|
|
9
|
+
browserUrl: {
|
|
10
|
+
type: 'string',
|
|
11
|
+
description: 'Connect to a running Chrome instance using port forwarding. For more details see: https://developer.chrome.com/docs/devtools/remote-debugging/local-server.',
|
|
12
|
+
alias: 'u',
|
|
13
|
+
coerce: (url) => {
|
|
14
|
+
new URL(url);
|
|
15
|
+
return url;
|
|
16
|
+
},
|
|
17
|
+
},
|
|
18
|
+
headless: {
|
|
19
|
+
type: 'boolean',
|
|
20
|
+
description: 'Whether to run in headless (no UI) mode.',
|
|
21
|
+
default: false,
|
|
22
|
+
},
|
|
23
|
+
executablePath: {
|
|
24
|
+
type: 'string',
|
|
25
|
+
description: 'Path to custom Chrome executable.',
|
|
26
|
+
conflicts: 'browserUrl',
|
|
27
|
+
alias: 'e',
|
|
28
|
+
},
|
|
29
|
+
isolated: {
|
|
30
|
+
type: 'boolean',
|
|
31
|
+
description: 'If specified, creates a temporary user-data-dir that is automatically cleaned up after the browser is closed.',
|
|
32
|
+
default: false,
|
|
33
|
+
},
|
|
34
|
+
customDevtools: {
|
|
35
|
+
type: 'string',
|
|
36
|
+
description: 'Path to custom DevTools.',
|
|
37
|
+
hidden: true,
|
|
38
|
+
conflicts: 'browserUrl',
|
|
39
|
+
alias: 'd',
|
|
40
|
+
},
|
|
41
|
+
channel: {
|
|
42
|
+
type: 'string',
|
|
43
|
+
description: 'Specify a different Chrome channel that should be used. The default is the stable channel version.',
|
|
44
|
+
choices: ['stable', 'canary', 'beta', 'dev'],
|
|
45
|
+
conflicts: ['browserUrl', 'executablePath'],
|
|
46
|
+
},
|
|
47
|
+
logFile: {
|
|
48
|
+
type: 'string',
|
|
49
|
+
describe: 'Save the logs to file.',
|
|
50
|
+
hidden: true,
|
|
51
|
+
},
|
|
52
|
+
};
|
|
53
|
+
export function parseArguments(version, argv = process.argv) {
|
|
54
|
+
const yargsInstance = yargs(hideBin(argv))
|
|
55
|
+
.scriptName('npx chrome-devtools-mcp@latest')
|
|
56
|
+
.options(cliOptions)
|
|
57
|
+
.check(args => {
|
|
58
|
+
// We can't set default in the options else
|
|
59
|
+
// Yargs will complain
|
|
60
|
+
if (!args.channel && !args.browserUrl && !args.executablePath) {
|
|
61
|
+
args.channel = 'stable';
|
|
62
|
+
}
|
|
63
|
+
return true;
|
|
64
|
+
})
|
|
65
|
+
.example([
|
|
66
|
+
[
|
|
67
|
+
'$0 --browserUrl http://127.0.0.1:9222',
|
|
68
|
+
'Connect to an existing browser instance',
|
|
69
|
+
],
|
|
70
|
+
['$0 --channel beta', 'Use Chrome Beta installed on this system'],
|
|
71
|
+
['$0 --channel canary', 'Use Chrome Canary installed on this system'],
|
|
72
|
+
['$0 --channel dev', 'Use Chrome Dev installed on this system'],
|
|
73
|
+
['$0 --channel stable', 'Use stable Chrome installed on this system'],
|
|
74
|
+
['$0 --logFile /tmp/log.txt', 'Save logs to a file'],
|
|
75
|
+
['$0 --help', 'Print CLI options'],
|
|
76
|
+
]);
|
|
77
|
+
return yargsInstance
|
|
78
|
+
.wrap(Math.min(120, yargsInstance.terminalWidth()))
|
|
79
|
+
.help()
|
|
80
|
+
.version(version)
|
|
81
|
+
.parseSync();
|
|
82
|
+
}
|
|
@@ -55,7 +55,9 @@ async function formatConsoleMessage(msg) {
|
|
|
55
55
|
return `${logLevel}> ${formatStackFrame(msg.location())}: ${text} ${formattedArgs}`.trim();
|
|
56
56
|
}
|
|
57
57
|
async function formatArgs(args) {
|
|
58
|
-
const argValues = await Promise.all(args.map(arg => arg.jsonValue().catch(() => {
|
|
58
|
+
const argValues = await Promise.all(args.map(arg => arg.jsonValue().catch(() => {
|
|
59
|
+
// Ignore errors
|
|
60
|
+
})));
|
|
59
61
|
return argValues
|
|
60
62
|
.map(value => {
|
|
61
63
|
return typeof value === 'object' ? JSON.stringify(value) : String(value);
|
package/build/src/index.js
CHANGED
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
*/
|
|
7
7
|
const [major, minor] = process.version.substring(1).split('.').map(Number);
|
|
8
8
|
if (major < 22 || (major === 22 && minor < 12)) {
|
|
9
|
-
console.error(`ERROR: \`chrome-devtools-mcp\` does not support Node ${process.version}. Please upgrade to Node 22
|
|
9
|
+
console.error(`ERROR: \`chrome-devtools-mcp\` does not support Node ${process.version}. Please upgrade to Node 22.12.0 or newer.`);
|
|
10
10
|
process.exit(1);
|
|
11
11
|
}
|
|
12
12
|
await import('./main.js');
|