chrome-devtools-mcp 1.1.1 → 1.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (109) hide show
  1. package/README.md +33 -4
  2. package/build/src/HeapSnapshotManager.js +30 -16
  3. package/build/src/McpContext.js +63 -9
  4. package/build/src/McpPage.js +9 -9
  5. package/build/src/McpResponse.js +176 -58
  6. package/build/src/PageCollector.js +8 -8
  7. package/build/src/ServiceWorkerCollector.js +171 -0
  8. package/build/src/TextSnapshot.js +1 -1
  9. package/build/src/ToolHandler.js +10 -4
  10. package/build/src/WaitForHelper.js +2 -2
  11. package/build/src/bin/chrome-devtools-cli-options.js +106 -4
  12. package/build/src/bin/chrome-devtools-mcp-cli-options.js +64 -5
  13. package/build/src/bin/chrome-devtools-mcp-main.js +5 -5
  14. package/build/src/browser.js +8 -4
  15. package/build/src/daemon/client.js +7 -7
  16. package/build/src/daemon/daemon.js +12 -12
  17. package/build/src/daemon/utils.js +2 -2
  18. package/build/src/{DevToolsConnectionAdapter.js → devtools/DevToolsConnectionAdapter.js} +1 -1
  19. package/build/src/{DevtoolsUtils.js → devtools/DevtoolsUtils.js} +79 -18
  20. package/build/src/devtools/McpHostBindingAdapter.js +165 -0
  21. package/build/src/formatters/ConsoleFormatter.js +1 -1
  22. package/build/src/formatters/HeapSnapshotFormatter.js +22 -0
  23. package/build/src/formatters/IssueFormatter.js +4 -4
  24. package/build/src/index.js +13 -1
  25. package/build/src/telemetry/ClearcutLogger.js +1 -1
  26. package/build/src/telemetry/WatchdogClient.js +4 -4
  27. package/build/src/telemetry/persistence.js +2 -2
  28. package/build/src/telemetry/watchdog/ClearcutSender.js +10 -10
  29. package/build/src/telemetry/watchdog/main.js +5 -5
  30. package/build/src/third_party/THIRD_PARTY_NOTICES +31 -1
  31. package/build/src/third_party/bundled-packages.json +2 -1
  32. package/build/src/third_party/devtools-formatter-worker.js +1 -1
  33. package/build/src/third_party/devtools-heap-snapshot-worker.js +161 -3
  34. package/build/src/third_party/index.js +7020 -5774
  35. package/build/src/third_party/issue-descriptions/emailVerificationRequestAccountsEmptyList.md +1 -0
  36. package/build/src/third_party/issue-descriptions/emailVerificationRequestAccountsHttpNotFound.md +1 -0
  37. package/build/src/third_party/issue-descriptions/emailVerificationRequestAccountsInvalidContentType.md +1 -0
  38. package/build/src/third_party/issue-descriptions/emailVerificationRequestAccountsInvalidResponse.md +1 -0
  39. package/build/src/third_party/issue-descriptions/emailVerificationRequestAccountsNoResponse.md +1 -0
  40. package/build/src/third_party/issue-descriptions/emailVerificationRequestDnsFetchFailed.md +1 -0
  41. package/build/src/third_party/issue-descriptions/emailVerificationRequestDnsInvalidRecord.md +1 -0
  42. package/build/src/third_party/issue-descriptions/emailVerificationRequestEmailVerificationWellKnownHttpNotFound.md +1 -0
  43. package/build/src/third_party/issue-descriptions/emailVerificationRequestEmailVerificationWellKnownInvalidContentType.md +1 -0
  44. package/build/src/third_party/issue-descriptions/emailVerificationRequestEmailVerificationWellKnownInvalidResponse.md +1 -0
  45. package/build/src/third_party/issue-descriptions/emailVerificationRequestEmailVerificationWellKnownNoResponse.md +1 -0
  46. package/build/src/third_party/issue-descriptions/emailVerificationRequestInvalidEmail.md +1 -0
  47. package/build/src/third_party/issue-descriptions/emailVerificationRequestJwksHttpNotFound.md +1 -0
  48. package/build/src/third_party/issue-descriptions/emailVerificationRequestJwksInvalidResponse.md +1 -0
  49. package/build/src/third_party/issue-descriptions/emailVerificationRequestKeyBindingSigningFailed.md +1 -0
  50. package/build/src/third_party/issue-descriptions/emailVerificationRequestRpOriginIsOpaque.md +1 -0
  51. package/build/src/third_party/issue-descriptions/emailVerificationRequestTokenHttpNotFound.md +1 -0
  52. package/build/src/third_party/issue-descriptions/emailVerificationRequestTokenInvalidContentType.md +1 -0
  53. package/build/src/third_party/issue-descriptions/emailVerificationRequestTokenInvalidResponse.md +1 -0
  54. package/build/src/third_party/issue-descriptions/emailVerificationRequestTokenInvalidSdJwt.md +1 -0
  55. package/build/src/third_party/issue-descriptions/emailVerificationRequestTokenMalformedSdJwt.md +1 -0
  56. package/build/src/third_party/issue-descriptions/emailVerificationRequestTokenNoResponse.md +1 -0
  57. package/build/src/third_party/issue-descriptions/emailVerificationRequestTokenVerificationKbInvalidAudience.md +1 -0
  58. package/build/src/third_party/issue-descriptions/emailVerificationRequestTokenVerificationKbInvalidIssuedAt.md +1 -0
  59. package/build/src/third_party/issue-descriptions/emailVerificationRequestTokenVerificationKbInvalidNonce.md +1 -0
  60. package/build/src/third_party/issue-descriptions/emailVerificationRequestTokenVerificationKbInvalidSdHash.md +1 -0
  61. package/build/src/third_party/issue-descriptions/emailVerificationRequestTokenVerificationKbInvalidTyp.md +1 -0
  62. package/build/src/third_party/issue-descriptions/emailVerificationRequestTokenVerificationKbMissingAud.md +1 -0
  63. package/build/src/third_party/issue-descriptions/emailVerificationRequestTokenVerificationKbMissingCnf.md +1 -0
  64. package/build/src/third_party/issue-descriptions/emailVerificationRequestTokenVerificationKbMissingIat.md +1 -0
  65. package/build/src/third_party/issue-descriptions/emailVerificationRequestTokenVerificationKbMissingNonce.md +1 -0
  66. package/build/src/third_party/issue-descriptions/emailVerificationRequestTokenVerificationKbMissingSdHash.md +1 -0
  67. package/build/src/third_party/issue-descriptions/emailVerificationRequestTokenVerificationKbSignatureFailed.md +1 -0
  68. package/build/src/third_party/issue-descriptions/emailVerificationRequestTokenVerificationSdJwtInvalidEmail.md +1 -0
  69. package/build/src/third_party/issue-descriptions/emailVerificationRequestTokenVerificationSdJwtInvalidEmailVerified.md +1 -0
  70. package/build/src/third_party/issue-descriptions/emailVerificationRequestTokenVerificationSdJwtInvalidHolderKey.md +1 -0
  71. package/build/src/third_party/issue-descriptions/emailVerificationRequestTokenVerificationSdJwtInvalidIssuedAt.md +1 -0
  72. package/build/src/third_party/issue-descriptions/emailVerificationRequestTokenVerificationSdJwtInvalidIssuer.md +1 -0
  73. package/build/src/third_party/issue-descriptions/emailVerificationRequestTokenVerificationSdJwtJwksMissingKeys.md +1 -0
  74. package/build/src/third_party/issue-descriptions/emailVerificationRequestTokenVerificationSdJwtMissingCnf.md +1 -0
  75. package/build/src/third_party/issue-descriptions/emailVerificationRequestTokenVerificationSdJwtMissingEmail.md +1 -0
  76. package/build/src/third_party/issue-descriptions/emailVerificationRequestTokenVerificationSdJwtMissingIat.md +1 -0
  77. package/build/src/third_party/issue-descriptions/emailVerificationRequestTokenVerificationSdJwtMissingIss.md +1 -0
  78. package/build/src/third_party/issue-descriptions/emailVerificationRequestTokenVerificationSdJwtSignatureFailed.md +1 -0
  79. package/build/src/third_party/issue-descriptions/emailVerificationRequestTokenVerificationSdJwtUnsupportedHeaderAlg.md +1 -0
  80. package/build/src/third_party/issue-descriptions/emailVerificationRequestUserLoggedOut.md +1 -0
  81. package/build/src/third_party/issue-descriptions/emailVerificationRequestWellKnownAccountsEndpointCrossOrigin.md +1 -0
  82. package/build/src/third_party/issue-descriptions/emailVerificationRequestWellKnownHttpNotFound.md +1 -0
  83. package/build/src/third_party/issue-descriptions/emailVerificationRequestWellKnownInvalidContentType.md +1 -0
  84. package/build/src/third_party/issue-descriptions/emailVerificationRequestWellKnownInvalidResponse.md +1 -0
  85. package/build/src/third_party/issue-descriptions/emailVerificationRequestWellKnownIssuanceEndpointCrossOrigin.md +1 -0
  86. package/build/src/third_party/issue-descriptions/emailVerificationRequestWellKnownListEmpty.md +1 -0
  87. package/build/src/third_party/issue-descriptions/emailVerificationRequestWellKnownMissingAccountsEndpoint.md +1 -0
  88. package/build/src/third_party/issue-descriptions/emailVerificationRequestWellKnownMissingIssuanceEndpoint.md +1 -0
  89. package/build/src/third_party/issue-descriptions/emailVerificationRequestWellKnownNoResponse.md +1 -0
  90. package/build/src/third_party/issue-descriptions/emailVerificationRequestWellKnownUnsupportedSigningAlgorithm.md +1 -0
  91. package/build/src/tools/console.js +7 -0
  92. package/build/src/tools/emulation.js +1 -0
  93. package/build/src/tools/extensions.js +5 -2
  94. package/build/src/tools/input.js +11 -3
  95. package/build/src/tools/lighthouse.js +11 -6
  96. package/build/src/tools/memory.js +109 -10
  97. package/build/src/tools/network.js +2 -2
  98. package/build/src/tools/pages.js +13 -5
  99. package/build/src/tools/performance.js +8 -7
  100. package/build/src/tools/screencast.js +32 -10
  101. package/build/src/tools/screenshot.js +158 -76
  102. package/build/src/tools/script.js +1 -1
  103. package/build/src/tools/slim/tools.js +3 -0
  104. package/build/src/tools/snapshot.js +3 -2
  105. package/build/src/tools/thirdPartyDeveloper.js +12 -2
  106. package/build/src/tools/webmcp.js +2 -0
  107. package/build/src/trace-processing/parse.js +5 -5
  108. package/build/src/version.js +1 -1
  109. package/package.json +4 -3
@@ -11,7 +11,7 @@ import { NetworkFormatter } from './formatters/NetworkFormatter.js';
11
11
  import { SnapshotFormatter } from './formatters/SnapshotFormatter.js';
12
12
  import { UncaughtError } from './PageCollector.js';
13
13
  import { TextSnapshot } from './TextSnapshot.js';
14
- import { DevTools } from './third_party/index.js';
14
+ import { DevTools, toonEncode } from './third_party/index.js';
15
15
  import { handleDialog } from './tools/pages.js';
16
16
  import { getInsightOutput, getTraceSummary } from './trace-processing/parse.js';
17
17
  import { paginate } from './utils/pagination.js';
@@ -61,7 +61,7 @@ export function replaceHtmlElementsWithUids(schema) {
61
61
  }
62
62
  }
63
63
  }
64
- async function getToolGroup(page) {
64
+ async function getToolGroups(page) {
65
65
  // Check if there is a `devtoolstooldiscovery` event listener
66
66
  const windowHandle = await page.pptrPage.evaluateHandle(() => window);
67
67
  // @ts-expect-error internal API
@@ -70,43 +70,82 @@ async function getToolGroup(page) {
70
70
  objectId: windowHandle.remoteObject().objectId,
71
71
  });
72
72
  if (listeners.find(l => l.type === 'devtoolstooldiscovery') === undefined) {
73
- return;
73
+ return [];
74
74
  }
75
- const toolGroup = await page.pptrPage.evaluate(() => {
75
+ const toolGroups = await page.pptrPage.evaluate(() => {
76
+ if (window.__dtmcp) {
77
+ window.__dtmcp.toolGroups = [];
78
+ }
76
79
  return new Promise(resolve => {
77
80
  const event = new CustomEvent('devtoolstooldiscovery');
81
+ const groups = [];
78
82
  // @ts-expect-error Adding custom property
79
- event.respondWith = (toolGroup) => {
83
+ event.respondWith = toolGroup => {
80
84
  if (!window.__dtmcp) {
81
85
  window.__dtmcp = {};
82
86
  }
83
- window.__dtmcp.toolGroup = toolGroup;
87
+ if (!window.__dtmcp.toolGroups) {
88
+ window.__dtmcp.toolGroups = [];
89
+ }
90
+ if (typeof toolGroup.name !== 'string' ||
91
+ (toolGroup.description &&
92
+ typeof toolGroup.description !== 'string') ||
93
+ !Array.isArray(toolGroup.tools)) {
94
+ console.error('Invalid toolGroup:', toolGroup);
95
+ return;
96
+ }
97
+ for (const tool of toolGroup.tools) {
98
+ if (typeof tool.name !== 'string' ||
99
+ typeof tool.description !== 'string' ||
100
+ typeof tool.inputSchema !== 'object' ||
101
+ typeof tool.execute !== 'function') {
102
+ console.error('Invalid tool:', tool);
103
+ return;
104
+ }
105
+ }
106
+ window.__dtmcp.toolGroups.push(toolGroup);
84
107
  // When receiving a toolGroup for the first time, expose a simple execution helper
85
108
  if (!window.__dtmcp.executeTool) {
86
109
  window.__dtmcp.executeTool = async (toolName, args) => {
87
- if (!window.__dtmcp?.toolGroup) {
110
+ if (!window.__dtmcp?.toolGroups ||
111
+ window.__dtmcp.toolGroups.length === 0) {
88
112
  throw new Error('No tools found on the page');
89
113
  }
90
- const tool = window.__dtmcp.toolGroup.tools.find(t => t.name === toolName);
91
- if (!tool) {
92
- throw new Error(`Tool ${toolName} not found`);
114
+ for (const group of window.__dtmcp.toolGroups) {
115
+ const tool = group.tools?.find(t => t.name === toolName);
116
+ if (tool) {
117
+ return await tool.execute(args);
118
+ }
93
119
  }
94
- return await tool.execute(args);
120
+ throw new Error(`Tool ${toolName} not found`);
95
121
  };
96
122
  }
97
- resolve(toolGroup);
123
+ groups.push(toolGroup);
98
124
  };
99
125
  window.dispatchEvent(event);
100
- // If the page does not synchronously call `event.respondWith`, return instead of timing out
101
- setTimeout(() => {
102
- resolve(null);
103
- }, 0);
126
+ // If at least one toolGroup was added synchronously, resolve with the array.
127
+ // Otherwise, use setTimeout to allow for any microtask/asynchronous respondWith calls, or resolve with an empty array.
128
+ if (groups.length > 0) {
129
+ resolve(groups);
130
+ }
131
+ else {
132
+ setTimeout(() => {
133
+ if (groups.length > 0) {
134
+ resolve(groups);
135
+ }
136
+ else {
137
+ resolve([]);
138
+ }
139
+ }, 0);
140
+ }
104
141
  });
105
142
  });
106
- for (const tool of toolGroup?.tools ?? []) {
107
- replaceHtmlElementsWithUids(tool.inputSchema);
143
+ for (const group of toolGroups) {
144
+ for (const tool of group.tools ?? []) {
145
+ replaceHtmlElementsWithUids(tool.inputSchema);
146
+ }
108
147
  }
109
- return toolGroup;
148
+ return toolGroups;
110
149
  }
111
150
  export class McpResponse {
112
151
  #includePages = false;
@@ -134,6 +173,9 @@ export class McpResponse {
134
173
  #redactNetworkHeaders = true;
135
174
  #error;
136
175
  #attachedWaitForResult;
176
+ get #deviceScope() {
177
+ return this.#page?.viewport?.isMobile ? 'PHONE' : 'DESKTOP';
178
+ }
137
179
  constructor(args) {
138
180
  this.#args = args;
139
181
  }
@@ -203,6 +245,7 @@ export class McpResponse {
203
245
  : undefined,
204
246
  types: options?.types,
205
247
  includePreservedMessages: options?.includePreservedMessages,
248
+ serviceWorkerId: options?.serviceWorkerId,
206
249
  };
207
250
  }
208
251
  setError(error) {
@@ -291,6 +334,20 @@ export class McpResponse {
291
334
  pagination: options,
292
335
  };
293
336
  }
337
+ setHeapSnapshotRetainingPaths(retainingPaths) {
338
+ this.#heapSnapshotOptions = {
339
+ ...this.#heapSnapshotOptions,
340
+ include: true,
341
+ retainingPaths,
342
+ };
343
+ }
344
+ setHeapSnapshotDominators(dominators) {
345
+ this.#heapSnapshotOptions = {
346
+ ...this.#heapSnapshotOptions,
347
+ include: true,
348
+ dominators,
349
+ };
350
+ }
294
351
  attachImage(value) {
295
352
  this.#images.push(value);
296
353
  }
@@ -306,7 +363,7 @@ export class McpResponse {
306
363
  get listWebMcpTools() {
307
364
  return this.#listWebMcpTools;
308
365
  }
309
- async handle(toolName, context) {
366
+ async handle(toolName, context, useToon = false) {
310
367
  if (this.#includePages) {
311
368
  await context.createPagesSnapshot();
312
369
  }
@@ -383,12 +440,11 @@ export class McpResponse {
383
440
  if (this.#listExtensions) {
384
441
  extensions = await context.listExtensions();
385
442
  }
386
- // Null indicates no tools.
387
- let thirdPartyDeveloperTools;
443
+ let thirdPartyDeveloperTools = [];
388
444
  if (this.#args.categoryExperimentalThirdParty &&
389
445
  this.#listThirdPartyDeveloperTools &&
390
446
  this.#page) {
391
- thirdPartyDeveloperTools = await getToolGroup(this.#page);
447
+ thirdPartyDeveloperTools = await getToolGroups(this.#page);
392
448
  if (thirdPartyDeveloperTools) {
393
449
  this.#page.thirdPartyDeveloperTools = thirdPartyDeveloperTools;
394
450
  }
@@ -401,11 +457,18 @@ export class McpResponse {
401
457
  }
402
458
  let consoleMessages;
403
459
  if (this.#consoleDataOptions?.include) {
404
- if (!this.#page) {
405
- throw new Error(`Response must have an McpPage`);
460
+ let messages;
461
+ let page;
462
+ if (this.#consoleDataOptions.serviceWorkerId) {
463
+ messages = context.getServiceWorkerConsoleData(this.#consoleDataOptions.serviceWorkerId);
464
+ }
465
+ else {
466
+ page = this.#page;
467
+ if (!page) {
468
+ throw new Error(`Response must have an McpPage`);
469
+ }
470
+ messages = context.getConsoleData(page, this.#consoleDataOptions.includePreservedMessages);
406
471
  }
407
- const page = this.#page;
408
- let messages = context.getConsoleData(this.#page, this.#consoleDataOptions.includePreservedMessages);
409
472
  if (this.#consoleDataOptions.types?.length) {
410
473
  const normalizedTypes = new Set(this.#consoleDataOptions.types);
411
474
  messages = messages.filter(message => {
@@ -422,7 +485,9 @@ export class McpResponse {
422
485
  const consoleMessageStableId = context.getConsoleMessageStableId(item);
423
486
  if ('args' in item || item instanceof UncaughtError) {
424
487
  const consoleMessage = item;
425
- const devTools = context.getDevToolsUniverse(page);
488
+ const devTools = page
489
+ ? context.getDevToolsUniverse(page)
490
+ : null;
426
491
  return await ConsoleFormatter.from(consoleMessage, {
427
492
  id: consoleMessageStableId,
428
493
  fetchDetailedData: false,
@@ -479,9 +544,9 @@ export class McpResponse {
479
544
  thirdPartyDeveloperTools,
480
545
  webmcpTools,
481
546
  errorMessage: this.#error?.message,
482
- });
547
+ }, useToon);
483
548
  }
484
- format(toolName, context, data) {
549
+ async format(toolName, context, data, useToon) {
485
550
  const structuredContent = {};
486
551
  const response = [];
487
552
  if (this.#textResponseLines.length) {
@@ -561,8 +626,12 @@ Call ${handleDialog.name} to handle it before continuing.`);
561
626
  const contextLabel = isolatedContextName
562
627
  ? ` isolatedContext=${isolatedContextName}`
563
628
  : '';
564
- parts.push(`${context.getPageId(page)}: ${page.url()}${context.isPageSelected(page) ? ' [selected]' : ''}${contextLabel}`);
565
- structuredPages.push(createStructuredPage(page, context));
629
+ const title = await fetchPageTitle(page);
630
+ const pageLabel = title
631
+ ? `${truncateTitle(title)} (${page.url()})`
632
+ : page.url();
633
+ parts.push(`${context.getPageId(page)}: ${pageLabel}${context.isPageSelected(page) ? ' [selected]' : ''}${contextLabel}`);
634
+ structuredPages.push(createStructuredPage(page, context, title));
566
635
  }
567
636
  response.push(...parts);
568
637
  structuredContent.pages = structuredPages;
@@ -576,8 +645,12 @@ Call ${handleDialog.name} to handle it before continuing.`);
576
645
  const contextLabel = isolatedContextName
577
646
  ? ` isolatedContext=${isolatedContextName}`
578
647
  : '';
579
- response.push(`${context.getPageId(page)}: ${page.url()}${context.isPageSelected(page) ? ' [selected]' : ''}${contextLabel}`);
580
- structuredExtensionPages.push(createStructuredPage(page, context));
648
+ const title = await fetchPageTitle(page);
649
+ const pageLabel = title
650
+ ? `${truncateTitle(title)} (${page.url()})`
651
+ : page.url();
652
+ response.push(`${context.getPageId(page)}: ${pageLabel}${context.isPageSelected(page) ? ' [selected]' : ''}${contextLabel}`);
653
+ structuredExtensionPages.push(createStructuredPage(page, context, title));
581
654
  }
582
655
  structuredContent.extensionPages = structuredExtensionPages;
583
656
  }
@@ -603,7 +676,7 @@ Call ${handleDialog.name} to handle it before continuing.`);
603
676
  structuredContent.tabId = this.#tabId;
604
677
  }
605
678
  if (data.traceSummary) {
606
- const summary = getTraceSummary(data.traceSummary);
679
+ const summary = getTraceSummary(data.traceSummary, this.#deviceScope);
607
680
  response.push(summary);
608
681
  structuredContent.traceSummary = summary;
609
682
  structuredContent.traceInsights = [];
@@ -617,7 +690,7 @@ Call ${handleDialog.name} to handle it before continuing.`);
617
690
  }
618
691
  }
619
692
  if (data.traceInsight) {
620
- const insightOutput = getInsightOutput(data.traceInsight.trace, data.traceInsight.insightSetId, data.traceInsight.insightName);
693
+ const insightOutput = getInsightOutput(data.traceInsight.trace, data.traceInsight.insightSetId, data.traceInsight.insightName, this.#deviceScope);
621
694
  if ('error' in insightOutput) {
622
695
  response.push(insightOutput.error);
623
696
  }
@@ -651,9 +724,11 @@ Call ${handleDialog.name} to handle it before continuing.`);
651
724
  structuredContent.snapshotFilePath = data.snapshot;
652
725
  }
653
726
  else {
654
- response.push('## Latest page snapshot');
655
- response.push(data.snapshot.toString());
656
727
  structuredContent.snapshot = data.snapshot.toJSON();
728
+ response.push('## Latest page snapshot');
729
+ response.push(useToon
730
+ ? toonEncode(structuredContent.snapshot)
731
+ : data.snapshot.toString());
657
732
  }
658
733
  }
659
734
  if (this.#heapSnapshotOptions?.include) {
@@ -678,8 +753,10 @@ Call ${handleDialog.name} to handle it before continuing.`);
678
753
  response.push(...paginationData.info);
679
754
  const paginatedRecord = Object.fromEntries(paginationData.items);
680
755
  const formatter = new HeapSnapshotFormatter(paginatedRecord);
681
- response.push(formatter.toString());
682
756
  structuredContent.heapSnapshotData = formatter.toJSON();
757
+ response.push(useToon
758
+ ? toonEncode(structuredContent.heapSnapshotData)
759
+ : formatter.toString());
683
760
  }
684
761
  const nodes = this.#heapSnapshotOptions.nodes;
685
762
  if (nodes) {
@@ -701,6 +778,36 @@ Call ${handleDialog.name} to handle it before continuing.`);
701
778
  response.push(...paginationData.info);
702
779
  structuredContent.heapSnapshotNodes = paginationData.items;
703
780
  }
781
+ const retainingPaths = this.#heapSnapshotOptions.retainingPaths;
782
+ if (retainingPaths) {
783
+ response.push('### Retaining Paths');
784
+ const { paths, limitsReached } = retainingPaths;
785
+ if (paths.length === 0) {
786
+ response.push('No retaining paths found.');
787
+ }
788
+ else {
789
+ response.push(HeapSnapshotFormatter.formatRetainingPaths(paths));
790
+ }
791
+ const reached = Object.entries(limitsReached)
792
+ .filter(([, hit]) => hit)
793
+ .map(([limit]) => limit);
794
+ if (reached.length > 0) {
795
+ response.push(`Note: results are truncated, the following limits were reached: ${reached.join(', ')}.`);
796
+ }
797
+ structuredContent.heapSnapshotRetainingPaths =
798
+ retainingPaths;
799
+ }
800
+ const dominators = this.#heapSnapshotOptions.dominators;
801
+ if (dominators) {
802
+ response.push('### Dominator Chain');
803
+ if (dominators.length === 0) {
804
+ response.push('No dominators found.');
805
+ }
806
+ else {
807
+ response.push(HeapSnapshotFormatter.formatDominators(dominators));
808
+ }
809
+ structuredContent.heapSnapshotDominators = dominators;
810
+ }
704
811
  }
705
812
  if (data.detailedNetworkRequest) {
706
813
  response.push(data.detailedNetworkRequest.toStringDetailed());
@@ -728,18 +835,11 @@ Call ${handleDialog.name} to handle it before continuing.`);
728
835
  response.push(extensionsMessage);
729
836
  }
730
837
  }
731
- if (data.thirdPartyDeveloperTools !== undefined) {
732
- if (data.thirdPartyDeveloperTools) {
733
- structuredContent.thirdPartyDeveloperTools =
734
- data.thirdPartyDeveloperTools;
735
- }
838
+ if (data.thirdPartyDeveloperTools.length) {
839
+ structuredContent.thirdPartyDeveloperTools =
840
+ data.thirdPartyDeveloperTools;
736
841
  response.push('## Third-party developer tools');
737
- if (data.thirdPartyDeveloperTools === null ||
738
- !data.thirdPartyDeveloperTools?.tools) {
739
- response.push('No third-party developer tools available.');
740
- }
741
- else {
742
- const toolGroup = data.thirdPartyDeveloperTools;
842
+ for (const toolGroup of data.thirdPartyDeveloperTools) {
743
843
  response.push(`${toolGroup.name}: ${toolGroup.description}`);
744
844
  response.push('Available tools:');
745
845
  const toolDefinitionsMessage = toolGroup.tools
@@ -778,11 +878,10 @@ Call ${handleDialog.name} to handle it before continuing.`);
778
878
  structuredContent.pagination = paginationData.pagination;
779
879
  response.push(...paginationData.info);
780
880
  if (data.networkRequests) {
781
- structuredContent.networkRequests = [];
782
- for (const formatter of paginationData.items) {
783
- response.push(formatter.toString());
784
- structuredContent.networkRequests.push(formatter.toJSON());
785
- }
881
+ structuredContent.networkRequests = paginationData.items.map(i => i.toJSON());
882
+ response.push(...(useToon
883
+ ? [toonEncode(structuredContent.networkRequests)]
884
+ : paginationData.items.map(i => i.toString())));
786
885
  }
787
886
  }
788
887
  else {
@@ -796,9 +895,14 @@ Call ${handleDialog.name} to handle it before continuing.`);
796
895
  const grouped = ConsoleFormatter.groupConsecutive(messages);
797
896
  const paginationData = this.#dataWithPagination(grouped, this.#consoleDataOptions.pagination);
798
897
  structuredContent.pagination = paginationData.pagination;
799
- response.push(...paginationData.info);
800
- response.push(...paginationData.items.map(item => item.toString()));
801
898
  structuredContent.consoleMessages = paginationData.items.map(item => item.toJSON());
899
+ response.push(...paginationData.info);
900
+ if (useToon) {
901
+ response.push(toonEncode(structuredContent.consoleMessages));
902
+ }
903
+ else {
904
+ response.push(...paginationData.items.map(item => item.toString()));
905
+ }
802
906
  }
803
907
  else {
804
908
  response.push('<no console messages found>');
@@ -857,11 +961,25 @@ Call ${handleDialog.name} to handle it before continuing.`);
857
961
  this.#textResponseLines = [];
858
962
  }
859
963
  }
860
- function createStructuredPage(page, context) {
964
+ function truncateTitle(title, maxLength = 50) {
965
+ if (title.length <= maxLength) {
966
+ return title;
967
+ }
968
+ return title.slice(0, maxLength - 3) + '...';
969
+ }
970
+ async function fetchPageTitle(page) {
971
+ return Promise.race([
972
+ page.title().catch(() => ''),
973
+ new Promise(resolve => setTimeout(() => resolve(''), 1000)),
974
+ ]);
975
+ }
976
+ function createStructuredPage(page, context, rawTitle) {
861
977
  const isolatedContextName = context.getIsolatedContextName(page);
978
+ const title = truncateTitle(rawTitle);
862
979
  const entry = {
863
980
  id: context.getPageId(page),
864
981
  url: page.url(),
982
+ title,
865
983
  selected: context.isPageSelected(page),
866
984
  };
867
985
  if (isolatedContextName) {
@@ -3,7 +3,7 @@
3
3
  * Copyright 2025 Google LLC
4
4
  * SPDX-License-Identifier: Apache-2.0
5
5
  */
6
- import { FakeIssuesManager } from './DevtoolsUtils.js';
6
+ import { FakeIssuesManager } from './devtools/DevtoolsUtils.js';
7
7
  import { logger } from './logger.js';
8
8
  import { DevTools } from './third_party/index.js';
9
9
  import { createIdGenerator, stableIdSymbol, } from './utils/id.js';
@@ -50,7 +50,7 @@ export class PageCollector {
50
50
  this.addPage(page);
51
51
  }
52
52
  catch (err) {
53
- logger('Error getting a page for a target onTargetCreated', err);
53
+ logger?.('Error getting a page for a target onTargetCreated', err);
54
54
  }
55
55
  };
56
56
  #onTargetDestroyed = async (target) => {
@@ -62,7 +62,7 @@ export class PageCollector {
62
62
  this.cleanupPageDestroyed(page);
63
63
  }
64
64
  catch (err) {
65
- logger('Error getting a page for a target onTargetDestroyed', err);
65
+ logger?.('Error getting a page for a target onTargetDestroyed', err);
66
66
  }
67
67
  };
68
68
  addPage(page) {
@@ -136,10 +136,10 @@ export class PageCollector {
136
136
  throw new Error('No requests found for selected page');
137
137
  }
138
138
  const item = this.find(page, item => item[stableIdSymbol] === stableId);
139
- if (item) {
140
- return item;
139
+ if (!item) {
140
+ throw new Error('Request not found for selected page');
141
141
  }
142
- throw new Error('Request not found for selected page');
142
+ return item;
143
143
  }
144
144
  find(page, filter) {
145
145
  const navigations = this.storage.get(page);
@@ -241,7 +241,7 @@ class PageEventSubscriber {
241
241
  // @ts-expect-error Protocol types diverge.
242
242
  inspectorIssue)[0];
243
243
  if (!issue) {
244
- logger('No issue mapping for for the issue: ', inspectorIssue.code);
244
+ logger?.('No issue mapping for for the issue: ', inspectorIssue.code);
245
245
  return;
246
246
  }
247
247
  const primaryKey = issue.primaryKey();
@@ -256,7 +256,7 @@ class PageEventSubscriber {
256
256
  });
257
257
  }
258
258
  catch (error) {
259
- logger('Error creating a new issue', error);
259
+ logger?.('Error creating a new issue', error);
260
260
  }
261
261
  };
262
262
  }
@@ -0,0 +1,171 @@
1
+ /**
2
+ * @license
3
+ * Copyright 2025 Google LLC
4
+ * SPDX-License-Identifier: Apache-2.0
5
+ */
6
+ import { UncaughtError } from './PageCollector.js';
7
+ import { createIdGenerator, stableIdSymbol } from './utils/id.js';
8
+ const CHROME_EXTENSION_PREFIX = 'chrome-extension://';
9
+ export class ServiceWorkerSubscriber {
10
+ #target;
11
+ #callback;
12
+ #session;
13
+ #worker;
14
+ constructor(target, callback) {
15
+ this.#target = target;
16
+ this.#callback = callback;
17
+ }
18
+ async subscribe() {
19
+ this.#session = await this.#target.createCDPSession();
20
+ await this.#session.send('Runtime.enable');
21
+ this.#session.on('Runtime.exceptionThrown', this.#onExceptionThrown);
22
+ this.#worker = (await this.#target.worker()) ?? undefined;
23
+ if (this.#worker) {
24
+ this.#worker.on('console', this.#onConsole);
25
+ }
26
+ }
27
+ async unsubscribe() {
28
+ if (this.#worker) {
29
+ this.#worker.off('console', this.#onConsole);
30
+ }
31
+ await this.#session?.detach();
32
+ }
33
+ #onConsole = (message) => {
34
+ this.#callback(message);
35
+ };
36
+ #onExceptionThrown = (event) => {
37
+ const url = this.#target.url();
38
+ const extensionId = extractExtensionId(url);
39
+ if (extensionId) {
40
+ this.#callback(new UncaughtError(event.exceptionDetails, extensionId));
41
+ }
42
+ };
43
+ }
44
+ export class ServiceWorkerConsoleCollector {
45
+ #storage = new Map();
46
+ #maxLogs;
47
+ #browser;
48
+ #serviceWorkerSubscribers = new Map();
49
+ #idGenerator = createIdGenerator();
50
+ constructor(browser, maxLogs = 1000) {
51
+ this.#browser = browser;
52
+ this.#maxLogs = maxLogs;
53
+ }
54
+ async init(workers) {
55
+ if (!this.#browser) {
56
+ return;
57
+ }
58
+ this.#browser.on('targetcreated', this.#onTargetCreated);
59
+ this.#browser.on('targetdestroyed', this.#onTargetDestroyed);
60
+ for (const worker of workers) {
61
+ void this.#onTargetCreated(worker.target);
62
+ }
63
+ }
64
+ dispose() {
65
+ if (!this.#browser) {
66
+ return;
67
+ }
68
+ this.#browser.off('targetcreated', this.#onTargetCreated);
69
+ this.#browser.off('targetdestroyed', this.#onTargetDestroyed);
70
+ for (const subscriber of this.#serviceWorkerSubscribers.values()) {
71
+ subscriber.unsubscribe().catch(err => {
72
+ if (err instanceof Error &&
73
+ !err.message.includes('Target closed') &&
74
+ !err.message.includes('Session closed')) {
75
+ // Swallow error as we are tearing down the system
76
+ }
77
+ });
78
+ }
79
+ this.#serviceWorkerSubscribers.clear();
80
+ }
81
+ #onTargetCreated = async (target) => {
82
+ if (this.#serviceWorkerSubscribers.has(target)) {
83
+ return;
84
+ }
85
+ const origin = target.url();
86
+ if (target.type() === 'service_worker' && isExtensionOrigin(origin)) {
87
+ const extensionId = extractExtensionId(origin);
88
+ if (!extensionId) {
89
+ return;
90
+ }
91
+ const subscriber = new ServiceWorkerSubscriber(target, item => {
92
+ this.addLog(extensionId, item);
93
+ });
94
+ try {
95
+ await subscriber.subscribe();
96
+ }
97
+ catch (err) {
98
+ if (err instanceof Error &&
99
+ !err.message.includes('Target closed') &&
100
+ !err.message.includes('Session closed')) {
101
+ throw err;
102
+ }
103
+ }
104
+ this.#serviceWorkerSubscribers.set(target, subscriber);
105
+ }
106
+ };
107
+ #onTargetDestroyed = async (target) => {
108
+ const subscriber = this.#serviceWorkerSubscribers.get(target);
109
+ if (subscriber) {
110
+ try {
111
+ await subscriber.unsubscribe();
112
+ }
113
+ catch (err) {
114
+ if (err instanceof Error &&
115
+ !err.message.includes('Target closed') &&
116
+ !err.message.includes('Session closed')) {
117
+ throw err;
118
+ }
119
+ }
120
+ this.#serviceWorkerSubscribers.delete(target);
121
+ }
122
+ };
123
+ addLog(extensionId, log) {
124
+ const logs = this.#storage.get(extensionId) ?? [];
125
+ const withId = log;
126
+ withId[stableIdSymbol] = this.#idGenerator();
127
+ logs.push(withId);
128
+ if (logs.length > this.#maxLogs) {
129
+ logs.shift();
130
+ }
131
+ this.#storage.set(extensionId, logs);
132
+ }
133
+ getData(extensionId) {
134
+ return this.#storage.get(extensionId) ?? [];
135
+ }
136
+ getById(extensionId, stableId) {
137
+ const logs = this.#storage.get(extensionId);
138
+ if (!logs) {
139
+ throw new Error('No logs found for selected extension');
140
+ }
141
+ const item = logs.find(item => item[stableIdSymbol] === stableId);
142
+ if (item) {
143
+ return item;
144
+ }
145
+ throw new Error('Log not found for selected extension');
146
+ }
147
+ find(extensionId, filter) {
148
+ const logs = this.#storage.get(extensionId);
149
+ if (!logs) {
150
+ return;
151
+ }
152
+ return logs.find(filter);
153
+ }
154
+ clearLogs(extensionId) {
155
+ this.#storage.delete(extensionId);
156
+ }
157
+ }
158
+ function extractExtensionId(origin) {
159
+ if (!origin || !isExtensionOrigin(origin)) {
160
+ return null;
161
+ }
162
+ const pathPart = origin.substring(CHROME_EXTENSION_PREFIX.length);
163
+ const slashIndex = pathPart.indexOf('/');
164
+ // if there's no / it means that pathPart is now the extensionId, otherwise
165
+ // we take everything until the first /
166
+ return slashIndex === -1 ? pathPart : pathPart.substring(0, slashIndex);
167
+ }
168
+ function isExtensionOrigin(origin) {
169
+ return origin.startsWith(CHROME_EXTENSION_PREFIX);
170
+ }
171
+ //# sourceMappingURL=ServiceWorkerCollector.js.map
@@ -186,7 +186,7 @@ export class TextSnapshot {
186
186
  }
187
187
  }
188
188
  catch (e) {
189
- logger(`Failed to collect descendants for backend node ${backendNodeId}`, e);
189
+ logger?.(`Failed to collect descendants for backend node ${backendNodeId}`, e);
190
190
  }
191
191
  return descendantIds;
192
192
  };