browser-debugger-cli 0.13.0 → 0.14.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 (112) hide show
  1. package/.claude/skills/bdg/SKILL.md +100 -186
  2. package/README.md +4 -4
  3. package/dist/commands/console.js +5 -1
  4. package/dist/commands/dom/a11y.d.ts +1 -1
  5. package/dist/commands/dom/a11y.js +20 -20
  6. package/dist/commands/dom/eval.d.ts +2 -1
  7. package/dist/commands/dom/eval.js +21 -3
  8. package/dist/commands/dom/formInteraction.js +1 -1
  9. package/dist/commands/dom/get.js +25 -7
  10. package/dist/commands/dom/index.js +7 -2
  11. package/dist/commands/dom/query.d.ts +2 -1
  12. package/dist/commands/dom/query.js +5 -3
  13. package/dist/commands/dom/screenshot.js +1 -0
  14. package/dist/commands/helpJson.js +1 -1
  15. package/dist/commands/network/list.js +46 -3
  16. package/dist/commands/optionBehaviors.d.ts +25 -2
  17. package/dist/commands/optionBehaviors.js +55 -42
  18. package/dist/commands/peek.js +3 -0
  19. package/dist/commands/shared/CommandRunner.js +13 -13
  20. package/dist/commands/shared/daemonErrorHandler.js +2 -2
  21. package/dist/commands/shared/dataFetcher.d.ts +4 -2
  22. package/dist/commands/shared/dataFetcher.js +11 -3
  23. package/dist/commands/shared/handleValidationError.js +3 -3
  24. package/dist/commands/shared/optionTypes.d.ts +14 -3
  25. package/dist/commands/shared/startHelpers.js +3 -3
  26. package/dist/connection/chromeIdentity.d.ts +8 -2
  27. package/dist/connection/chromeIdentity.js +85 -13
  28. package/dist/constants.d.ts +29 -1
  29. package/dist/constants.js +35 -1
  30. package/dist/daemon/SessionController.js +2 -0
  31. package/dist/daemon/session/Session.d.ts +2 -1
  32. package/dist/daemon/session/Session.js +10 -2
  33. package/dist/daemon/session/TelemetryStore.d.ts +7 -0
  34. package/dist/daemon/session/TelemetryStore.js +6 -0
  35. package/dist/daemon/session/commandRegistry.js +23 -5
  36. package/dist/daemon/session/matchedStylesReset.d.ts +26 -0
  37. package/dist/daemon/session/matchedStylesReset.js +46 -0
  38. package/dist/daemon/session/plugins.js +1 -0
  39. package/dist/daemon/session/triggeredRequests.d.ts +0 -5
  40. package/dist/daemon/session/triggeredRequests.js +13 -7
  41. package/dist/daemon.js +742 -460
  42. package/dist/errors/messages.d.ts +8 -0
  43. package/dist/errors/messages.js +10 -0
  44. package/dist/index.js +710 -518
  45. package/dist/ipc/protocol/commands.d.ts +4 -0
  46. package/dist/ipc/protocol/inspectTypes.d.ts +5 -2
  47. package/dist/ipc/session/types.d.ts +5 -1
  48. package/dist/program.d.ts +14 -0
  49. package/dist/program.js +53 -0
  50. package/dist/runtime/dom/elementGeometry.d.ts +23 -0
  51. package/dist/runtime/dom/elementGeometry.js +17 -15
  52. package/dist/runtime/dom/elementInfo.d.ts +6 -4
  53. package/dist/runtime/dom/elementInfo.js +7 -4
  54. package/dist/runtime/dom/frameScopedConnection.d.ts +7 -0
  55. package/dist/runtime/dom/frameScopedConnection.js +2 -2
  56. package/dist/runtime/dom/inspect.d.ts +17 -3
  57. package/dist/runtime/dom/inspect.js +40 -26
  58. package/dist/runtime/dom/inspectModel.d.ts +3 -3
  59. package/dist/runtime/dom/inspectRules.d.ts +29 -3
  60. package/dist/runtime/dom/inspectRules.js +205 -11
  61. package/dist/runtime/dom/layout.d.ts +0 -2
  62. package/dist/runtime/dom/layout.js +1 -2
  63. package/dist/runtime/dom/reactEventHelpers.d.ts +4 -1
  64. package/dist/runtime/dom/reactEventHelpers.js +9 -2
  65. package/dist/runtime/dom/targetNode.d.ts +10 -6
  66. package/dist/runtime/dom/targetNode.js +15 -8
  67. package/dist/telemetry/a11y.d.ts +15 -1
  68. package/dist/telemetry/a11y.js +83 -0
  69. package/dist/telemetry/har/builder.js +1 -1
  70. package/dist/telemetry/network.d.ts +13 -16
  71. package/dist/telemetry/network.js +30 -52
  72. package/dist/telemetry/networkRetention.d.ts +83 -0
  73. package/dist/telemetry/networkRetention.js +117 -0
  74. package/dist/types.d.ts +26 -0
  75. package/dist/ui/OutputBuilder.d.ts +10 -0
  76. package/dist/ui/OutputBuilder.js +12 -0
  77. package/dist/ui/formatters/a11y.d.ts +5 -7
  78. package/dist/ui/formatters/a11y.js +7 -61
  79. package/dist/ui/formatters/console/chronological.js +4 -4
  80. package/dist/ui/formatters/console/follow.d.ts +4 -2
  81. package/dist/ui/formatters/console/follow.js +6 -3
  82. package/dist/ui/formatters/console/json.d.ts +3 -6
  83. package/dist/ui/formatters/console/json.js +9 -13
  84. package/dist/ui/formatters/console/shared.d.ts +17 -2
  85. package/dist/ui/formatters/console/shared.js +17 -0
  86. package/dist/ui/formatters/console/summarize.d.ts +2 -2
  87. package/dist/ui/formatters/console/summarize.js +22 -7
  88. package/dist/ui/formatters/console.d.ts +1 -1
  89. package/dist/ui/formatters/console.js +1 -5
  90. package/dist/ui/formatters/details.js +1 -1
  91. package/dist/ui/formatters/dom.d.ts +13 -4
  92. package/dist/ui/formatters/dom.js +25 -7
  93. package/dist/ui/formatters/layout.js +2 -1
  94. package/dist/ui/formatters/longValues.d.ts +14 -0
  95. package/dist/ui/formatters/longValues.js +23 -0
  96. package/dist/ui/formatters/networkList.d.ts +8 -2
  97. package/dist/ui/formatters/networkList.js +11 -2
  98. package/dist/ui/formatters/preview.d.ts +4 -1
  99. package/dist/ui/formatters/preview.js +55 -13
  100. package/dist/ui/formatters/status.js +7 -0
  101. package/dist/ui/formatters/triggeredRequests.js +2 -1
  102. package/dist/ui/messages/chrome.d.ts +20 -1
  103. package/dist/ui/messages/chrome.js +29 -3
  104. package/dist/ui/messages/commands.d.ts +29 -8
  105. package/dist/ui/messages/commands.js +36 -8
  106. package/dist/ui/messages/networkMessages.d.ts +24 -0
  107. package/dist/ui/messages/networkMessages.js +45 -0
  108. package/dist/utils/http.d.ts +9 -2
  109. package/dist/utils/http.js +4 -3
  110. package/dist/utils/strings.d.ts +19 -0
  111. package/dist/utils/strings.js +16 -0
  112. package/package.json +2 -2
@@ -3,7 +3,7 @@
3
3
  */
4
4
  import { sessionUnavailableSuggestion } from '../../errors/messages.js';
5
5
  import { genericError } from '../../errors/messages.js';
6
- import { OutputBuilder } from '../../ui/OutputBuilder.js';
6
+ import { OutputBuilder, stringifyEnvelope } from '../../ui/OutputBuilder.js';
7
7
  import { connectionLostRetryMessage, connectionLostStopHintMessage, followedSessionEndedMessage, } from '../../ui/messages/preview.js';
8
8
  import { EXIT_CODES } from '../../utils/exitCodes.js';
9
9
  /** Follow-mode state: whether a session ever answered, and whether its loss was reported */
@@ -42,7 +42,7 @@ export function handleDaemonConnectionError(error, options) {
42
42
  exitCode,
43
43
  ...(suggestion && { suggestion }),
44
44
  });
45
- console.log(follow ? JSON.stringify(envelope) : JSON.stringify(envelope, null, 2));
45
+ console.log(follow ? JSON.stringify(envelope) : stringifyEnvelope(envelope));
46
46
  }
47
47
  else {
48
48
  console.error(genericError(message));
@@ -3,6 +3,7 @@
3
3
  */
4
4
  import type { PeekSection } from '../../ipc/protocol/commands.js';
5
5
  import type { BdgOutput, ConsoleMessage, NetworkRequest } from '../../types.js';
6
+ import type { NetworkEvictionCounts } from '../../ui/messages/networkMessages.js';
6
7
  export type FetchSuccess<T> = {
7
8
  success: true;
8
9
  data: T;
@@ -47,12 +48,13 @@ export declare function fetchPreviewData(query?: PreviewQuery): Promise<FetchRes
47
48
  * Fetch all captured network requests from daemon.
48
49
  *
49
50
  * @param withHeaders - Include request/response headers (needed by header filters)
50
- * @returns Requests, and when the page crashed (while it is not loaded
51
- * again), or a fetch error
51
+ * @returns Requests, when the page crashed (while it is not loaded again)
52
+ * and what the session let go at its capture limits, or a fetch error
52
53
  */
53
54
  export declare function fetchNetworkRequests(withHeaders?: boolean): Promise<FetchResult<{
54
55
  requests: NetworkRequest[];
55
56
  pageCrashedAt: number | undefined;
57
+ evictions: NetworkEvictionCounts;
56
58
  }>>;
57
59
  /**
58
60
  * Fetch all console messages from daemon.
@@ -90,16 +90,24 @@ export async function fetchPreviewData(query = {}) {
90
90
  * Fetch all captured network requests from daemon.
91
91
  *
92
92
  * @param withHeaders - Include request/response headers (needed by header filters)
93
- * @returns Requests, and when the page crashed (while it is not loaded
94
- * again), or a fetch error
93
+ * @returns Requests, when the page crashed (while it is not loaded again)
94
+ * and what the session let go at its capture limits, or a fetch error
95
95
  */
96
96
  export async function fetchNetworkRequests(withHeaders = false) {
97
97
  const result = await fetchPreviewData({ lastN: 0, only: 'network', withHeaders });
98
98
  if (!result.success)
99
99
  return result;
100
+ const { totals, pageCrashedAt } = result.data.output;
100
101
  return {
101
102
  success: true,
102
- data: { requests: result.data.network, pageCrashedAt: result.data.output.pageCrashedAt },
103
+ data: {
104
+ requests: result.data.network,
105
+ pageCrashedAt,
106
+ evictions: {
107
+ requestsDropped: totals?.networkDropped ?? 0,
108
+ bodiesEvicted: totals?.networkBodiesEvicted ?? 0,
109
+ },
110
+ },
103
111
  };
104
112
  }
105
113
  /**
@@ -1,6 +1,6 @@
1
1
  import { CommandError } from '../../errors/index.js';
2
2
  import { genericError } from '../../errors/messages.js';
3
- import { OutputBuilder } from '../../ui/OutputBuilder.js';
3
+ import { OutputBuilder, stringifyEnvelope } from '../../ui/OutputBuilder.js';
4
4
  import { escapeControlChars } from '../../ui/formatting.js';
5
5
  import { EXIT_CODES } from '../../utils/exitCodes.js';
6
6
  /**
@@ -24,7 +24,7 @@ export function handleValidationError(error, json) {
24
24
  if (error.metadata.suggestion) {
25
25
  errorOptions.suggestion = error.metadata.suggestion;
26
26
  }
27
- console.log(JSON.stringify(OutputBuilder.buildJsonError(error.message, errorOptions), null, 2));
27
+ console.log(stringifyEnvelope(OutputBuilder.buildJsonError(error.message, errorOptions)));
28
28
  }
29
29
  else {
30
30
  console.error(genericError(error.message));
@@ -38,7 +38,7 @@ export function handleValidationError(error, json) {
38
38
  const envelope = OutputBuilder.buildJsonError(message, {
39
39
  exitCode: EXIT_CODES.INVALID_ARGUMENTS,
40
40
  });
41
- console.log(JSON.stringify(envelope, null, 2));
41
+ console.log(stringifyEnvelope(envelope));
42
42
  }
43
43
  else {
44
44
  console.error(genericError(message));
@@ -143,12 +143,12 @@ export type DetailsCommandOptions = BaseOptions & {
143
143
  };
144
144
  /** Options for DOM query command */
145
145
  export type DomQueryCommandOptions = BaseOptions & {
146
- /** Matches listed (0 = all); default 50, or 1000 with --json */
146
+ /** Matches listed (0 = all); default 50, or 100 with --json */
147
147
  limit?: number;
148
148
  };
149
149
  /** Options for DOM get command */
150
150
  export type DomGetCommandOptions = BaseOptions & RawOptions & SelectionOptions & {
151
- /** All of the element's text instead of its first 500 characters (semantic output) */
151
+ /** All of the element's text (semantic output) or HTML (`--raw`), instead of its start */
152
152
  full?: boolean;
153
153
  /** Which match of the selector (0-based); `--nth` is its alias */
154
154
  index?: number;
@@ -159,6 +159,8 @@ export type DomScreenshotCommandOptions = BaseOptions & ScreenshotOptions;
159
159
  export interface DomEvalCommandOptions extends BaseOptions {
160
160
  /** Iframe to evaluate in: index, name/id attribute, or part of the name, id or URL */
161
161
  frame?: string;
162
+ /** The whole value instead of its first 20000 characters */
163
+ full?: boolean;
162
164
  }
163
165
  /** Options for DOM frames command */
164
166
  export type DomFramesCommandOptions = BaseOptions;
@@ -276,7 +278,12 @@ export interface WaitCommandOptions extends BaseOptions {
276
278
  timeout: number;
277
279
  }
278
280
  /** Options for A11y tree command */
279
- export type A11yTreeCommandOptions = BaseOptions;
281
+ export interface A11yTreeCommandOptions extends BaseOptions {
282
+ /** Nodes to list (0 = all); default 50, also with --json */
283
+ limit?: number;
284
+ /** Levels below the root to list (0 = root only) */
285
+ depth?: number;
286
+ }
280
287
  /** Options for A11y query command */
281
288
  export interface A11yQueryCommandOptions extends BaseOptions {
282
289
  /** Matches to list (0 = all) */
@@ -354,6 +361,8 @@ export interface ConsoleCommandOptions extends BaseOptions {
354
361
  history?: boolean;
355
362
  /** Filter by message level (error, warning, info, debug) */
356
363
  level?: ConsoleLevel;
364
+ /** Message texts whole instead of cut */
365
+ full?: boolean;
357
366
  }
358
367
  /**
359
368
  * Options for preview display.
@@ -380,6 +389,8 @@ export interface PeekCommandOptions extends BaseOptions, PreviewDisplayOptions {
380
389
  type?: string;
381
390
  /** Refresh interval of --follow in ms (string from CLI, default: 1000) */
382
391
  interval?: string;
392
+ /** Console message texts whole instead of cut */
393
+ full?: boolean;
383
394
  }
384
395
  /**
385
396
  * Options for tail command.
@@ -14,7 +14,7 @@ import { IPCErrorCode, } from '../../ipc/index.js';
14
14
  import { IPCTimeoutError } from '../../ipc/transport/index.js';
15
15
  import { isConnectionError } from '../../ipc/utils/errors.js';
16
16
  import { getSessionName } from '../../session/paths.js';
17
- import { OutputBuilder, buildSuccessResponse } from '../../ui/OutputBuilder.js';
17
+ import { OutputBuilder, buildSuccessResponse, stringifyEnvelope } from '../../ui/OutputBuilder.js';
18
18
  import { escapeControlChars, joinLines } from '../../ui/formatting.js';
19
19
  import { createLogger } from '../../ui/logging/index.js';
20
20
  import { daemonStillExitingHint, daemonStillExitingSuggestion, startNotices, } from '../../ui/messages/session.js';
@@ -318,7 +318,7 @@ function reportStartOutcome(outcome, options) {
318
318
  exitCode: outcome.exitCode,
319
319
  ...outcome.details,
320
320
  });
321
- console.log(JSON.stringify(envelope, null, 2));
321
+ console.log(stringifyEnvelope(envelope));
322
322
  }
323
323
  else {
324
324
  console.error(escapeControlChars(outcome.human));
@@ -340,7 +340,7 @@ function reportStartOutcome(outcome, options) {
340
340
  daemonPid: data.daemonPid,
341
341
  ...(autoStopAt && { autoStopAt: autoStopAt.toISOString() }),
342
342
  };
343
- console.log(JSON.stringify(buildSuccessResponse(result), null, 2));
343
+ console.log(stringifyEnvelope(buildSuccessResponse(result)));
344
344
  }
345
345
  else {
346
346
  const page = {
@@ -49,12 +49,18 @@ export declare function waitForDevToolsEndpoint(logs: StartupLogs, isRunning: ()
49
49
  * Check that the Chrome answering on 127.0.0.1:<port> is the one just
50
50
  * launched, so bdg never drives another session's browser.
51
51
  *
52
+ * A Chrome that announced 127.0.0.1:<port> but does not answer yet (a slow
53
+ * start, its `/json/version` request timing out) is asked again until the
54
+ * deadline; only an answer from something else is a port conflict. A Chrome
55
+ * that fell back to [::1] is asked once: 127.0.0.1 is held by something else.
56
+ *
52
57
  * @param options - Chrome's log positions from before the launch, requested
53
58
  * port (free on 127.0.0.1 and ::1 right before the launch), Chrome's PID and
54
- * longest wait for its announcement
59
+ * longest wait for its announcement and its answer
55
60
  * @throws ChromeLaunchError: CHROME_DIED_AFTER_LAUNCH if Chrome exits first;
56
61
  * PORT_IN_USE if another browser or process answers on the port;
57
- * CHROME_LAUNCH_FAILED if Chrome announced nothing and nothing answers
62
+ * CHROME_LAUNCH_FAILED if Chrome announced nothing and nothing answers, or
63
+ * announced the port but did not answer on it in time
58
64
  */
59
65
  export declare function verifyLaunchedChrome(options: {
60
66
  logs: StartupLogs;
@@ -18,8 +18,9 @@
18
18
  * before the launch, and a second listener is how a conflict shows.
19
19
  */
20
20
  import { createLogger } from '../ui/logging/index.js';
21
+ import { chromeNotAnsweringReason, portTakenByReason } from '../ui/messages/chrome.js';
21
22
  import { delay } from '../utils/async.js';
22
- import { fetchBrowserWsUrl } from '../utils/http.js';
23
+ import { CDP_HTTP_TIMEOUT_MS, fetchBrowserWsUrl, probeDevToolsEndpoint, } from '../utils/http.js';
23
24
  import { isProcessAlive } from '../utils/process.js';
24
25
  import { ChromeLaunchError } from './errors.js';
25
26
  import { acceptsConnections } from './portReservation.js';
@@ -29,6 +30,8 @@ const LISTENING_PATTERN = /^DevTools listening on (ws:\/\/\S+)/;
29
30
  /** How long to wait for Chrome to announce its endpoint after the port answered */
30
31
  const ENDPOINT_WAIT_MS = 10000;
31
32
  const ENDPOINT_POLL_MS = 50;
33
+ /** Shortest wait for one answer, also when the deadline has passed */
34
+ const MIN_ANSWER_WAIT_MS = 1000;
32
35
  const log = createLogger('chrome');
33
36
  /**
34
37
  * Find the endpoint Chrome announced in its output (the last announcement).
@@ -75,32 +78,75 @@ export async function waitForDevToolsEndpoint(logs, isRunning, timeoutMs = ENDPO
75
78
  * Check that the Chrome answering on 127.0.0.1:<port> is the one just
76
79
  * launched, so bdg never drives another session's browser.
77
80
  *
81
+ * A Chrome that announced 127.0.0.1:<port> but does not answer yet (a slow
82
+ * start, its `/json/version` request timing out) is asked again until the
83
+ * deadline; only an answer from something else is a port conflict. A Chrome
84
+ * that fell back to [::1] is asked once: 127.0.0.1 is held by something else.
85
+ *
78
86
  * @param options - Chrome's log positions from before the launch, requested
79
87
  * port (free on 127.0.0.1 and ::1 right before the launch), Chrome's PID and
80
- * longest wait for its announcement
88
+ * longest wait for its announcement and its answer
81
89
  * @throws ChromeLaunchError: CHROME_DIED_AFTER_LAUNCH if Chrome exits first;
82
90
  * PORT_IN_USE if another browser or process answers on the port;
83
- * CHROME_LAUNCH_FAILED if Chrome announced nothing and nothing answers
91
+ * CHROME_LAUNCH_FAILED if Chrome announced nothing and nothing answers, or
92
+ * announced the port but did not answer on it in time
84
93
  */
85
94
  export async function verifyLaunchedChrome(options) {
86
- const { logs, port, pid, timeoutMs } = options;
95
+ const { logs, port, pid, timeoutMs = ENDPOINT_WAIT_MS } = options;
96
+ const started = Date.now();
87
97
  const isRunning = () => isProcessAlive(pid);
88
98
  const endpoint = await waitForDevToolsEndpoint(logs, isRunning, timeoutMs);
89
- if (!endpoint && !isRunning()) {
90
- throw new ChromeLaunchError(`Chrome died immediately after launch (PID: ${pid})`, {
91
- issue: { code: 'CHROME_DIED_AFTER_LAUNCH', context: { port, pid } },
92
- });
93
- }
99
+ if (!endpoint && !isRunning())
100
+ throw diedError(port, pid);
94
101
  if (!endpoint)
95
102
  return acceptUnannouncedChrome(logs, port);
96
103
  if (endpoint.port !== port) {
97
104
  throw portTakenError(port, `Chrome listens on port ${endpoint.port} instead`);
98
105
  }
99
- const answering = await fetchBrowserWsUrl(port, log);
100
- if (!answering || new URL(answering).pathname !== endpoint.browserPath) {
101
- throw portTakenError(port, `another process answers on 127.0.0.1 (Chrome listens on ${endpoint.host})`);
106
+ const onLoopback = endpoint.host === '127.0.0.1';
107
+ const deadline = onLoopback ? started + timeoutMs : Date.now();
108
+ const answer = await waitForAnswer(port, deadline, isRunning);
109
+ if (answer.kind === 'devtools' && new URL(answer.wsUrl).pathname === endpoint.browserPath) {
110
+ log.debug(`Chrome on port ${port} is the launched one (${endpoint.browserPath})`);
111
+ return;
112
+ }
113
+ if (answer.kind === 'unreachable' && !isRunning())
114
+ throw diedError(port, pid);
115
+ if (answer.kind === 'unreachable' && onLoopback) {
116
+ throw slowStartError(port, Date.now() - started);
117
+ }
118
+ throw portTakenError(port, portTakenByReason(answerSource(answer), endpoint.host));
119
+ }
120
+ /**
121
+ * Ask 127.0.0.1:<port> for its DevTools version until something answers,
122
+ * Chrome exits or the deadline passes (at least once). Each request waits at
123
+ * most until the deadline (but at least MIN_ANSWER_WAIT_MS).
124
+ *
125
+ * @param port - Requested port
126
+ * @param deadline - Time (ms since epoch) to stop asking
127
+ * @param isRunning - Whether Chrome is still running
128
+ * @returns The first answer, or the last `unreachable` result
129
+ */
130
+ async function waitForAnswer(port, deadline, isRunning) {
131
+ for (;;) {
132
+ const remaining = Math.max(deadline - Date.now(), MIN_ANSWER_WAIT_MS);
133
+ const timeoutMs = Math.min(CDP_HTTP_TIMEOUT_MS, remaining);
134
+ const answer = await probeDevToolsEndpoint(port, undefined, { timeoutMs });
135
+ if (answer.kind !== 'unreachable' || !isRunning() || Date.now() >= deadline)
136
+ return answer;
137
+ await delay(ENDPOINT_POLL_MS);
102
138
  }
103
- log.debug(`Chrome on port ${port} is the launched one (${endpoint.browserPath})`);
139
+ }
140
+ /**
141
+ * What answered on 127.0.0.1:<port> instead of the launched Chrome.
142
+ *
143
+ * @param answer - The answer
144
+ * @returns A browser, another process, or nothing
145
+ */
146
+ function answerSource(answer) {
147
+ if (answer.kind === 'devtools')
148
+ return 'browser';
149
+ return answer.kind === 'not-devtools' ? 'process' : 'nothing';
104
150
  }
105
151
  /**
106
152
  * Accept a Chrome that did not announce its endpoint, if a browser answers on
@@ -128,6 +174,32 @@ async function acceptUnannouncedChrome(logs, port) {
128
174
  }
129
175
  log.info(`Warning: ${missing}; using the browser on 127.0.0.1:${port}, which was free before the launch`);
130
176
  }
177
+ /**
178
+ * The error for a Chrome that exited during the launch checks.
179
+ *
180
+ * @param port - Requested port
181
+ * @param pid - Chrome's PID
182
+ * @returns Launch error with the CHROME_DIED_AFTER_LAUNCH issue
183
+ */
184
+ function diedError(port, pid) {
185
+ return new ChromeLaunchError(`Chrome died immediately after launch (PID: ${pid})`, {
186
+ issue: { code: 'CHROME_DIED_AFTER_LAUNCH', context: { port, pid } },
187
+ });
188
+ }
189
+ /**
190
+ * The error for a Chrome that announced the port but did not answer on it in
191
+ * time.
192
+ *
193
+ * @param port - Requested port
194
+ * @param waitedMs - How long bdg waited
195
+ * @returns Launch error with the CHROME_LAUNCH_FAILED issue
196
+ */
197
+ function slowStartError(port, waitedMs) {
198
+ const reason = chromeNotAnsweringReason(port, waitedMs);
199
+ return new ChromeLaunchError(reason, {
200
+ issue: { code: 'CHROME_LAUNCH_FAILED', context: { port, reason } },
201
+ });
202
+ }
131
203
  /**
132
204
  * The error for a port another process answers on.
133
205
  *
@@ -58,7 +58,8 @@ export declare const DOCKER_CHROME_FLAGS: string[];
58
58
  */
59
59
  export declare const BDG_CHROME_PREFS: Record<string, unknown>;
60
60
  /**
61
- * Maximum network requests to collect before dropping new requests
61
+ * Finished network requests kept: past this the oldest are dropped, so the
62
+ * newest are kept (requests in flight are never dropped)
62
63
  * Prevents memory issues in long-running sessions with high network activity
63
64
  */
64
65
  export declare const MAX_NETWORK_REQUESTS = 10000;
@@ -88,6 +89,24 @@ export declare const OBJECT_EXPANSION_FAILURE_THRESHOLD = 5;
88
89
  * Can be overridden with --max-body-size flag
89
90
  */
90
91
  export declare const MAX_RESPONSE_SIZE: number;
92
+ /**
93
+ * Total size of the response bodies a session keeps (100MB)
94
+ * Past this the oldest bodies are replaced by a placeholder; their requests stay
95
+ */
96
+ export declare const MAX_TOTAL_BODY_BYTES: number;
97
+ /**
98
+ * Characters of one value `dom get --raw` and `dom eval` print (human output,
99
+ * and eval string results and outer HTML in JSON)
100
+ */
101
+ export declare const MAX_VALUE_LENGTH = 20000;
102
+ /**
103
+ * Characters of a console message text in human output (`console`, `peek`)
104
+ */
105
+ export declare const MAX_CONSOLE_TEXT_LENGTH = 200;
106
+ /**
107
+ * Characters of a console message text in JSON output (`console`, `peek`)
108
+ */
109
+ export declare const MAX_CONSOLE_JSON_TEXT_LENGTH = 10000;
91
110
  /**
92
111
  * Total Chrome network buffer size (50MB)
93
112
  * Limits total memory used by Chrome for preserving network payloads
@@ -103,6 +122,15 @@ export declare const CHROME_NETWORK_BUFFER_PER_RESOURCE: number;
103
122
  * Limits size of POST body data included in requestWillBeSent notification
104
123
  */
105
124
  export declare const CHROME_POST_DATA_LIMIT: number;
125
+ /** Matches `dom query` and `dom a11y query` list with `--json` and no `--limit` */
126
+ export declare const QUERY_JSON_LIST_LIMIT = 100;
127
+ /** Matches `dom layout` measures per command (the rest are counted as omitted) */
128
+ export declare const LAYOUT_ELEMENT_LIMIT = 100;
129
+ /**
130
+ * Requests listed in an action's result (a click that loads a page triggers
131
+ * its whole load); notable ones are kept before assets
132
+ */
133
+ export declare const MAX_TRIGGERED_REQUESTS = 50;
106
134
  /**
107
135
  * Default page readiness timeout (2 seconds)
108
136
  * Maximum time to wait for page to be ready before proceeding
package/dist/constants.js CHANGED
@@ -87,7 +87,8 @@ export const BDG_CHROME_PREFS = {
87
87
  // DATA COLLECTION LIMITS
88
88
  // ============================================================================
89
89
  /**
90
- * Maximum network requests to collect before dropping new requests
90
+ * Finished network requests kept: past this the oldest are dropped, so the
91
+ * newest are kept (requests in flight are never dropped)
91
92
  * Prevents memory issues in long-running sessions with high network activity
92
93
  */
93
94
  export const MAX_NETWORK_REQUESTS = 10000;
@@ -120,6 +121,27 @@ export const OBJECT_EXPANSION_FAILURE_THRESHOLD = 5;
120
121
  * Can be overridden with --max-body-size flag
121
122
  */
122
123
  export const MAX_RESPONSE_SIZE = 5 * 1024 * 1024; // 5MB
124
+ /**
125
+ * Total size of the response bodies a session keeps (100MB)
126
+ * Past this the oldest bodies are replaced by a placeholder; their requests stay
127
+ */
128
+ export const MAX_TOTAL_BODY_BYTES = 100 * 1024 * 1024;
129
+ // ============================================================================
130
+ // OUTPUT VALUE LIMITS (lifted by --full)
131
+ // ============================================================================
132
+ /**
133
+ * Characters of one value `dom get --raw` and `dom eval` print (human output,
134
+ * and eval string results and outer HTML in JSON)
135
+ */
136
+ export const MAX_VALUE_LENGTH = 20_000;
137
+ /**
138
+ * Characters of a console message text in human output (`console`, `peek`)
139
+ */
140
+ export const MAX_CONSOLE_TEXT_LENGTH = 200;
141
+ /**
142
+ * Characters of a console message text in JSON output (`console`, `peek`)
143
+ */
144
+ export const MAX_CONSOLE_JSON_TEXT_LENGTH = 10_000;
123
145
  // ============================================================================
124
146
  // CHROME CDP BUFFER LIMITS
125
147
  // ============================================================================
@@ -139,6 +161,18 @@ export const CHROME_NETWORK_BUFFER_PER_RESOURCE = 10 * 1024 * 1024; // 10MB
139
161
  */
140
162
  export const CHROME_POST_DATA_LIMIT = 1 * 1024 * 1024; // 1MB
141
163
  // ============================================================================
164
+ // JSON LIST LIMITS
165
+ // ============================================================================
166
+ /** Matches `dom query` and `dom a11y query` list with `--json` and no `--limit` */
167
+ export const QUERY_JSON_LIST_LIMIT = 100;
168
+ /** Matches `dom layout` measures per command (the rest are counted as omitted) */
169
+ export const LAYOUT_ELEMENT_LIMIT = 100;
170
+ /**
171
+ * Requests listed in an action's result (a click that loads a page triggers
172
+ * its whole load); notable ones are kept before assets
173
+ */
174
+ export const MAX_TRIGGERED_REQUESTS = 50;
175
+ // ============================================================================
142
176
  // TIMEOUTS & INTERVALS
143
177
  // ============================================================================
144
178
  /**
@@ -245,6 +245,8 @@ export class SessionController {
245
245
  network: data.totalNetwork,
246
246
  console: data.totalConsole,
247
247
  ...(data.droppedConsole && { consoleDropped: data.droppedConsole }),
248
+ ...(data.droppedNetwork && { networkDropped: data.droppedNetwork }),
249
+ ...(data.evictedNetworkBodies && { networkBodiesEvicted: data.evictedNetworkBodies }),
248
250
  },
249
251
  currentNavigationId: data.currentNavigationId,
250
252
  ...(data.pageCrashedAt !== undefined && { pageCrashedAt: data.pageCrashedAt }),
@@ -83,7 +83,8 @@ export declare class Session {
83
83
  * Execute a registered command against this session. After a renderer
84
84
  * crash only {@link RUN_ON_CRASHED_PAGE} commands run (`bdg cdp` only for
85
85
  * {@link CRASH_SAFE_CDP} methods); others fail with exit 107 instead of
86
- * waiting for a page that cannot answer.
86
+ * waiting for a page that cannot answer. Commands that may change the page
87
+ * drop `dom inspect`'s kept matched rules ({@link withMatchedStylesReset}).
87
88
  *
88
89
  * @param name - Command name
89
90
  * @param params - Command parameters
@@ -11,8 +11,10 @@ import { connectCDP, navigateToTarget } from './cdpSetup.js';
11
11
  import { externalChromePort, findPageTarget, setupChromeConnection, } from './chromeConnection.js';
12
12
  import { startTelemetryCollectors } from './collectors.js';
13
13
  import { createCommandRegistry } from './commandRegistry.js';
14
+ import { withMatchedStylesReset } from './matchedStylesReset.js';
14
15
  import { teardownSession } from './teardown.js';
15
16
  import { CommandError } from '../../errors/index.js';
17
+ import { unknownSessionCommandMessage } from '../../errors/messages.js';
16
18
  import { applySessionEmulation } from '../../runtime/page/emulation.js';
17
19
  import { readPageLoadingState } from '../../runtime/page/loadingState.js';
18
20
  import { reapOrphanedChrome, removeSessionFiles } from '../../session/cleanup/staleSession.js';
@@ -151,7 +153,8 @@ export class Session {
151
153
  * Execute a registered command against this session. After a renderer
152
154
  * crash only {@link RUN_ON_CRASHED_PAGE} commands run (`bdg cdp` only for
153
155
  * {@link CRASH_SAFE_CDP} methods); others fail with exit 107 instead of
154
- * waiting for a page that cannot answer.
156
+ * waiting for a page that cannot answer. Commands that may change the page
157
+ * drop `dom inspect`'s kept matched rules ({@link withMatchedStylesReset}).
155
158
  *
156
159
  * @param name - Command name
157
160
  * @param params - Command parameters
@@ -168,7 +171,12 @@ export class Session {
168
171
  if (crashedAt !== undefined && needsPage) {
169
172
  return Promise.reject(pageCrashedCommandError(crashedAt));
170
173
  }
171
- return this.registry[name](this.cdp, params);
174
+ if (!Object.hasOwn(this.registry, name)) {
175
+ return Promise.reject(new CommandError(unknownSessionCommandMessage(name), {}, EXIT_CODES.INVALID_ARGUMENTS));
176
+ }
177
+ const handler = this.registry[name];
178
+ const cdp = this.cdp;
179
+ return withMatchedStylesReset(cdp, name, () => handler(cdp, params));
172
180
  }
173
181
  /**
174
182
  * Summary of this session for start responses.
@@ -1,9 +1,16 @@
1
1
  import type { DialogInfo } from '../../ipc/protocol/domTypes.js';
2
2
  import type { NavigationEvent } from '../../telemetry/navigation.js';
3
3
  import type { PendingRequest } from '../../telemetry/network.js';
4
+ import type { NetworkEvictions } from '../../telemetry/networkRetention.js';
4
5
  import type { CDPTarget, ConsoleMessage, NetworkRequest, TelemetryType, WebSocketConnection } from '../../types.js';
5
6
  export declare class TelemetryStore {
7
+ /**
8
+ * Finished requests, oldest first: the newest are kept, so the session's
9
+ * `networkEvictions.requestsDropped` oldest ones are gone
10
+ */
6
11
  readonly networkRequests: NetworkRequest[];
12
+ /** Requests dropped and response bodies evicted at the capture limits */
13
+ readonly networkEvictions: NetworkEvictions;
7
14
  /** Requests still in flight, keyed by CDP requestId */
8
15
  readonly pendingNetworkRequests: Map<string, PendingRequest>;
9
16
  readonly consoleMessages: ConsoleMessage[];
@@ -1,7 +1,13 @@
1
1
  import { MAX_CONSOLE_MESSAGES } from '../../constants.js';
2
2
  import { dialogConsoleText } from '../../ui/messages/commands.js';
3
3
  export class TelemetryStore {
4
+ /**
5
+ * Finished requests, oldest first: the newest are kept, so the session's
6
+ * `networkEvictions.requestsDropped` oldest ones are gone
7
+ */
4
8
  networkRequests = [];
9
+ /** Requests dropped and response bodies evicted at the capture limits */
10
+ networkEvictions = { requestsDropped: 0, bodiesEvicted: 0 };
5
11
  /** Requests still in flight, keyed by CDP requestId */
6
12
  pendingNetworkRequests = new Map();
7
13
  consoleMessages = [];
@@ -21,7 +21,7 @@ import { evaluateInBdgWorld, sendForBdgScript } from '../../runtime/page/bdgWorl
21
21
  import { emulatePage, pageAppearance } from '../../runtime/page/emulation.js';
22
22
  import { readDocumentReadyState } from '../../runtime/page/loadingState.js';
23
23
  import { navigatePage } from '../../runtime/page/navigation.js';
24
- import { skippedBodyReason } from '../../telemetry/network.js';
24
+ import { skippedBodyReason } from '../../telemetry/networkRetention.js';
25
25
  import { consoleMessageDroppedError } from '../../ui/messages/consoleMessages.js';
26
26
  import { sessionCommand } from '../../ui/messages/sessionCommand.js';
27
27
  import { EXIT_CODES } from '../../utils/exitCodes.js';
@@ -140,6 +140,20 @@ function webSocketAsRequest(connection) {
140
140
  webSocket: { frames: connection.frames, ...(closedTime !== undefined && { closedTime }) },
141
141
  };
142
142
  }
143
+ /**
144
+ * Status activity counts of what the network capture let go at its limits,
145
+ * left out when nothing was.
146
+ *
147
+ * @param store - Telemetry store
148
+ * @returns `networkRequestsDropped` and `networkBodiesEvicted` when non-zero
149
+ */
150
+ function networkEvictionActivity(store) {
151
+ const { requestsDropped, bodiesEvicted } = store.networkEvictions;
152
+ return {
153
+ ...(requestsDropped > 0 && { networkRequestsDropped: requestsDropped }),
154
+ ...(bodiesEvicted > 0 && { networkBodiesEvicted: bodiesEvicted }),
155
+ };
156
+ }
143
157
  /**
144
158
  * All captured network activity: finished and in-flight requests and
145
159
  * WebSocket connections, in start order.
@@ -319,6 +333,7 @@ export function createCommandRegistry(store, emulation) {
319
333
  const totalNetwork = allNetwork.length;
320
334
  const totalConsole = store.consoleMessages.length;
321
335
  const dropped = store.consoleDropped;
336
+ const { requestsDropped, bodiesEvicted } = store.networkEvictions;
322
337
  const networkBounds = calculateSliceBounds(totalNetwork, lastN, offset);
323
338
  const consoleBounds = calculateSliceBounds(totalConsole, lastN, offset);
324
339
  const recentNetwork = params.only === 'console'
@@ -347,6 +362,8 @@ export function createCommandRegistry(store, emulation) {
347
362
  totalNetwork,
348
363
  totalConsole,
349
364
  ...(dropped > 0 && { droppedConsole: dropped }),
365
+ ...(requestsDropped > 0 && { droppedNetwork: requestsDropped }),
366
+ ...(bodiesEvicted > 0 && { evictedNetworkBodies: bodiesEvicted }),
350
367
  hasMoreNetwork: networkBounds.start > 0,
351
368
  hasMoreConsole: consoleBounds.start > 0,
352
369
  });
@@ -377,12 +394,13 @@ export function createCommandRegistry(store, emulation) {
377
394
  : { crashedAt: store.pageCrashedAt }),
378
395
  },
379
396
  activeTelemetry: store.activeTelemetry,
380
- activity: filterDefined({
397
+ activity: {
381
398
  networkRequestsCaptured: store.networkRequests.length,
382
399
  consoleMessagesCaptured: store.consoleMessages.length,
383
- lastNetworkRequestAt: lastNetworkRequest?.timestamp,
384
- lastConsoleMessageAt: lastConsoleMessage?.timestamp,
385
- }),
400
+ ...(lastNetworkRequest && { lastNetworkRequestAt: lastNetworkRequest.timestamp }),
401
+ ...(lastConsoleMessage && { lastConsoleMessageAt: lastConsoleMessage.timestamp }),
402
+ ...networkEvictionActivity(store),
403
+ },
386
404
  navigationId: store.getCurrentNavigationId?.() ?? 0,
387
405
  };
388
406
  return result;
@@ -0,0 +1,26 @@
1
+ /**
2
+ * `dom inspect` keeps slow matched-rules answers for a few seconds, but page
3
+ * state such as `:checked`, `:hover` or `:focus` changes without any CDP
4
+ * event. Every daemon command that may change the page drops them, before
5
+ * and after it runs. Hooking the commands rather than the CDP methods they
6
+ * send keeps `dom inspect`'s own page scripts from clearing its answers, and
7
+ * listing the read-only commands makes a new command reset by default.
8
+ */
9
+ import type { CDPConnection } from '../../connection/cdp.js';
10
+ import type { CommandName } from '../../ipc/index.js';
11
+ /**
12
+ * Commands that leave the page as it is: they read it, or only the telemetry.
13
+ * Any command not listed (new ones included) resets by default.
14
+ */
15
+ export declare const KEEPS_MATCHED_STYLES: ReadonlySet<CommandName>;
16
+ /**
17
+ * Run a command, dropping the kept matched rules before and after it unless
18
+ * it leaves the page as it is.
19
+ *
20
+ * @param cdp - CDP connection
21
+ * @param name - Command name
22
+ * @param run - The command
23
+ * @returns Its result
24
+ */
25
+ export declare function withMatchedStylesReset<T>(cdp: CDPConnection, name: CommandName, run: () => Promise<T>): Promise<T>;
26
+ //# sourceMappingURL=matchedStylesReset.d.ts.map