browser-debugger-cli 0.12.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 (224) hide show
  1. package/.claude/skills/bdg/SKILL.md +100 -186
  2. package/README.md +5 -4
  3. package/dist/commands/cdp.d.ts +22 -1
  4. package/dist/commands/cdp.js +100 -43
  5. package/dist/commands/console.d.ts +12 -0
  6. package/dist/commands/console.js +67 -13
  7. package/dist/commands/dom/DomElementResolver.d.ts +3 -1
  8. package/dist/commands/dom/DomElementResolver.js +10 -3
  9. package/dist/commands/dom/a11y.d.ts +1 -1
  10. package/dist/commands/dom/a11y.js +23 -22
  11. package/dist/commands/dom/eval.d.ts +4 -2
  12. package/dist/commands/dom/eval.js +31 -7
  13. package/dist/commands/dom/form.js +10 -9
  14. package/dist/commands/dom/formInteraction.js +9 -8
  15. package/dist/commands/dom/get.js +32 -14
  16. package/dist/commands/dom/helpers/index.d.ts +1 -1
  17. package/dist/commands/dom/helpers/index.js +1 -1
  18. package/dist/commands/dom/helpers/query.d.ts +27 -3
  19. package/dist/commands/dom/helpers/query.js +152 -64
  20. package/dist/commands/dom/helpers/screenshot.js +13 -13
  21. package/dist/commands/dom/index.js +10 -3
  22. package/dist/commands/dom/query.d.ts +20 -2
  23. package/dist/commands/dom/query.js +39 -6
  24. package/dist/commands/dom/screenshot.js +3 -1
  25. package/dist/commands/dom/semanticUtils.d.ts +3 -2
  26. package/dist/commands/dom/semanticUtils.js +40 -9
  27. package/dist/commands/helpJson.d.ts +82 -19
  28. package/dist/commands/helpJson.js +112 -41
  29. package/dist/commands/helpTopic.d.ts +16 -1
  30. package/dist/commands/helpTopic.js +59 -1
  31. package/dist/commands/installSkill.d.ts +15 -5
  32. package/dist/commands/installSkill.js +86 -16
  33. package/dist/commands/network/list.js +65 -12
  34. package/dist/commands/optionBehaviors.d.ts +25 -2
  35. package/dist/commands/optionBehaviors.js +81 -46
  36. package/dist/commands/peek.js +3 -0
  37. package/dist/commands/shared/CommandRunner.js +13 -13
  38. package/dist/commands/shared/daemonErrorHandler.d.ts +5 -2
  39. package/dist/commands/shared/daemonErrorHandler.js +21 -10
  40. package/dist/commands/shared/dataFetcher.d.ts +14 -4
  41. package/dist/commands/shared/dataFetcher.js +20 -4
  42. package/dist/commands/shared/followMode.d.ts +9 -1
  43. package/dist/commands/shared/followMode.js +22 -4
  44. package/dist/commands/shared/handleValidationError.js +3 -3
  45. package/dist/commands/shared/optionTypes.d.ts +17 -3
  46. package/dist/commands/shared/outputFile.js +6 -1
  47. package/dist/commands/shared/startHelpers.js +3 -3
  48. package/dist/commands/start.d.ts +7 -5
  49. package/dist/commands/start.js +65 -21
  50. package/dist/commands/stop.d.ts +11 -0
  51. package/dist/commands/stop.js +24 -1
  52. package/dist/commands.js +1 -1
  53. package/dist/connection/cdp.d.ts +7 -0
  54. package/dist/connection/cdp.js +9 -0
  55. package/dist/connection/chromeIdentity.d.ts +8 -2
  56. package/dist/connection/chromeIdentity.js +85 -13
  57. package/dist/connection/launcher.js +3 -2
  58. package/dist/constants.d.ts +29 -1
  59. package/dist/constants.js +35 -1
  60. package/dist/daemon/SessionController.js +8 -1
  61. package/dist/daemon/launcher.d.ts +3 -2
  62. package/dist/daemon/launcher.js +47 -3
  63. package/dist/daemon/session/Session.d.ts +5 -1
  64. package/dist/daemon/session/Session.js +42 -3
  65. package/dist/daemon/session/TelemetryStore.d.ts +15 -1
  66. package/dist/daemon/session/TelemetryStore.js +19 -1
  67. package/dist/daemon/session/commandRegistry.js +52 -18
  68. package/dist/daemon/session/interactions.d.ts +2 -1
  69. package/dist/daemon/session/interactions.js +13 -1
  70. package/dist/daemon/session/matchedStylesReset.d.ts +26 -0
  71. package/dist/daemon/session/matchedStylesReset.js +46 -0
  72. package/dist/daemon/session/plugins.js +17 -2
  73. package/dist/daemon/session/teardown.js +1 -1
  74. package/dist/daemon/session/triggeredRequests.d.ts +0 -5
  75. package/dist/daemon/session/triggeredRequests.js +13 -7
  76. package/dist/daemon.js +2385 -1229
  77. package/dist/errors/messages.d.ts +62 -11
  78. package/dist/errors/messages.js +119 -22
  79. package/dist/index.js +14995 -9866
  80. package/dist/ipc/client.d.ts +18 -2
  81. package/dist/ipc/client.js +26 -5
  82. package/dist/ipc/protocol/auditTypes.d.ts +8 -2
  83. package/dist/ipc/protocol/commands.d.ts +16 -0
  84. package/dist/ipc/protocol/domTypes.d.ts +12 -0
  85. package/dist/ipc/protocol/inspectTypes.d.ts +7 -2
  86. package/dist/ipc/session/types.d.ts +7 -1
  87. package/dist/program.d.ts +14 -0
  88. package/dist/program.js +53 -0
  89. package/dist/runtime/dom/actionEffects.d.ts +5 -1
  90. package/dist/runtime/dom/actionEffects.js +26 -14
  91. package/dist/runtime/dom/audit.js +3 -2
  92. package/dist/runtime/dom/auditModel.js +6 -1
  93. package/dist/runtime/dom/auditScripts.d.ts +9 -3
  94. package/dist/runtime/dom/auditScripts.js +41 -5
  95. package/dist/runtime/dom/elementGeometry.d.ts +33 -3
  96. package/dist/runtime/dom/elementGeometry.js +44 -19
  97. package/dist/runtime/dom/elementInfo.d.ts +76 -18
  98. package/dist/runtime/dom/elementInfo.js +190 -40
  99. package/dist/runtime/dom/evalHelpers.d.ts +12 -2
  100. package/dist/runtime/dom/evalHelpers.js +67 -7
  101. package/dist/runtime/dom/formDiscovery.d.ts +6 -2
  102. package/dist/runtime/dom/formDiscovery.js +20 -3
  103. package/dist/runtime/dom/formFillHelpers/fill.js +7 -11
  104. package/dist/runtime/dom/formFillHelpers/pressKey.js +2 -2
  105. package/dist/runtime/dom/formFillHelpers/shared.d.ts +16 -10
  106. package/dist/runtime/dom/formFillHelpers/shared.js +19 -52
  107. package/dist/runtime/dom/formSubmitHelpers.js +4 -3
  108. package/dist/runtime/dom/frameLayout.js +1 -0
  109. package/dist/runtime/dom/frameScopedConnection.d.ts +7 -0
  110. package/dist/runtime/dom/frameScopedConnection.js +2 -2
  111. package/dist/runtime/dom/inspect.d.ts +17 -3
  112. package/dist/runtime/dom/inspect.js +45 -32
  113. package/dist/runtime/dom/inspectAllStyles.js +1 -0
  114. package/dist/runtime/dom/inspectHints.d.ts +1 -1
  115. package/dist/runtime/dom/inspectModel.d.ts +5 -4
  116. package/dist/runtime/dom/inspectModel.js +7 -3
  117. package/dist/runtime/dom/inspectPaintModel.d.ts +2 -0
  118. package/dist/runtime/dom/inspectPaintModel.js +3 -1
  119. package/dist/runtime/dom/inspectRules.d.ts +29 -3
  120. package/dist/runtime/dom/inspectRules.js +205 -11
  121. package/dist/runtime/dom/inspectScripts.d.ts +29 -2
  122. package/dist/runtime/dom/inspectScripts.js +49 -10
  123. package/dist/runtime/dom/layout.d.ts +0 -2
  124. package/dist/runtime/dom/layout.js +10 -9
  125. package/dist/runtime/dom/reactEventHelpers.d.ts +17 -4
  126. package/dist/runtime/dom/reactEventHelpers.js +71 -28
  127. package/dist/runtime/dom/targetNode.d.ts +27 -10
  128. package/dist/runtime/dom/targetNode.js +283 -16
  129. package/dist/runtime/dom/wait.js +2 -1
  130. package/dist/runtime/page/bdgWorld.d.ts +57 -0
  131. package/dist/runtime/page/bdgWorld.js +180 -0
  132. package/dist/runtime/page/replacedBuiltins.d.ts +28 -0
  133. package/dist/runtime/page/replacedBuiltins.js +136 -0
  134. package/dist/session/QueryCacheManager.d.ts +4 -1
  135. package/dist/session/QueryCacheManager.js +5 -2
  136. package/dist/session/chrome.d.ts +4 -1
  137. package/dist/session/chrome.js +7 -1
  138. package/dist/session/cleanup/staleSession.d.ts +21 -4
  139. package/dist/session/cleanup/staleSession.js +79 -9
  140. package/dist/session/cleanup/userCommands.d.ts +4 -1
  141. package/dist/session/cleanup/userCommands.js +10 -5
  142. package/dist/session/daemonSocket.d.ts +10 -0
  143. package/dist/session/daemonSocket.js +22 -0
  144. package/dist/session/lastSession.d.ts +6 -3
  145. package/dist/session/lastSession.js +11 -5
  146. package/dist/session/paths.d.ts +3 -1
  147. package/dist/session/paths.js +5 -5
  148. package/dist/session/portClaims.js +4 -3
  149. package/dist/session/sessionList.d.ts +13 -5
  150. package/dist/session/sessionList.js +31 -7
  151. package/dist/telemetry/a11y.d.ts +15 -1
  152. package/dist/telemetry/a11y.js +85 -2
  153. package/dist/telemetry/console.d.ts +2 -1
  154. package/dist/telemetry/console.js +30 -21
  155. package/dist/telemetry/har/builder.js +1 -1
  156. package/dist/telemetry/network.d.ts +13 -16
  157. package/dist/telemetry/network.js +30 -52
  158. package/dist/telemetry/networkRetention.d.ts +83 -0
  159. package/dist/telemetry/networkRetention.js +117 -0
  160. package/dist/telemetry/pageCrash.d.ts +26 -0
  161. package/dist/telemetry/pageCrash.js +53 -0
  162. package/dist/types.d.ts +42 -0
  163. package/dist/ui/OutputBuilder.d.ts +10 -0
  164. package/dist/ui/OutputBuilder.js +12 -0
  165. package/dist/ui/formatters/a11y.d.ts +5 -7
  166. package/dist/ui/formatters/a11y.js +7 -61
  167. package/dist/ui/formatters/audit.js +14 -5
  168. package/dist/ui/formatters/cdp.d.ts +138 -0
  169. package/dist/ui/formatters/cdp.js +131 -0
  170. package/dist/ui/formatters/console/chronological.js +7 -5
  171. package/dist/ui/formatters/console/follow.d.ts +5 -2
  172. package/dist/ui/formatters/console/follow.js +7 -4
  173. package/dist/ui/formatters/console/json.d.ts +4 -7
  174. package/dist/ui/formatters/console/json.js +16 -14
  175. package/dist/ui/formatters/console/shared.d.ts +47 -2
  176. package/dist/ui/formatters/console/shared.js +33 -0
  177. package/dist/ui/formatters/console/summarize.d.ts +9 -2
  178. package/dist/ui/formatters/console/summarize.js +57 -11
  179. package/dist/ui/formatters/console.d.ts +3 -2
  180. package/dist/ui/formatters/console.js +8 -10
  181. package/dist/ui/formatters/details.js +4 -2
  182. package/dist/ui/formatters/dom.d.ts +14 -5
  183. package/dist/ui/formatters/dom.js +30 -13
  184. package/dist/ui/formatters/helpFormatters.js +1 -1
  185. package/dist/ui/formatters/inspect.js +9 -3
  186. package/dist/ui/formatters/installSkill.d.ts +9 -1
  187. package/dist/ui/formatters/installSkill.js +32 -6
  188. package/dist/ui/formatters/layout.js +4 -2
  189. package/dist/ui/formatters/longValues.d.ts +14 -0
  190. package/dist/ui/formatters/longValues.js +23 -0
  191. package/dist/ui/formatters/networkList.d.ts +8 -2
  192. package/dist/ui/formatters/networkList.js +11 -3
  193. package/dist/ui/formatters/preview.d.ts +6 -1
  194. package/dist/ui/formatters/preview.js +67 -15
  195. package/dist/ui/formatters/sessions.d.ts +2 -2
  196. package/dist/ui/formatters/sessions.js +9 -2
  197. package/dist/ui/formatters/status.js +7 -0
  198. package/dist/ui/formatters/triggeredRequests.js +2 -1
  199. package/dist/ui/logging/logger.d.ts +1 -1
  200. package/dist/ui/messages/chrome.d.ts +20 -1
  201. package/dist/ui/messages/chrome.js +29 -3
  202. package/dist/ui/messages/commands.d.ts +153 -12
  203. package/dist/ui/messages/commands.js +198 -15
  204. package/dist/ui/messages/consoleMessages.d.ts +24 -0
  205. package/dist/ui/messages/consoleMessages.js +32 -0
  206. package/dist/ui/messages/networkMessages.d.ts +24 -0
  207. package/dist/ui/messages/networkMessages.js +45 -0
  208. package/dist/ui/messages/preview.d.ts +6 -0
  209. package/dist/ui/messages/preview.js +9 -1
  210. package/dist/ui/messages/session.d.ts +13 -2
  211. package/dist/ui/messages/session.js +22 -3
  212. package/dist/utils/directories.d.ts +34 -0
  213. package/dist/utils/directories.js +88 -0
  214. package/dist/utils/display.d.ts +16 -0
  215. package/dist/utils/display.js +42 -0
  216. package/dist/utils/exitCodes.d.ts +1 -0
  217. package/dist/utils/exitCodes.js +6 -0
  218. package/dist/utils/http.d.ts +9 -2
  219. package/dist/utils/http.js +4 -3
  220. package/dist/utils/process.d.ts +12 -0
  221. package/dist/utils/process.js +25 -0
  222. package/dist/utils/strings.d.ts +19 -0
  223. package/dist/utils/strings.js +16 -0
  224. package/package.json +2 -2
@@ -4,13 +4,15 @@
4
4
  */
5
5
  import type { StatusResponseData } from '../ipc/session/queries.js';
6
6
  import { type SocketProbeResult } from './daemonSocket.js';
7
+ import { type UnexpectedEndReason } from './lastSession.js';
7
8
  import { type SessionDirEntry } from './paths.js';
8
9
  /**
9
10
  * State of a session: its daemon runs (`active`, `starting`, `ending`,
10
- * `unresponsive`), or died and left its Chrome running (`crashed`) or only
11
- * files (`stale`).
11
+ * `unresponsive`), died and left its Chrome running (`crashed`) or only
12
+ * files (`stale`), or exited after the session ended without `bdg stop`
13
+ * (`ended`: Chrome crashed or was closed, the page was closed, `--timeout`).
12
14
  */
13
- export type RunningSessionState = 'active' | 'starting' | 'ending' | 'unresponsive' | 'crashed' | 'stale';
15
+ export type RunningSessionState = 'active' | 'starting' | 'ending' | 'unresponsive' | 'crashed' | 'stale' | 'ended';
14
16
  /**
15
17
  * One session in the list.
16
18
  */
@@ -27,6 +29,10 @@ export interface RunningSessionInfo {
27
29
  chromePid?: number;
28
30
  /** Command that cleans up a crashed or stale session */
29
31
  cleanup?: string;
32
+ /** Why an `ended` session ended */
33
+ endReason?: UnexpectedEndReason;
34
+ /** When an `ended` session ended (epoch ms) */
35
+ endedAt?: number;
30
36
  }
31
37
  /**
32
38
  * Summarize a daemon's status response.
@@ -38,8 +44,10 @@ export interface RunningSessionInfo {
38
44
  export declare function toRunningSession(name: string | null, data: StatusResponseData): RunningSessionInfo;
39
45
  /**
40
46
  * A session whose daemon is gone: `crashed` while the Chrome bdg launched for
41
- * it still runs, `stale` when only its files are left. Nothing is changed on
42
- * disk; `cleanup` names the command that removes them.
47
+ * it still runs, `stale` when only its files are left, `ended` when it left
48
+ * only the record of an end without `bdg stop` ({@link describeEnded}).
49
+ * Nothing is changed on disk; `cleanup` names the command that removes
50
+ * leftover files.
43
51
  *
44
52
  * @param entry - Session directory
45
53
  * @param probe - Result of probing its daemon socket
@@ -6,6 +6,7 @@ import * as fs from 'fs';
6
6
  import { getStatus } from '../ipc/client.js';
7
7
  import { isSessionChrome, readLiveDaemonPid } from './cleanup/staleSession.js';
8
8
  import { probeDaemonSocket } from './daemonSocket.js';
9
+ import { readLastSessionEnd } from './lastSession.js';
9
10
  import { SESSION_STATE_FILES, getNamedSessionDir, listSessionDirs, sessionFilePathIn, } from './paths.js';
10
11
  import { readPidFromFile } from './pid.js';
11
12
  import { readPortFile } from './portClaims.js';
@@ -58,9 +59,10 @@ async function describeSession(entry) {
58
59
  return { name, state: 'unresponsive' };
59
60
  }
60
61
  /**
61
- * A session whose daemon socket does not answer: `starting` while its daemon
62
- * (from daemon.pid, verified by command line) runs but has not bound the
63
- * socket yet, otherwise see {@link describeLeftovers}.
62
+ * A session whose daemon socket does not answer: `ending` while its daemon
63
+ * (from daemon.pid, verified by command line) still runs, since a daemon
64
+ * writes daemon.pid only after it opened its socket and removes the socket
65
+ * first when it shuts down; otherwise see {@link describeLeftovers}.
64
66
  *
65
67
  * @param entry - Session directory
66
68
  * @param probe - Result of probing its daemon socket
@@ -69,13 +71,15 @@ async function describeSession(entry) {
69
71
  function describeWithoutSocket(entry, probe) {
70
72
  const daemonPid = readLiveDaemonPid(entry.dir);
71
73
  if (daemonPid !== null)
72
- return { name: entry.name, state: 'starting', daemonPid };
74
+ return { name: entry.name, state: 'ending', daemonPid };
73
75
  return describeLeftovers(entry, probe);
74
76
  }
75
77
  /**
76
78
  * A session whose daemon is gone: `crashed` while the Chrome bdg launched for
77
- * it still runs, `stale` when only its files are left. Nothing is changed on
78
- * disk; `cleanup` names the command that removes them.
79
+ * it still runs, `stale` when only its files are left, `ended` when it left
80
+ * only the record of an end without `bdg stop` ({@link describeEnded}).
81
+ * Nothing is changed on disk; `cleanup` names the command that removes
82
+ * leftover files.
79
83
  *
80
84
  * @param entry - Session directory
81
85
  * @param probe - Result of probing its daemon socket
@@ -87,7 +91,7 @@ export function describeLeftovers({ name, dir }, probe) {
87
91
  const leftover = probe === 'stale' ||
88
92
  SESSION_STATE_FILES.some((type) => fs.existsSync(sessionFilePathIn(dir, type)));
89
93
  if (!orphan && !leftover)
90
- return null;
94
+ return describeEnded({ name, dir });
91
95
  const port = leftoverPort(dir);
92
96
  return {
93
97
  name,
@@ -97,6 +101,26 @@ export function describeLeftovers({ name, dir }, probe) {
97
101
  cleanup: sessionCommand('bdg cleanup', name),
98
102
  };
99
103
  }
104
+ /**
105
+ * A session that ended without `bdg stop` and whose daemon exited cleanly:
106
+ * `ended`, with why and when, until the session starts again or
107
+ * `bdg cleanup` clears the record.
108
+ *
109
+ * @param entry - Session directory
110
+ * @returns Session info, or null when the directory holds no such record
111
+ */
112
+ function describeEnded({ name, dir }) {
113
+ const end = readLastSessionEnd(dir);
114
+ if (end === null)
115
+ return null;
116
+ return {
117
+ name,
118
+ state: 'ended',
119
+ endReason: end.reason,
120
+ endedAt: end.endedAt,
121
+ cleanup: sessionCommand('bdg cleanup', name),
122
+ };
123
+ }
100
124
  /**
101
125
  * The port a dead session used: from its metadata, else its `port.txt`.
102
126
  *
@@ -1,5 +1,5 @@
1
1
  import type { Protocol } from '../connection/typed-cdp.js';
2
- import type { A11yNode, A11yTree, A11yQueryPattern, A11yQueryResult, NodeRef } from '../types.js';
2
+ import type { A11yNode, A11yTree, A11yQueryPattern, A11yQueryResult, ListedA11yTree, NodeRef } from '../types.js';
3
3
  /**
4
4
  * Builds accessibility tree from raw CDP nodes.
5
5
  *
@@ -14,6 +14,20 @@ import type { A11yNode, A11yTree, A11yQueryPattern, A11yQueryResult, NodeRef } f
14
14
  * @throws Error if no root node found
15
15
  */
16
16
  export declare function buildTreeFromRawNodes(rawNodes: Protocol.Accessibility.AXNode[]): A11yTree;
17
+ /**
18
+ * The nodes `dom a11y tree` lists, depth-first from the root (then nodes not
19
+ * under it, such as frame content): the first `limit` (0 = all) no deeper
20
+ * than `maxDepth`. Noise ({@link isTreeNoise}) is counted as skipped and its
21
+ * children move up a level, so the budget goes to meaningful nodes; nodes cut
22
+ * by the limit or depth are counted as omitted. Every node of the tree is
23
+ * listed, omitted or skipped.
24
+ *
25
+ * @param tree - Accessibility tree
26
+ * @param limit - Nodes to list (0 = all listable ones)
27
+ * @param maxDepth - Deepest level to list (0 = root only; undefined = all)
28
+ * @returns Listed nodes with their depth, the tree size, and how many were cut or skipped
29
+ */
30
+ export declare function listA11yTree(tree: A11yTree, limit: number, maxDepth?: number): ListedA11yTree;
17
31
  /**
18
32
  * Collects the full accessibility tree from the page via IPC.
19
33
  *
@@ -1,6 +1,6 @@
1
1
  import { CommandError } from '../errors/index.js';
2
2
  import { unknownQueryFieldError } from '../errors/messages.js';
3
- import { callCDP } from '../ipc/client.js';
3
+ import { callBdgScript, callCDP } from '../ipc/client.js';
4
4
  import { MASKED_VALUE, SENSITIVE_FIELD_JS } from '../runtime/dom/elementInfo.js';
5
5
  import { childFrameIds } from '../runtime/dom/frameLayout.js';
6
6
  import { ConcurrencyLimiter } from '../utils/concurrency.js';
@@ -47,6 +47,89 @@ export function buildTreeFromRawNodes(rawNodes) {
47
47
  const root = nodes.get(rootRaw.nodeId);
48
48
  return { root, nodes, count: nodes.size };
49
49
  }
50
+ /** Roles that only lay out their children and say nothing themselves */
51
+ const LAYOUT_ROLES = new Set([
52
+ 'generic',
53
+ 'none',
54
+ 'presentation',
55
+ 'LayoutTable',
56
+ 'LayoutTableRow',
57
+ 'LayoutTableCell',
58
+ ]);
59
+ /**
60
+ * Whether a node is left out of `dom a11y tree`: text boxes, blank text,
61
+ * text that repeats its parent's name, and nameless layout wrappers.
62
+ *
63
+ * @param node - Accessibility node
64
+ * @param parentName - Name of the nearest named ancestor
65
+ * @returns True when the node says nothing of its own
66
+ */
67
+ function isTreeNoise(node, parentName) {
68
+ return (node.role === 'InlineTextBox' ||
69
+ (node.role === 'StaticText' && (node.name === parentName || !node.name?.trim())) ||
70
+ (LAYOUT_ROLES.has(node.role) && !node.name));
71
+ }
72
+ /**
73
+ * The nodes `dom a11y tree` lists, depth-first from the root (then nodes not
74
+ * under it, such as frame content): the first `limit` (0 = all) no deeper
75
+ * than `maxDepth`. Noise ({@link isTreeNoise}) is counted as skipped and its
76
+ * children move up a level, so the budget goes to meaningful nodes; nodes cut
77
+ * by the limit or depth are counted as omitted. Every node of the tree is
78
+ * listed, omitted or skipped.
79
+ *
80
+ * @param tree - Accessibility tree
81
+ * @param limit - Nodes to list (0 = all listable ones)
82
+ * @param maxDepth - Deepest level to list (0 = root only; undefined = all)
83
+ * @returns Listed nodes with their depth, the tree size, and how many were cut or skipped
84
+ */
85
+ export function listA11yTree(tree, limit, maxDepth) {
86
+ const nodes = [];
87
+ const visited = new Set();
88
+ let omitted = 0;
89
+ let skipped = 0;
90
+ const visit = (node, depth, parentName) => {
91
+ if (visited.has(node.nodeId))
92
+ return;
93
+ visited.add(node.nodeId);
94
+ const listable = !isTreeNoise(node, parentName);
95
+ if (listable) {
96
+ const fits = (limit === 0 || nodes.length < limit) && (maxDepth ?? depth) >= depth;
97
+ if (fits)
98
+ nodes.push(listedNode(node, depth));
99
+ else
100
+ omitted++;
101
+ }
102
+ else {
103
+ skipped++;
104
+ }
105
+ for (const childId of node.childIds ?? []) {
106
+ const child = tree.nodes.get(childId);
107
+ if (child)
108
+ visit(child, listable ? depth + 1 : depth, node.name ?? parentName);
109
+ }
110
+ };
111
+ visit(tree.root, 0, undefined);
112
+ for (const node of tree.nodes.values())
113
+ visit(node, 0, undefined);
114
+ return {
115
+ nodes,
116
+ count: tree.count,
117
+ ...(omitted > 0 && { omitted }),
118
+ ...(skipped > 0 && { skipped }),
119
+ };
120
+ }
121
+ /**
122
+ * A node as the tree lists it: its depth instead of child ids (its children
123
+ * follow it, one level deeper).
124
+ *
125
+ * @param node - Accessibility node
126
+ * @param depth - Its level in the listing
127
+ * @returns Listed node
128
+ */
129
+ function listedNode(node, depth) {
130
+ const { childIds: _childIds, ...rest } = node;
131
+ return { ...rest, depth };
132
+ }
50
133
  /**
51
134
  * Ids of a node's children as shown: ignored children are replaced by their
52
135
  * own non-ignored descendants; ids missing from the tree are dropped.
@@ -118,7 +201,7 @@ async function sensitiveNodeIds(tree) {
118
201
  */
119
202
  async function isSensitiveField(backendNodeId) {
120
203
  const objectGroup = 'bdg-a11y-secret';
121
- const resolved = await callCDP('DOM.resolveNode', { backendNodeId, objectGroup });
204
+ const resolved = await callBdgScript('DOM.resolveNode', { backendNodeId, objectGroup });
122
205
  const objectId = resolved.data?.result?.object
123
206
  ?.objectId;
124
207
  if (!objectId)
@@ -31,7 +31,8 @@ export declare function formatExceptionText(details: Protocol.Runtime.ExceptionD
31
31
  * @param messages - Array to populate with console messages
32
32
  * @param includeAll - If true, disable default pattern filtering
33
33
  * @param getCurrentNavigationId - Function to get current navigation ID
34
+ * @param onDropped - Called for each message dropped at the limit (the oldest go first)
34
35
  * @returns Cleanup function to remove event handlers
35
36
  */
36
- export declare function startConsoleCollection(cdp: CDPConnection, messages: ConsoleMessage[], includeAll?: boolean, getCurrentNavigationId?: () => number): Promise<CleanupFunction>;
37
+ export declare function startConsoleCollection(cdp: CDPConnection, messages: ConsoleMessage[], includeAll?: boolean, getCurrentNavigationId?: () => number, onDropped?: () => void): Promise<CleanupFunction>;
37
38
  //# sourceMappingURL=console.d.ts.map
@@ -75,15 +75,23 @@ function createMessage(type, text, timestamp, args, context) {
75
75
  };
76
76
  }
77
77
  /**
78
- * Insert a message in timestamp order.
79
- * Messages are kept sorted by timestamp to handle async expansion delays.
78
+ * Insert a message in timestamp order (async expansion can deliver a message
79
+ * late). At {@link MAX_CONSOLE_MESSAGES} the oldest message is dropped to
80
+ * make room, so the newest are kept; a late message older than all kept ones
81
+ * is dropped itself.
80
82
  *
81
- * @returns False when the message limit is reached (the message is dropped)
83
+ * @param messages - Messages, oldest first (updated)
84
+ * @param message - Message to add
85
+ * @param onDropped - Called for each message dropped
86
+ * @returns False when the message itself was dropped
82
87
  */
83
- function insertMessageByTimestamp(messages, message) {
88
+ function insertMessageByTimestamp(messages, message, onDropped) {
84
89
  if (messages.length >= MAX_CONSOLE_MESSAGES) {
85
- log.debug(`Warning: Console message limit reached (${MAX_CONSOLE_MESSAGES})`);
86
- return false;
90
+ const oldest = messages[0];
91
+ onDropped();
92
+ if (oldest && message.timestamp < oldest.timestamp)
93
+ return false;
94
+ messages.shift();
87
95
  }
88
96
  const insertIndex = findInsertIndex(messages, message.timestamp);
89
97
  messages.splice(insertIndex, 0, message);
@@ -111,32 +119,31 @@ function findInsertIndex(messages, timestamp) {
111
119
  /**
112
120
  * Handle a console API call event with object expansion.
113
121
  */
114
- function handleConsoleAPICall(cdp, messages, params, context, includeAll) {
122
+ function handleConsoleAPICall(cdp, insert, params, context, includeAll) {
115
123
  const basicText = formatConsoleArgs(params.args);
116
124
  if (shouldExcludeConsoleMessage(basicText, params.type, includeAll)) {
117
125
  return;
118
126
  }
119
127
  if (hasArgsNeedingExpansion(params.args) && !usesFormatSpecifiers(params.args)) {
120
- handleExpandableMessage(cdp, messages, params, context, basicText);
128
+ handleExpandableMessage(cdp, insert, params, context, basicText);
121
129
  }
122
130
  else {
123
- const message = createMessage(params.type, basicText, params.timestamp, params.args, context);
124
- insertMessageByTimestamp(messages, message);
131
+ insert(createMessage(params.type, basicText, params.timestamp, params.args, context));
125
132
  }
126
133
  }
127
134
  /**
128
135
  * Handle a message with expandable objects.
129
136
  */
130
- function handleExpandableMessage(cdp, messages, params, context, fallbackText) {
137
+ function handleExpandableMessage(cdp, insert, params, context, fallbackText) {
131
138
  void expandConsoleArgs(cdp, params.args)
132
139
  .then((expandedText) => {
133
140
  const message = createMessage(params.type, expandedText, params.timestamp, params.args, context);
134
- insertMessageByTimestamp(messages, message);
141
+ insert(message);
135
142
  })
136
143
  .catch((error) => {
137
144
  log.debug(`Object expansion failed, using basic text: ${String(error)}`);
138
145
  const message = createMessage(params.type, fallbackText, params.timestamp, params.args, context);
139
- insertMessageByTimestamp(messages, message);
146
+ insert(message);
140
147
  });
141
148
  }
142
149
  /**
@@ -178,14 +185,14 @@ export function formatExceptionText(details) {
178
185
  *
179
186
  * @returns The message added, undefined when filtered out or over the limit
180
187
  */
181
- function handleExceptionThrown(messages, params, context, includeAll) {
188
+ function handleExceptionThrown(insert, params, context, includeAll) {
182
189
  const exception = params.exceptionDetails;
183
190
  const text = formatExceptionText(exception);
184
191
  if (shouldExcludeConsoleMessage(text, 'error', includeAll)) {
185
192
  return undefined;
186
193
  }
187
194
  const message = createMessage('error', text, params.timestamp, undefined, context);
188
- return insertMessageByTimestamp(messages, message) ? message : undefined;
195
+ return insert(message) ? message : undefined;
189
196
  }
190
197
  /** CDP's text of an unhandled promise rejection (the only kind Chrome revokes) */
191
198
  const UNHANDLED_REJECTION_TEXT = 'Uncaught (in promise)';
@@ -270,7 +277,7 @@ class RevocableRejections {
270
277
  * Worker entries are skipped: workers are attached directly, so their console
271
278
  * calls already arrive (with arguments) from the worker's own session.
272
279
  */
273
- function handleLogEntry(messages, entry, navigationId, includeAll) {
280
+ function handleLogEntry(insert, entry, navigationId, includeAll) {
274
281
  const type = LOG_LEVEL_TYPES[entry.level];
275
282
  if (entry.source === 'worker' || shouldExcludeConsoleMessage(entry.text, type, includeAll)) {
276
283
  return;
@@ -287,7 +294,7 @@ function handleLogEntry(messages, entry, navigationId, includeAll) {
287
294
  stackTrace: convertStackTrace(entry.stackTrace) ?? location,
288
295
  source: entry.source,
289
296
  };
290
- insertMessageByTimestamp(messages, createMessage(type, entry.text, entry.timestamp, undefined, context));
297
+ insert(createMessage(type, entry.text, entry.timestamp, undefined, context));
291
298
  }
292
299
  /**
293
300
  * Enable console and log events on an attached target's session.
@@ -325,17 +332,19 @@ async function startOptionalSources(cdp, typed) {
325
332
  * @param messages - Array to populate with console messages
326
333
  * @param includeAll - If true, disable default pattern filtering
327
334
  * @param getCurrentNavigationId - Function to get current navigation ID
335
+ * @param onDropped - Called for each message dropped at the limit (the oldest go first)
328
336
  * @returns Cleanup function to remove event handlers
329
337
  */
330
- export async function startConsoleCollection(cdp, messages, includeAll = false, getCurrentNavigationId) {
338
+ export async function startConsoleCollection(cdp, messages, includeAll = false, getCurrentNavigationId, onDropped = () => undefined) {
331
339
  const registry = new CDPHandlerRegistry();
332
340
  const typed = new TypedCDPConnection(cdp);
341
+ const insert = (message) => insertMessageByTimestamp(messages, message, onDropped);
333
342
  registry.registerTyped(typed, 'Runtime.consoleAPICalled', (params, sessionId) => {
334
343
  const context = {
335
344
  navigationId: getCurrentNavigationId?.(),
336
345
  stackTrace: convertStackTrace(params.stackTrace),
337
346
  };
338
- handleConsoleAPICall(senderFor(cdp, sessionId), messages, params, context, includeAll);
347
+ handleConsoleAPICall(senderFor(cdp, sessionId), insert, params, context, includeAll);
339
348
  });
340
349
  const rejections = new RevocableRejections();
341
350
  registry.registerTyped(typed, 'Runtime.exceptionThrown', (params, sessionId) => {
@@ -343,7 +352,7 @@ export async function startConsoleCollection(cdp, messages, includeAll = false,
343
352
  navigationId: getCurrentNavigationId?.(),
344
353
  stackTrace: convertStackTrace(params.exceptionDetails.stackTrace),
345
354
  };
346
- const message = handleExceptionThrown(messages, params, context, includeAll);
355
+ const message = handleExceptionThrown(insert, params, context, includeAll);
347
356
  if (message)
348
357
  rejections.track(params.exceptionDetails, message, sessionId);
349
358
  });
@@ -357,7 +366,7 @@ export async function startConsoleCollection(cdp, messages, includeAll = false,
357
366
  rejections.forgetSession(sessionId);
358
367
  });
359
368
  registry.registerTyped(typed, 'Log.entryAdded', ({ entry }) => {
360
- handleLogEntry(messages, entry, getCurrentNavigationId?.(), includeAll);
369
+ handleLogEntry(insert, entry, getCurrentNavigationId?.(), includeAll);
361
370
  });
362
371
  try {
363
372
  await typed.send('Runtime.enable', {});
@@ -2,7 +2,7 @@
2
2
  * HAR (HTTP Archive) builder for transforming network telemetry to HAR 1.2 format.
3
3
  */
4
4
  import { createRequire } from 'node:module';
5
- import { skippedBodyReason } from '../network.js';
5
+ import { skippedBodyReason } from '../networkRetention.js';
6
6
  /**
7
7
  * Loads Node builtins on first use: importing `node:http` in an ES module
8
8
  * reads all its exports, which loads undici and zlib (about 8 ms of every CLI
@@ -1,19 +1,6 @@
1
1
  import type { CDPConnection } from '../connection/cdp.js';
2
2
  import type { NetworkRequest, WebSocketConnection, CleanupFunction } from '../types.js';
3
- /**
4
- * Placeholder stored instead of a response body that was not fetched.
5
- *
6
- * @param reason - Why the body was skipped
7
- * @returns Placeholder text shown by `bdg details`
8
- */
9
- export declare function skippedBodyPlaceholder(reason: string): string;
10
- /**
11
- * Extract the reason from a skipped-body placeholder.
12
- *
13
- * @param body - Stored response body
14
- * @returns Reason if `body` is a placeholder, otherwise undefined
15
- */
16
- export declare function skippedBodyReason(body: string | undefined): string | undefined;
3
+ import { type NetworkEvictions } from './networkRetention.js';
17
4
  /**
18
5
  * A request that has started but not finished or failed yet.
19
6
  */
@@ -41,6 +28,12 @@ export interface NetworkCollectionOptions {
41
28
  networkInclude?: string[];
42
29
  networkExclude?: string[];
43
30
  maxBodySize?: number;
31
+ /** Finished requests kept at most; past it the oldest are dropped (default {@link MAX_NETWORK_REQUESTS}) */
32
+ maxRequests?: number;
33
+ /** Total size of stored response bodies; past it the oldest are evicted (default {@link MAX_TOTAL_BODY_BYTES}) */
34
+ maxTotalBodyBytes?: number;
35
+ /** Counters of dropped requests and evicted bodies; otherwise the collector keeps private ones */
36
+ evictions?: NetworkEvictions | undefined;
44
37
  getCurrentNavigationId?: (() => number) | undefined;
45
38
  }
46
39
  /**
@@ -56,8 +49,12 @@ export interface NetworkCollectionOptions {
56
49
  *
57
50
  * @remarks
58
51
  * - Chrome buffer limits: 50MB total, 10MB per resource, 1MB POST data (with fallback)
59
- * - Stale requests (incomplete after 60s) are removed from tracking but NOT added to output
60
- * - Request limit of 10,000 prevents memory issues in long-running sessions
52
+ * - The newest 10,000 finished requests are kept: past that the oldest finished
53
+ * ones are dropped (counted in `evictions.requestsDropped`); requests in flight
54
+ * are tracked separately and never dropped mid-flight
55
+ * - Stored response bodies total at most 100MB: past that the oldest bodies are
56
+ * replaced by a placeholder (counted in `evictions.bodiesEvicted`), their
57
+ * request metadata stays
61
58
  * - Response bodies are automatically skipped for images, fonts, CSS, and source maps (see DEFAULT_SKIP_BODY_PATTERNS)
62
59
  * - Response bodies larger than 5MB are skipped with a placeholder message
63
60
  * - By default, common tracking/analytics domains are filtered out (use includeAll to disable)
@@ -1,12 +1,13 @@
1
1
  import { CDPHandlerRegistry } from '../connection/handlers.js';
2
2
  import { TypedCDPConnection } from '../connection/typed-cdp.js';
3
- import { MAX_NETWORK_REQUESTS, MAX_RESPONSE_SIZE, CHROME_NETWORK_BUFFER_TOTAL, CHROME_NETWORK_BUFFER_PER_RESOURCE, CHROME_POST_DATA_LIMIT, } from '../constants.js';
3
+ import { MAX_NETWORK_REQUESTS, MAX_RESPONSE_SIZE, MAX_TOTAL_BODY_BYTES, CHROME_NETWORK_BUFFER_TOTAL, CHROME_NETWORK_BUFFER_PER_RESOURCE, CHROME_POST_DATA_LIMIT, } from '../constants.js';
4
4
  import { attachChildTargets } from './attachedTargets.js';
5
5
  import { createLogger } from '../ui/logging/index.js';
6
6
  import { getErrorMessage } from '../utils/errors.js';
7
7
  import { filterDefined } from '../utils/objects.js';
8
8
  import { shouldExcludeDomain, shouldExcludeUrl, shouldFetchBodyWithReason } from './filters.js';
9
9
  import { ExtraInfoTracker } from './networkExtraInfo.js';
10
+ import { RequestRetention, skippedBodyPlaceholder, } from './networkRetention.js';
10
11
  const log = createLogger('network');
11
12
  /**
12
13
  * Check if a request should be filtered out based on domain and URL patterns.
@@ -21,27 +22,26 @@ function shouldFilterRequest(url, includeAll, networkInclude, networkExclude) {
21
22
  return false;
22
23
  }
23
24
  /**
24
- * Fetch response body for a request with cancellation support.
25
+ * Fetch response body for a request with cancellation support: a body that
26
+ * arrives after its fetch was cancelled (removed from `pendingFetches`: the
27
+ * collector stopped, or the request was dropped) is discarded.
25
28
  *
26
29
  * @param cdp - CDP connection instance
27
30
  * @param requestId - Request ID to fetch body for
28
- * @param request - Network request object to populate with body
31
+ * @param request - Network request the body belongs to
29
32
  * @param pendingFetches - Set to track pending fetch operations for cleanup
33
+ * @param retention - Stores the body within the session's body budget
34
+ * @param sessionId - Session of the iframe or worker that made the request
30
35
  */
31
- function fetchResponseBody(cdp, requestId, request, pendingFetches, sessionId) {
36
+ function fetchResponseBody(cdp, requestId, request, pendingFetches, retention, sessionId) {
32
37
  pendingFetches.add(requestId);
33
38
  void cdp
34
39
  .send('Network.getResponseBody', { requestId }, sessionId)
35
40
  .then((response) => {
36
41
  if (!pendingFetches.has(requestId))
37
42
  return;
38
- const typedResponse = response;
39
- request.responseBody = typedResponse.body;
40
- if (typedResponse.base64Encoded)
41
- request.responseBodyBase64 = true;
42
- if (typedResponse.body) {
43
- request.decodedBodyLength = Buffer.byteLength(typedResponse.body, typedResponse.base64Encoded ? 'base64' : 'utf-8');
44
- }
43
+ const { body, base64Encoded } = response;
44
+ retention.storeBody(request, body, base64Encoded);
45
45
  })
46
46
  .catch((error) => {
47
47
  log.debug(`Failed to fetch response body for request ${requestId}: ${getErrorMessage(error)}`);
@@ -83,25 +83,6 @@ function createNetworkRequest(params, getCurrentNavigationId) {
83
83
  ...(params.type !== undefined && { resourceType: params.type }),
84
84
  };
85
85
  }
86
- const SKIPPED_BODY_PATTERN = /^\[SKIPPED: (.*)\]$/s;
87
- /**
88
- * Placeholder stored instead of a response body that was not fetched.
89
- *
90
- * @param reason - Why the body was skipped
91
- * @returns Placeholder text shown by `bdg details`
92
- */
93
- export function skippedBodyPlaceholder(reason) {
94
- return `[SKIPPED: ${reason}]`;
95
- }
96
- /**
97
- * Extract the reason from a skipped-body placeholder.
98
- *
99
- * @param body - Stored response body
100
- * @returns Reason if `body` is a placeholder, otherwise undefined
101
- */
102
- export function skippedBodyReason(body) {
103
- return body === undefined ? undefined : SKIPPED_BODY_PATTERN.exec(body)?.[1];
104
- }
105
86
  /**
106
87
  * Copy response fields (status, headers, timing, connection) onto a request.
107
88
  *
@@ -272,21 +253,31 @@ async function collectChildTargetNetwork(cdp) {
272
253
  *
273
254
  * @remarks
274
255
  * - Chrome buffer limits: 50MB total, 10MB per resource, 1MB POST data (with fallback)
275
- * - Stale requests (incomplete after 60s) are removed from tracking but NOT added to output
276
- * - Request limit of 10,000 prevents memory issues in long-running sessions
256
+ * - The newest 10,000 finished requests are kept: past that the oldest finished
257
+ * ones are dropped (counted in `evictions.requestsDropped`); requests in flight
258
+ * are tracked separately and never dropped mid-flight
259
+ * - Stored response bodies total at most 100MB: past that the oldest bodies are
260
+ * replaced by a placeholder (counted in `evictions.bodiesEvicted`), their
261
+ * request metadata stays
277
262
  * - Response bodies are automatically skipped for images, fonts, CSS, and source maps (see DEFAULT_SKIP_BODY_PATTERNS)
278
263
  * - Response bodies larger than 5MB are skipped with a placeholder message
279
264
  * - By default, common tracking/analytics domains are filtered out (use includeAll to disable)
280
265
  * - Pattern precedence: include patterns always trump exclude patterns
281
266
  */
282
267
  export async function startNetworkCollection(cdp, requests, options = {}) {
283
- const { includeAll = false, fetchAllBodies = false, fetchBodiesInclude = [], fetchBodiesExclude = [], networkInclude = [], networkExclude = [], maxBodySize = MAX_RESPONSE_SIZE, getCurrentNavigationId, } = options;
268
+ const { includeAll = false, fetchAllBodies = false, fetchBodiesInclude = [], fetchBodiesExclude = [], networkInclude = [], networkExclude = [], maxBodySize = MAX_RESPONSE_SIZE, maxRequests = MAX_NETWORK_REQUESTS, maxTotalBodyBytes = MAX_TOTAL_BODY_BYTES, evictions = { requestsDropped: 0, bodiesEvicted: 0 }, getCurrentNavigationId, } = options;
284
269
  const requestMap = options.pendingRequests ?? new Map();
285
270
  const pendingFetches = new Set();
286
271
  const redirectHops = new Map();
287
272
  const extraInfo = new ExtraInfoTracker((requestId) => requestMap.get(requestId)?.request);
288
273
  const registry = new CDPHandlerRegistry();
289
274
  const typed = new TypedCDPConnection(cdp);
275
+ const retention = new RequestRetention(requests, { maxRequests, maxTotalBodyBytes }, evictions);
276
+ const record = (request) => {
277
+ const dropped = retention.add(request);
278
+ if (dropped)
279
+ pendingFetches.delete(dropped.requestId);
280
+ };
290
281
  let bodiesFetched = 0;
291
282
  let bodiesSkipped = 0;
292
283
  try {
@@ -305,11 +296,9 @@ export async function startNetworkCollection(cdp, requests, options = {}) {
305
296
  const entry = requestMap.get(requestId);
306
297
  if (!entry)
307
298
  return;
308
- if (requests.length < MAX_NETWORK_REQUESTS) {
309
- applyFailure(entry.request, failure);
310
- requests.push(entry.request);
311
- extraInfo.complete(requestId, entry.request);
312
- }
299
+ applyFailure(entry.request, failure);
300
+ record(entry.request);
301
+ extraInfo.complete(requestId, entry.request);
313
302
  requestMap.delete(requestId);
314
303
  redirectHops.delete(requestId);
315
304
  };
@@ -349,16 +338,11 @@ export async function startNetworkCollection(cdp, requests, options = {}) {
349
338
  const hop = completeRedirectHop(previous.request, params, redirectHops);
350
339
  extraInfo.applyResponse(params.requestId, hop);
351
340
  extraInfo.recordRedirectHop(params.requestId, hop);
352
- if (requests.length < MAX_NETWORK_REQUESTS)
353
- requests.push(hop);
341
+ record(hop);
354
342
  }
355
343
  if (shouldFilterRequest(params.request.url, includeAll, networkInclude, networkExclude)) {
356
344
  return;
357
345
  }
358
- if (requestMap.size >= MAX_NETWORK_REQUESTS) {
359
- log.debug(`Warning: Network request limit reached (${MAX_NETWORK_REQUESTS}), dropping new requests`);
360
- return;
361
- }
362
346
  const request = createNetworkRequest(params, getCurrentNavigationId);
363
347
  extraInfo.applyRequest(params.requestId, request);
364
348
  if (params.type === 'Document' && params.loaderId) {
@@ -399,12 +383,6 @@ export async function startNetworkCollection(cdp, requests, options = {}) {
399
383
  const entry = requestMap.get(params.requestId);
400
384
  if (!entry)
401
385
  return;
402
- if (requests.length >= MAX_NETWORK_REQUESTS) {
403
- log.debug(`Warning: Network request limit reached (${MAX_NETWORK_REQUESTS})`);
404
- requestMap.delete(params.requestId);
405
- redirectHops.delete(params.requestId);
406
- return;
407
- }
408
386
  const request = entry.request;
409
387
  if (params.encodedDataLength !== undefined) {
410
388
  request.encodedDataLength = params.encodedDataLength;
@@ -419,13 +397,13 @@ export async function startNetworkCollection(cdp, requests, options = {}) {
419
397
  });
420
398
  if (decision.should) {
421
399
  bodiesFetched++;
422
- fetchResponseBody(cdp, params.requestId, request, pendingFetches, entry.sessionId);
400
+ fetchResponseBody(cdp, params.requestId, request, pendingFetches, retention, entry.sessionId);
423
401
  }
424
402
  else {
425
403
  bodiesSkipped++;
426
404
  request.responseBody = skippedBodyPlaceholder(decision.reason ?? 'not captured');
427
405
  }
428
- requests.push(request);
406
+ record(request);
429
407
  extraInfo.complete(params.requestId, request);
430
408
  requestMap.delete(params.requestId);
431
409
  redirectHops.delete(params.requestId);