@crouton-kit/capture 1.4.2 → 1.4.10
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +178 -0
- package/bin/capture +31278 -7312
- package/package.json +3 -1
- package/src/capture.ts +21 -18
- package/src/cdp/args.ts +269 -49
- package/src/cdp/bridge/server.ts +6 -5
- package/src/cdp/bridge/spawn.ts +41 -97
- package/src/cdp/commands/bridge-serve.ts +11 -21
- package/src/cdp/commands/cdp.ts +44 -22
- package/src/cdp/commands/heap/census.ts +61 -0
- package/src/cdp/commands/heap/common.ts +77 -0
- package/src/cdp/commands/heap/diff.ts +92 -0
- package/src/cdp/commands/heap/index.ts +38 -0
- package/src/cdp/commands/heap/objects.ts +42 -0
- package/src/cdp/commands/heap/retainers.ts +52 -0
- package/src/cdp/commands/heap/snapshot.ts +94 -0
- package/src/cdp/commands/lib.ts +9 -17
- package/src/cdp/commands/lighthouse.ts +414 -0
- package/src/cdp/commands/measure/census.ts +2 -1
- package/src/cdp/commands/measure/check.ts +70 -16
- package/src/cdp/commands/measure/diff.ts +1 -2
- package/src/cdp/commands/measure/explain.ts +44 -7
- package/src/cdp/commands/measure/index.ts +24 -32
- package/src/cdp/commands/measure/map-ax.ts +2 -1
- package/src/cdp/commands/measure/map-focus.ts +2 -1
- package/src/cdp/commands/measure/map-layers.ts +5 -3
- package/src/cdp/commands/measure/map-scroll.ts +2 -1
- package/src/cdp/commands/measure/snap.ts +5 -4
- package/src/cdp/commands/measure/sweep.ts +16 -11
- package/src/cdp/commands/measure/text.ts +147 -0
- package/src/cdp/commands/motion/index.ts +10 -18
- package/src/cdp/commands/motion/jank.ts +13 -22
- package/src/cdp/commands/motion/mask.ts +10 -9
- package/src/cdp/commands/motion/rec.ts +228 -26
- package/src/cdp/commands/motion/timeline.ts +1 -1
- package/src/cdp/commands/page/click.ts +15 -5
- package/src/cdp/commands/page/elements.ts +43 -11
- package/src/cdp/commands/page/exec.ts +53 -18
- package/src/cdp/commands/page/index.ts +20 -20
- package/src/cdp/commands/page/inspect.ts +338 -0
- package/src/cdp/commands/page/navigate.ts +83 -38
- package/src/cdp/commands/page/repeat.ts +176 -0
- package/src/cdp/commands/page/scroll.ts +10 -5
- package/src/cdp/commands/page/shot.ts +66 -31
- package/src/cdp/commands/page/type.ts +6 -2
- package/src/cdp/commands/perf/index.ts +32 -0
- package/src/cdp/commands/perf/insights.ts +214 -0
- package/src/cdp/commands/perf/trace.ts +235 -0
- package/src/cdp/commands/perf/vitals.ts +76 -0
- package/src/cdp/commands/session/collectors.ts +54 -0
- package/src/cdp/commands/stub.ts +10 -0
- package/src/cdp/commands/tab/close.ts +2 -2
- package/src/cdp/commands/tab/index.ts +16 -23
- package/src/cdp/commands/tab/launch.ts +1 -1
- package/src/cdp/commands/tab/mock/index.ts +24 -0
- package/src/cdp/commands/tab/mock/lifecycle.ts +88 -0
- package/src/cdp/commands/tab/mock/start.ts +65 -0
- package/src/cdp/commands/tab/mock/stop.ts +39 -0
- package/src/cdp/commands/tab/network.ts +22 -13
- package/src/cdp/commands/tab/reset.ts +11 -12
- package/src/cdp/connection.ts +19 -39
- package/src/cdp/detect.ts +27 -7
- package/src/cdp/dispatch.ts +25 -4
- package/src/cdp/domains.ts +2 -2
- package/src/cdp/har-recorder.ts +0 -1
- package/src/cdp/heap-snapshot.ts +647 -0
- package/src/cdp/host/client.ts +22 -0
- package/src/cdp/host/collector.ts +74 -0
- package/src/cdp/host/collectors/heap.ts +79 -0
- package/src/cdp/host/collectors/intercept.ts +247 -0
- package/src/cdp/{recorder-bridge.ts → host/collectors/motion.ts} +388 -473
- package/src/cdp/host/collectors/trace.ts +108 -0
- package/src/cdp/host/core.ts +217 -0
- package/src/cdp/host/handle.ts +47 -0
- package/src/cdp/host/held-client.ts +91 -0
- package/src/cdp/host/kinds.ts +97 -0
- package/src/cdp/host/lifecycle.ts +123 -0
- package/src/cdp/host/server.ts +88 -0
- package/src/cdp/measure/check.ts +181 -16
- package/src/cdp/measure/collectors/hittest.ts +16 -10
- package/src/cdp/measure/collectors/scroll.ts +15 -7
- package/src/cdp/measure/collectors/style-provenance.ts +11 -0
- package/src/cdp/measure/collectors/styles.ts +4 -4
- package/src/cdp/measure/explain.ts +14 -2
- package/src/cdp/measure/map-layers.ts +52 -8
- package/src/cdp/measure/snapshot.ts +5 -3
- package/src/cdp/measure/sweep.ts +29 -6
- package/src/cdp/measure/text.ts +227 -0
- package/src/cdp/motion/jank.ts +29 -83
- package/src/cdp/motion/mask.ts +18 -9
- package/src/cdp/motion/recorder.ts +86 -731
- package/src/cdp/scope-lock.ts +9 -9
- package/src/cdp/screenshot.ts +51 -21
- package/src/cdp/timing.ts +5 -5
- package/src/cdp/trace-analysis.ts +320 -0
- package/src/cdp/types.ts +31 -1
- package/src/cdp.ts +4 -6
- package/src/har-manager.ts +2 -4
- package/src/interact.ts +50 -23
- package/src/output/artifact.ts +49 -5
- package/src/output/render.ts +60 -9
- package/src/output/selection.ts +6 -0
- package/src/session/artifacts.ts +122 -12
- package/src/session/commands.ts +348 -113
- package/src/session/coordinator.ts +0 -93
- package/src/session/har-redact.ts +29 -23
- package/src/session-context.ts +13 -9
- package/src/cdp/bridge/protocol.ts +0 -224
- package/src/cdp/recorder-client.ts +0 -326
- package/src/cdp/tab-lock.ts +0 -132
|
@@ -24,7 +24,7 @@ import { getActiveSession } from '../../../session-context.js';
|
|
|
24
24
|
import { createOneshotSession } from '../../../session/commands.js';
|
|
25
25
|
import { writePrivateFile } from '../../../session/artifacts.js';
|
|
26
26
|
import { withScopeSerialization } from '../../scope-lock.js';
|
|
27
|
-
import {
|
|
27
|
+
import { isMotionHeldClient } from '../../host/held-client.js';
|
|
28
28
|
import { hasImports, bundleExec } from '../../../vault/bundle.js';
|
|
29
29
|
import {
|
|
30
30
|
capped,
|
|
@@ -43,6 +43,8 @@ import {
|
|
|
43
43
|
const GENEROUS_RESULT_CAP = 4000;
|
|
44
44
|
|
|
45
45
|
const DEFAULT_SETTLE_MS = 3000;
|
|
46
|
+
const DEFAULT_EVALUATION_TIMEOUT_MS = 10_000;
|
|
47
|
+
const TERMINATE_TIMEOUT_MS = 2_000;
|
|
46
48
|
|
|
47
49
|
const USAGE = `capture page exec <code> | --file <path> — run arbitrary JavaScript in the tab: a bare expression evaluates directly; a statement body may use top-level return/await (wrapped in an async IIFE); leading static imports bundle the forked vault libs (dev checkout only)
|
|
48
50
|
|
|
@@ -50,12 +52,14 @@ input:
|
|
|
50
52
|
<code> JS source as one argument (exactly one of <code> / --file)
|
|
51
53
|
--file <path> read the JS source from a file instead of inline
|
|
52
54
|
--settle <ms> network-settle window after execution so an active session's HAR captures the requests the code triggers (default: ${DEFAULT_SETTLE_MS}; 0 disables); the block reports the measured requested/waited settle
|
|
55
|
+
--timeout <ms> evaluation deadline (default: ${DEFAULT_EVALUATION_TIMEOUT_MS}); on expiry Capture requests Runtime.terminateExecution and reports whether cancellation completed or the renderer remains blocked
|
|
53
56
|
--target <id> target a tab explicitly (default: the active session tab, else exactly one available page tab; multiple page tabs require --target or --url)
|
|
54
57
|
--url <pattern> target the first tab whose URL matches <pattern>
|
|
58
|
+
--artifact-dir <path> root for a sessionless oversize-result bundle
|
|
55
59
|
output:
|
|
56
60
|
<exec-result result-chars=…> — the JSON-serialized return value inline, escaped and capped at ${GENEROUS_RESULT_CAP} chars; a larger result is also written whole to a private artifact file (the active session's page/ dir, else a one-shot artifact dir) whose absolute path appears in the block. --json mirrors the same block with the value at full fidelity.
|
|
57
61
|
effects:
|
|
58
|
-
runs the code in the page with full DOM/JS access — whatever the code mutates, it mutates; emulates focus on the tab for the duration of the call (disabled again before the command returns); writes the oversize-result artifact file when the inline cap is exceeded`;
|
|
62
|
+
runs the code in the page with full DOM/JS access — whatever the code mutates, it mutates; emulates focus on the tab for the duration of the call (disabled again before the command returns); writes the oversize-result artifact file under --artifact-dir when sessionless and the inline cap is exceeded`;
|
|
59
63
|
|
|
60
64
|
// ---------------------------------------------------------------------------
|
|
61
65
|
// Test-injectable dependency seam (repo CDP-stub pattern — see
|
|
@@ -66,10 +70,10 @@ export interface PageExecDeps {
|
|
|
66
70
|
withPageAction: typeof withPageAction;
|
|
67
71
|
getActiveSession: typeof getActiveSession;
|
|
68
72
|
createOneshotSession: typeof createOneshotSession;
|
|
69
|
-
|
|
73
|
+
isMotionHeldClient: typeof isMotionHeldClient;
|
|
70
74
|
}
|
|
71
75
|
|
|
72
|
-
let deps: PageExecDeps = { withPageAction, getActiveSession, createOneshotSession,
|
|
76
|
+
let deps: PageExecDeps = { withPageAction, getActiveSession, createOneshotSession, isMotionHeldClient };
|
|
73
77
|
|
|
74
78
|
/** Swap the connection/session seams for the CDP-stub tests. */
|
|
75
79
|
export function __setPageExecDepsForTest(overrides: Partial<PageExecDeps>): () => void {
|
|
@@ -91,12 +95,12 @@ function emitExecError(parsed: ParsedArgs, code: string, summary: FactLine, foll
|
|
|
91
95
|
* the active session's `page/` dir when a session exists, else into a fresh
|
|
92
96
|
* one-shot artifact dir. Returns the absolute file path.
|
|
93
97
|
*/
|
|
94
|
-
function spillWholeResult(payload: string): string {
|
|
98
|
+
function spillWholeResult(payload: string, artifactDir?: string): string {
|
|
95
99
|
const name = `exec-${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 8)}.json`;
|
|
96
100
|
const session = deps.getActiveSession();
|
|
97
101
|
const filePath = session
|
|
98
102
|
? path.join(session.dir, 'page', name)
|
|
99
|
-
: path.join(deps.createOneshotSession('page').artifactsDir, name);
|
|
103
|
+
: path.join(deps.createOneshotSession('page', artifactDir).artifactsDir, name);
|
|
100
104
|
writePrivateFile(filePath, payload);
|
|
101
105
|
return filePath;
|
|
102
106
|
}
|
|
@@ -110,7 +114,19 @@ interface EvalResult {
|
|
|
110
114
|
* subset of `CDPClient` both a direct connection and the recorder-held
|
|
111
115
|
* adapter satisfy. */
|
|
112
116
|
interface ExecClient {
|
|
113
|
-
send(method: string, params?: Record<string, unknown
|
|
117
|
+
send(method: string, params?: Record<string, unknown>, timeoutMs?: number): Promise<unknown>;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
class ExecDeadlineError extends Error {
|
|
121
|
+
constructor(readonly cancellation: 'completed' | 'blocked', timeoutMs: number, detail: string) {
|
|
122
|
+
super(detail || (cancellation === 'completed'
|
|
123
|
+
? `Evaluation exceeded ${timeoutMs}ms and Runtime.terminateExecution completed.`
|
|
124
|
+
: `Evaluation exceeded ${timeoutMs}ms and Runtime.terminateExecution did not complete.`));
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
function isRequestTimeout(error: unknown): boolean {
|
|
129
|
+
return error instanceof Error && /CDP request timeout \(\d+ms\): Runtime\.evaluate|collector-host CDP call "Runtime\.evaluate" failed: CDP request timeout/.test(error.message);
|
|
114
130
|
}
|
|
115
131
|
|
|
116
132
|
/**
|
|
@@ -121,7 +137,7 @@ interface ExecClient {
|
|
|
121
137
|
* failure prevents success: alone it throws a typed cleanup failure; paired
|
|
122
138
|
* with a primary failure it throws an `AggregateError` preserving both.
|
|
123
139
|
*/
|
|
124
|
-
async function focusScopedEvaluate(client: ExecClient, expression: string): Promise<EvalResult> {
|
|
140
|
+
async function focusScopedEvaluate(client: ExecClient, expression: string, evaluationTimeoutMs: number): Promise<EvalResult> {
|
|
125
141
|
let ownsFocusOverride = false;
|
|
126
142
|
let primaryFailed = false;
|
|
127
143
|
let primaryError: unknown;
|
|
@@ -130,11 +146,22 @@ async function focusScopedEvaluate(client: ExecClient, expression: string): Prom
|
|
|
130
146
|
// by the browser, without foregrounding the tab.
|
|
131
147
|
ownsFocusOverride = true;
|
|
132
148
|
await client.send('Emulation.setFocusEmulationEnabled', { enabled: true });
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
149
|
+
try {
|
|
150
|
+
return (await client.send('Runtime.evaluate', {
|
|
151
|
+
expression,
|
|
152
|
+
awaitPromise: true,
|
|
153
|
+
returnByValue: true,
|
|
154
|
+
}, evaluationTimeoutMs)) as EvalResult;
|
|
155
|
+
} catch (error) {
|
|
156
|
+
if (!isRequestTimeout(error)) throw error;
|
|
157
|
+
try {
|
|
158
|
+
await client.send('Runtime.terminateExecution', {}, TERMINATE_TIMEOUT_MS);
|
|
159
|
+
throw new ExecDeadlineError('completed', evaluationTimeoutMs, `Evaluation exceeded ${evaluationTimeoutMs}ms; Runtime.terminateExecution completed.`);
|
|
160
|
+
} catch (terminationError) {
|
|
161
|
+
if (terminationError instanceof ExecDeadlineError) throw terminationError;
|
|
162
|
+
throw new ExecDeadlineError('blocked', evaluationTimeoutMs, `Evaluation exceeded ${evaluationTimeoutMs}ms; Runtime.terminateExecution did not complete within ${TERMINATE_TIMEOUT_MS}ms, so the renderer remains blocked until this target navigates or closes.`);
|
|
163
|
+
}
|
|
164
|
+
}
|
|
138
165
|
} catch (error) {
|
|
139
166
|
primaryFailed = true;
|
|
140
167
|
primaryError = error;
|
|
@@ -175,13 +202,13 @@ async function focusScopedEvaluate(client: ExecClient, expression: string): Prom
|
|
|
175
202
|
* needs no lock — its emulation state is scoped to its own websocket
|
|
176
203
|
* session, which closes with the command.
|
|
177
204
|
*/
|
|
178
|
-
async function evaluateWithFocusEmulation(client: ExecClient, expression: string): Promise<EvalResult> {
|
|
205
|
+
async function evaluateWithFocusEmulation(client: ExecClient, expression: string, evaluationTimeoutMs: number): Promise<EvalResult> {
|
|
179
206
|
return withScopeSerialization(
|
|
180
207
|
client,
|
|
181
208
|
'focus',
|
|
182
209
|
'page exec',
|
|
183
|
-
() => focusScopedEvaluate(client, expression),
|
|
184
|
-
{
|
|
210
|
+
() => focusScopedEvaluate(client, expression, evaluationTimeoutMs),
|
|
211
|
+
{ isMotionHeldClient: deps.isMotionHeldClient, getActiveSession: deps.getActiveSession },
|
|
185
212
|
);
|
|
186
213
|
}
|
|
187
214
|
|
|
@@ -237,6 +264,7 @@ export async function cmdPageExec(parsed: ParsedArgs, _args: string[]): Promise<
|
|
|
237
264
|
}
|
|
238
265
|
|
|
239
266
|
const settle = parsed.settle ?? DEFAULT_SETTLE_MS;
|
|
267
|
+
const evaluationTimeoutMs = parsed.timeoutMs ?? DEFAULT_EVALUATION_TIMEOUT_MS;
|
|
240
268
|
|
|
241
269
|
// A prebuilt bundle is already a complete IIFE returning the user's
|
|
242
270
|
// promise; otherwise buildExecExpression wraps statement bodies. Pure
|
|
@@ -253,11 +281,18 @@ export async function cmdPageExec(parsed: ParsedArgs, _args: string[]): Promise<
|
|
|
253
281
|
const outcome = await deps.withPageAction(
|
|
254
282
|
{ ...parsed, command: 'exec' },
|
|
255
283
|
{ settleMs: settle },
|
|
256
|
-
(client) => evaluateWithFocusEmulation(client, expression),
|
|
284
|
+
(client) => evaluateWithFocusEmulation(client, expression, evaluationTimeoutMs),
|
|
257
285
|
);
|
|
258
286
|
evalResult = outcome.result;
|
|
259
287
|
settleFacts = outcome.settle;
|
|
260
288
|
} catch (err) {
|
|
289
|
+
if (err instanceof ExecDeadlineError) {
|
|
290
|
+
return emitExecError(
|
|
291
|
+
parsed,
|
|
292
|
+
err.cancellation === 'completed' ? 'exec_timeout_cancelled' : 'target_wedged',
|
|
293
|
+
fact`${err.message}`,
|
|
294
|
+
);
|
|
295
|
+
}
|
|
261
296
|
// A typed failure (e.g. `recorder_unavailable` from strict A2 routing,
|
|
262
297
|
// `har_missing`, or a lone `focus_cleanup_failed`) crosses the boundary
|
|
263
298
|
// unrelabeled — only a genuinely untyped error is the exec-specific
|
|
@@ -293,7 +328,7 @@ export async function cmdPageExec(parsed: ParsedArgs, _args: string[]): Promise<
|
|
|
293
328
|
|
|
294
329
|
let spillPath: string | undefined;
|
|
295
330
|
if (payload.length > GENEROUS_RESULT_CAP) {
|
|
296
|
-
spillPath = spillWholeResult(payload);
|
|
331
|
+
spillPath = spillWholeResult(payload, parsed.artifactDir);
|
|
297
332
|
}
|
|
298
333
|
|
|
299
334
|
const inlineCap = parsed.json ? payload.length : GENEROUS_RESULT_CAP;
|
|
@@ -18,31 +18,27 @@ import { cmdPageNavigate } from './navigate.js';
|
|
|
18
18
|
import { cmdPageExec } from './exec.js';
|
|
19
19
|
import { cmdPageShot } from './shot.js';
|
|
20
20
|
import { cmdPageElements } from './elements.js';
|
|
21
|
+
import { inspectMain } from './inspect.js';
|
|
22
|
+
import { cmdPageRepeat } from './repeat.js';
|
|
21
23
|
|
|
22
24
|
/** Root-help representation of this branch, assembled by `src/capture.ts`. */
|
|
23
25
|
export const COMMAND_BLOCK = `<command name="page">
|
|
24
|
-
verbs against the live session tab — act (click, type, scroll, navigate, exec) and
|
|
25
|
-
use when driving or inspecting the
|
|
26
|
-
click · type · scroll · navigate · exec · shot · elements — \`capture page -h\`
|
|
26
|
+
verbs against the live session tab — act on it (click, type, scroll, navigate, exec, repeat) and inspect its current state (shot, elements, inspect)
|
|
27
|
+
use when driving or inspecting the current live tab
|
|
27
28
|
</command>`;
|
|
28
29
|
|
|
29
|
-
export const PAGE_USAGE =
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
<subcommand name="
|
|
38
|
-
<subcommand name="
|
|
39
|
-
<subcommand name="
|
|
40
|
-
|
|
41
|
-
<subcommand name="exec" args="<code> | --file <path>" whenToUse="run arbitrary JS in the tab (expressions, return, await)"/>
|
|
42
|
-
<subcommand name="shot" args="[--viewport <WxH>] [--full-page] [--color-scheme <dark|light>] [--crop <x,y,w,h> | --crop-selector <sel> [--pad <px>]] [--zoom <factor>] [--out <path>]" whenToUse="look at the page right now without acting"/>
|
|
43
|
-
<subcommand name="elements" args="[--all] [--limit <n>]" whenToUse="list what can be acted on — role, name, backend:<id> per element"/>
|
|
44
|
-
|
|
45
|
-
capture page <leaf> -h Per-leaf usage`;
|
|
30
|
+
export const PAGE_USAGE = `<command name="page" description="operations against the live session tab">
|
|
31
|
+
<model>All verbs auto-target the active session tab; --target/--url override. Driving verbs resolve exactly one element via the unified target grammar — bare CSS selector (which takes precedence) or exact accessible name when CSS finds none, ax:<name>, axid:<id>, backend:<id> — and reject an ambiguous target with the candidate list. During a live composed recording every verb routes through the recorder.</model>
|
|
32
|
+
<subcommand name="click" description="real click on one element" whenToUse="Use to activate a control through the page's normal click handling."/>
|
|
33
|
+
<subcommand name="type" description="text entry into an element" whenToUse="Use to enter text into the focused element or a resolved field."/>
|
|
34
|
+
<subcommand name="scroll" description="scroll one container" whenToUse="Use to move a resolved scrollable container to a position."/>
|
|
35
|
+
<subcommand name="navigate" description="navigate the tab" whenToUse="Use to load a URL in the current tab and wait for it to settle."/>
|
|
36
|
+
<subcommand name="exec" description="arbitrary JavaScript in the tab" whenToUse="Use when no named page command exposes the needed page operation."/>
|
|
37
|
+
<subcommand name="shot" description="current-page screenshot" whenToUse="Use to inspect the rendered page without interacting with it."/>
|
|
38
|
+
<subcommand name="elements" description="actionable page elements" whenToUse="Use to discover targets for page interactions."/>
|
|
39
|
+
<subcommand name="inspect" description="live-tab factual inventories" whenToUse="Use to read listeners, cardinality, frame lifecycle, or Resource Timing facts from the current tab."/>
|
|
40
|
+
<subcommand name="repeat" description="repeat a declarative click/type sequence" whenToUse="Use to drive the same bounded interaction sequence over multiple iterations and retain per-iteration completion facts."/>
|
|
41
|
+
</command>`;
|
|
46
42
|
|
|
47
43
|
export async function pageMain(parsed: ParsedArgs, args: string[]): Promise<void> {
|
|
48
44
|
const leaf = parsed.positional[0];
|
|
@@ -63,6 +59,10 @@ export async function pageMain(parsed: ParsedArgs, args: string[]): Promise<void
|
|
|
63
59
|
return cmdPageShot(rest, args);
|
|
64
60
|
case 'elements':
|
|
65
61
|
return cmdPageElements(rest, args);
|
|
62
|
+
case 'inspect':
|
|
63
|
+
return inspectMain(rest, args);
|
|
64
|
+
case 'repeat':
|
|
65
|
+
return cmdPageRepeat(rest, args);
|
|
66
66
|
case undefined:
|
|
67
67
|
console.log(PAGE_USAGE);
|
|
68
68
|
return;
|
|
@@ -0,0 +1,338 @@
|
|
|
1
|
+
import { type CDPClient } from '../../client.js';
|
|
2
|
+
import { withConnection } from '../../connection.js';
|
|
3
|
+
import { type ParsedArgs } from '../../types.js';
|
|
4
|
+
import { invalidInput } from '../../../errors.js';
|
|
5
|
+
import { resolveLiveTarget, type LiveClient, type ResolutionFailure } from '../../../interact.js';
|
|
6
|
+
import { emitResolutionError } from './click.js';
|
|
7
|
+
import { emitResult, fact, lineList, type FactLine } from '../../../output/render.js';
|
|
8
|
+
|
|
9
|
+
const DEFAULT_LIMIT = 100;
|
|
10
|
+
|
|
11
|
+
const INSPECT_USAGE = `<command name="inspect" description="factual live-tab inventories">
|
|
12
|
+
<model>Each leaf reads the current selected tab without a page-observable write. List leaves filter before --limit and mirror only their bounded final records. This branch does not enumerate scroll containers; \`capture measure map scroll\` owns that measured topology.</model>
|
|
13
|
+
<subcommand name="listeners" description="registered event listeners" whenToUse="Use to read listeners on window, document, or one live-resolved target."/>
|
|
14
|
+
<subcommand name="cardinality" description="DOM and document counts" whenToUse="Use to read Chrome's current DOM/document cardinality counters."/>
|
|
15
|
+
<subcommand name="frames" description="attached and observed-detached frame identities" whenToUse="Use to read the current frame tree and frame lifecycle events observed during a bounded window."/>
|
|
16
|
+
<subcommand name="resources" description="Resource Timing entries" whenToUse="Use to read the current page's Performance Resource Timing view."/>
|
|
17
|
+
</command>`;
|
|
18
|
+
|
|
19
|
+
const LISTENERS_USAGE = `capture page inspect listeners [--scope <window|document|target> --selector <target>] [--event <type>] [--limit <n>] — list registered listeners on one live identity
|
|
20
|
+
|
|
21
|
+
input:
|
|
22
|
+
--scope <kind> listener owner: window (default), document, or target; target requires --selector
|
|
23
|
+
--selector <target> target-scope identity using the live page grammar: bare CSS selector (or exact accessible name when CSS finds none), ax:<name>, axid:<id>, backend:<id>
|
|
24
|
+
--event <type> exact event-type filter; applies before --limit
|
|
25
|
+
--limit <n> maximum listener records rendered and mirrored (positive integer, default ${DEFAULT_LIMIT}); the summary retains the filtered count
|
|
26
|
+
--target <tabId> | --url <pattern> | --port <n> tab targeting; defaults to the active session tab
|
|
27
|
+
output:
|
|
28
|
+
<listeners identity=… count=… displayed=…> — listener event type, capture/passive/once registration facts, handler description, and script URL/line/column provenance when CDP supplies it
|
|
29
|
+
effects:
|
|
30
|
+
read-only CDP collection: enables Debugger to map listener script ids to the script URLs Chrome reports, then reads DOMDebugger listener registrations; no page-observable write`;
|
|
31
|
+
|
|
32
|
+
const CARDINALITY_USAGE = `capture page inspect cardinality — read bounded current DOM/document counts
|
|
33
|
+
|
|
34
|
+
input:
|
|
35
|
+
--target <tabId> | --url <pattern> | --port <n> tab targeting; defaults to the active session tab
|
|
36
|
+
output:
|
|
37
|
+
<cardinality documents=… nodes=… listeners=… elements=… frames=…> — Chrome Memory.getDOMCounters values, the current document element count, and the Performance metric frame count with collection-method provenance
|
|
38
|
+
effects:
|
|
39
|
+
read-only CDP collection through Memory.getDOMCounters, Runtime.evaluate, and Performance.getMetrics; no page-observable write`;
|
|
40
|
+
|
|
41
|
+
const FRAMES_USAGE = `capture page inspect frames [--duration <seconds>] — read attached frame identities and lifecycle events observed in a bounded window
|
|
42
|
+
|
|
43
|
+
input:
|
|
44
|
+
--duration <seconds> observation window after the current frame-tree read (default 0; maximum 60); frame attach, navigate, and detach events received in this window are recorded
|
|
45
|
+
--limit <n> maximum attached/detached frame records rendered and mirrored (positive integer, default ${DEFAULT_LIMIT}); the summary retains attached and detached totals
|
|
46
|
+
--target <tabId> | --url <pattern> | --port <n> tab targeting; defaults to the active session tab
|
|
47
|
+
output:
|
|
48
|
+
<frames attached=… detached=… displayed=… requested-ms=… observed-ms=…> — current attached frame ids, parent ids, and URLs plus identities and URL facts for frames observed detaching during this command's collection window; attached/detached records are bounded by --limit
|
|
49
|
+
effects:
|
|
50
|
+
read-only CDP collection: enables Page, reads Page.getFrameTree, and observes Page frame lifecycle events for the bounded window; no page-observable write`;
|
|
51
|
+
|
|
52
|
+
const RESOURCES_USAGE = `capture page inspect resources [--name <text>] [--limit <n>] — list the current page Resource Timing entries
|
|
53
|
+
|
|
54
|
+
input:
|
|
55
|
+
--name <text> case-insensitive substring filter over entry name URLs; applies before --limit
|
|
56
|
+
--limit <n> maximum entries rendered and mirrored (positive integer, default ${DEFAULT_LIMIT}); the summary retains the filtered count
|
|
57
|
+
--target <tabId> | --url <pattern> | --port <n> tab targeting; defaults to the active session tab
|
|
58
|
+
output:
|
|
59
|
+
<resources count=… displayed=…> — Resource Timing name, initiator type, timing offsets/duration, transfer/body sizes, protocol, and collection provenance
|
|
60
|
+
effects:
|
|
61
|
+
read-only Runtime evaluation of performance.getEntriesByType('resource'); no page-observable write`;
|
|
62
|
+
|
|
63
|
+
interface ScriptParsed { scriptId?: unknown; url?: unknown; }
|
|
64
|
+
interface ListenerRaw {
|
|
65
|
+
type?: unknown;
|
|
66
|
+
useCapture?: unknown;
|
|
67
|
+
passive?: unknown;
|
|
68
|
+
once?: unknown;
|
|
69
|
+
scriptId?: unknown;
|
|
70
|
+
lineNumber?: unknown;
|
|
71
|
+
columnNumber?: unknown;
|
|
72
|
+
handler?: { description?: unknown; className?: unknown };
|
|
73
|
+
}
|
|
74
|
+
interface ListenerRecord {
|
|
75
|
+
readonly event: string;
|
|
76
|
+
readonly capture: boolean;
|
|
77
|
+
readonly passive: boolean;
|
|
78
|
+
readonly once: boolean;
|
|
79
|
+
readonly handler?: string;
|
|
80
|
+
readonly scriptId?: string;
|
|
81
|
+
readonly scriptUrl?: string;
|
|
82
|
+
readonly line?: number;
|
|
83
|
+
readonly column?: number;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
function asRecord(value: unknown): Record<string, unknown> {
|
|
87
|
+
return value !== null && typeof value === 'object' && !Array.isArray(value) ? value as Record<string, unknown> : {};
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
function countLimit(parsed: ParsedArgs): number {
|
|
91
|
+
const limit = parsed.limit ?? DEFAULT_LIMIT;
|
|
92
|
+
if (!Number.isSafeInteger(limit) || limit < 1) throw invalidInput(`received: --limit ${String(parsed.limit)}; expected: a positive safe integer.`, 'invalid_flag');
|
|
93
|
+
return limit;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
async function listenerObjectId(client: LiveClient, parsed: ParsedArgs): Promise<{ identity: string; objectId: string } | { failure: ResolutionFailure }> {
|
|
97
|
+
const scope = parsed.scope ?? 'window';
|
|
98
|
+
if (scope === 'target') {
|
|
99
|
+
const resolved = await resolveLiveTarget(client, parsed.selector!);
|
|
100
|
+
if (!resolved.ok) return { failure: resolved };
|
|
101
|
+
const response = asRecord(await client.send('DOM.resolveNode', { backendNodeId: resolved.backendNodeId }));
|
|
102
|
+
const object = asRecord(response.object);
|
|
103
|
+
const objectId = typeof object.objectId === 'string' ? object.objectId : undefined;
|
|
104
|
+
if (!objectId) throw new Error('DOM.resolveNode returned no object id for the live listener target.');
|
|
105
|
+
return { identity: `backend:${resolved.backendNodeId}`, objectId };
|
|
106
|
+
}
|
|
107
|
+
const expression = scope === 'window' ? 'window' : 'document';
|
|
108
|
+
const response = asRecord(await client.send('Runtime.evaluate', { expression, returnByValue: false }));
|
|
109
|
+
const result = asRecord(response.result);
|
|
110
|
+
const objectId = typeof result.objectId === 'string' ? result.objectId : undefined;
|
|
111
|
+
if (!objectId) throw new Error(`Runtime.evaluate returned no object id for ${scope}.`);
|
|
112
|
+
return { identity: scope, objectId };
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
async function cmdListeners(parsed: ParsedArgs): Promise<void> {
|
|
116
|
+
if (parsed.help) return void console.log(LISTENERS_USAGE);
|
|
117
|
+
const limit = countLimit(parsed);
|
|
118
|
+
const outcome = await withConnection(parsed, async (client) => {
|
|
119
|
+
const scriptUrls = new Map<string, string>();
|
|
120
|
+
const parsedHandler = (params: unknown) => {
|
|
121
|
+
const event = params as ScriptParsed;
|
|
122
|
+
if (typeof event.scriptId === 'string' && typeof event.url === 'string') scriptUrls.set(event.scriptId, event.url);
|
|
123
|
+
};
|
|
124
|
+
client.on('Debugger.scriptParsed', parsedHandler);
|
|
125
|
+
try {
|
|
126
|
+
await client.send('Debugger.enable');
|
|
127
|
+
const target = await listenerObjectId(client as unknown as LiveClient, parsed);
|
|
128
|
+
if ('failure' in target) return target;
|
|
129
|
+
try {
|
|
130
|
+
const response = asRecord(await client.send('DOMDebugger.getEventListeners', { objectId: target.objectId, depth: 1, pierce: true }));
|
|
131
|
+
const listeners = Array.isArray(response.listeners) ? response.listeners as ListenerRaw[] : [];
|
|
132
|
+
const records = listeners.flatMap((listener): ListenerRecord[] => {
|
|
133
|
+
if (typeof listener.type !== 'string') return [];
|
|
134
|
+
if (parsed.event !== undefined && listener.type !== parsed.event) return [];
|
|
135
|
+
const scriptId = typeof listener.scriptId === 'string' ? listener.scriptId : undefined;
|
|
136
|
+
return [{
|
|
137
|
+
event: listener.type,
|
|
138
|
+
capture: listener.useCapture === true,
|
|
139
|
+
passive: listener.passive === true,
|
|
140
|
+
once: listener.once === true,
|
|
141
|
+
handler: typeof listener.handler?.description === 'string' ? listener.handler.description : typeof listener.handler?.className === 'string' ? listener.handler.className : undefined,
|
|
142
|
+
scriptId,
|
|
143
|
+
scriptUrl: scriptId === undefined ? undefined : scriptUrls.get(scriptId),
|
|
144
|
+
line: typeof listener.lineNumber === 'number' ? listener.lineNumber : undefined,
|
|
145
|
+
column: typeof listener.columnNumber === 'number' ? listener.columnNumber : undefined,
|
|
146
|
+
}];
|
|
147
|
+
});
|
|
148
|
+
return { identity: target.identity, records };
|
|
149
|
+
} finally {
|
|
150
|
+
await client.send('Runtime.releaseObject', { objectId: target.objectId });
|
|
151
|
+
}
|
|
152
|
+
} finally {
|
|
153
|
+
client.off('Debugger.scriptParsed', parsedHandler);
|
|
154
|
+
}
|
|
155
|
+
}, { settle: 0 });
|
|
156
|
+
|
|
157
|
+
if ('failure' in outcome) return emitResolutionError(parsed, 'page inspect listeners', outcome.failure);
|
|
158
|
+
const shown = outcome.records.slice(0, limit);
|
|
159
|
+
const rows: FactLine[] = shown.map(record => fact`${record.event} capture=${String(record.capture)} passive=${String(record.passive)} once=${String(record.once)}${record.handler === undefined ? '' : ` handler=${record.handler}`}${record.scriptUrl === undefined ? record.scriptId === undefined ? '' : ` script-id=${record.scriptId}` : ` script=${record.scriptUrl}:${record.line ?? 0}:${record.column ?? 0}`}`);
|
|
160
|
+
const sections: FactLine[] = rows.length === 0 ? [] : [lineList(rows)];
|
|
161
|
+
if (shown.length < outcome.records.length) sections.push(fact`listeners-truncated: listing capped at ${shown.length} of ${outcome.records.length} filtered listeners (--limit).`);
|
|
162
|
+
emitResult({
|
|
163
|
+
tag: 'listeners',
|
|
164
|
+
attrs: { identity: outcome.identity, count: outcome.records.length, displayed: shown.length, event: parsed.event },
|
|
165
|
+
summary: fact`${outcome.records.length} registered listener(s) collected from ${outcome.identity}; Debugger.scriptParsed maps script ids to URLs when Chrome supplied those events.`,
|
|
166
|
+
sections,
|
|
167
|
+
jsonSections: { listeners: shown },
|
|
168
|
+
}, { json: parsed.json });
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
async function cmdCardinality(parsed: ParsedArgs): Promise<void> {
|
|
172
|
+
if (parsed.help) return void console.log(CARDINALITY_USAGE);
|
|
173
|
+
const result = await withConnection(parsed, async (client) => {
|
|
174
|
+
const counters = asRecord(await client.send('Memory.getDOMCounters'));
|
|
175
|
+
const document = asRecord(await client.send('Runtime.evaluate', { expression: 'document.querySelectorAll("*").length', returnByValue: true }));
|
|
176
|
+
await client.send('Performance.enable');
|
|
177
|
+
const metricResult = asRecord(await client.send('Performance.getMetrics'));
|
|
178
|
+
const metricItems = Array.isArray(metricResult.metrics) ? metricResult.metrics.map(asRecord) : [];
|
|
179
|
+
const metric = (name: string): number | undefined => {
|
|
180
|
+
const found = metricItems.find(item => item.name === name);
|
|
181
|
+
return typeof found?.value === 'number' ? found.value : undefined;
|
|
182
|
+
};
|
|
183
|
+
return {
|
|
184
|
+
documents: typeof counters.documents === 'number' ? counters.documents : undefined,
|
|
185
|
+
nodes: typeof counters.nodes === 'number' ? counters.nodes : undefined,
|
|
186
|
+
listeners: typeof counters.jsEventListeners === 'number' ? counters.jsEventListeners : undefined,
|
|
187
|
+
elements: typeof asRecord(document.result).value === 'number' ? asRecord(document.result).value as number : undefined,
|
|
188
|
+
frames: metric('Frames'),
|
|
189
|
+
};
|
|
190
|
+
}, { settle: 0 });
|
|
191
|
+
emitResult({
|
|
192
|
+
tag: 'cardinality',
|
|
193
|
+
attrs: result,
|
|
194
|
+
summary: fact`Counts collected now: documents/nodes/listeners from Memory.getDOMCounters; elements from document.querySelectorAll("*").length; frames from Performance.getMetrics.`,
|
|
195
|
+
jsonSections: { collection: { documents: 'Memory.getDOMCounters', nodes: 'Memory.getDOMCounters', listeners: 'Memory.getDOMCounters', elements: 'Runtime.evaluate document.querySelectorAll("*").length', frames: 'Performance.getMetrics Frames' } },
|
|
196
|
+
}, { json: parsed.json });
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
interface FrameRecord { id: string; parentId?: string; url?: string; }
|
|
200
|
+
interface DetachedFrameRecord extends FrameRecord { reason?: string; }
|
|
201
|
+
function flattenFrameTree(frameTree: unknown, out: FrameRecord[]): void {
|
|
202
|
+
const tree = asRecord(frameTree);
|
|
203
|
+
const frame = asRecord(tree.frame);
|
|
204
|
+
if (typeof frame.id === 'string') out.push({ id: frame.id, parentId: typeof frame.parentId === 'string' ? frame.parentId : undefined, url: typeof frame.url === 'string' ? frame.url : undefined });
|
|
205
|
+
if (Array.isArray(tree.childFrames)) for (const child of tree.childFrames) flattenFrameTree(child, out);
|
|
206
|
+
}
|
|
207
|
+
function sleep(ms: number): Promise<void> { return new Promise(resolve => setTimeout(resolve, ms)); }
|
|
208
|
+
|
|
209
|
+
async function cmdFrames(parsed: ParsedArgs): Promise<void> {
|
|
210
|
+
if (parsed.help) return void console.log(FRAMES_USAGE);
|
|
211
|
+
const requestedMs = parsed.duration ?? 0;
|
|
212
|
+
const limit = countLimit(parsed);
|
|
213
|
+
const result = await withConnection(parsed, async (client) => {
|
|
214
|
+
const known = new Map<string, FrameRecord>();
|
|
215
|
+
const detached: DetachedFrameRecord[] = [];
|
|
216
|
+
const attachedHandler = (params: unknown) => {
|
|
217
|
+
const event = asRecord(params);
|
|
218
|
+
if (typeof event.frameId === 'string') known.set(event.frameId, { id: event.frameId, parentId: typeof event.parentFrameId === 'string' ? event.parentFrameId : undefined });
|
|
219
|
+
};
|
|
220
|
+
const navigatedHandler = (params: unknown) => {
|
|
221
|
+
const frame = asRecord(asRecord(params).frame);
|
|
222
|
+
if (typeof frame.id === 'string') known.set(frame.id, { id: frame.id, parentId: typeof frame.parentId === 'string' ? frame.parentId : undefined, url: typeof frame.url === 'string' ? frame.url : undefined });
|
|
223
|
+
};
|
|
224
|
+
const detachedHandler = (params: unknown) => {
|
|
225
|
+
const event = asRecord(params);
|
|
226
|
+
if (typeof event.frameId !== 'string') return;
|
|
227
|
+
const prior = known.get(event.frameId) ?? { id: event.frameId };
|
|
228
|
+
detached.push({ ...prior, reason: typeof event.reason === 'string' ? event.reason : undefined });
|
|
229
|
+
known.delete(event.frameId);
|
|
230
|
+
};
|
|
231
|
+
client.on('Page.frameAttached', attachedHandler);
|
|
232
|
+
client.on('Page.frameNavigated', navigatedHandler);
|
|
233
|
+
client.on('Page.frameDetached', detachedHandler);
|
|
234
|
+
try {
|
|
235
|
+
await client.send('Page.enable');
|
|
236
|
+
const tree = asRecord(await client.send('Page.getFrameTree'));
|
|
237
|
+
const attached: FrameRecord[] = [];
|
|
238
|
+
flattenFrameTree(tree.frameTree, attached);
|
|
239
|
+
for (const frame of attached) known.set(frame.id, frame);
|
|
240
|
+
const observationStarted = performance.now();
|
|
241
|
+
if (requestedMs > 0) await sleep(requestedMs);
|
|
242
|
+
const current: FrameRecord[] = [];
|
|
243
|
+
flattenFrameTree(asRecord(await client.send('Page.getFrameTree')).frameTree, current);
|
|
244
|
+
const observedMs = performance.now() - observationStarted;
|
|
245
|
+
return { attached: current, detached, observedMs };
|
|
246
|
+
} finally {
|
|
247
|
+
client.off('Page.frameAttached', attachedHandler);
|
|
248
|
+
client.off('Page.frameNavigated', navigatedHandler);
|
|
249
|
+
client.off('Page.frameDetached', detachedHandler);
|
|
250
|
+
}
|
|
251
|
+
}, { settle: 0 });
|
|
252
|
+
const records = [
|
|
253
|
+
...result.attached.map(frame => ({ state: 'attached' as const, ...frame })),
|
|
254
|
+
...result.detached.map(frame => ({ state: 'detached' as const, ...frame })),
|
|
255
|
+
];
|
|
256
|
+
const shown = records.slice(0, limit);
|
|
257
|
+
const rows = shown.map(frame => frame.state === 'attached'
|
|
258
|
+
? fact`attached frame:${frame.id}${frame.parentId === undefined ? '' : ` parent:${frame.parentId}`}${frame.url === undefined ? '' : ` url=${frame.url}`}`
|
|
259
|
+
: fact`detached frame:${frame.id}${frame.parentId === undefined ? '' : ` parent:${frame.parentId}`}${frame.url === undefined ? '' : ` url=${frame.url}`}${frame.reason === undefined ? '' : ` reason=${frame.reason}`}`);
|
|
260
|
+
const sections: FactLine[] = rows.length ? [lineList(rows)] : [];
|
|
261
|
+
if (shown.length < records.length) sections.push(fact`frames-truncated: listing capped at ${shown.length} of ${records.length} attached/detached frame records (--limit).`);
|
|
262
|
+
emitResult({
|
|
263
|
+
tag: 'frames',
|
|
264
|
+
attrs: { attached: result.attached.length, detached: result.detached.length, displayed: shown.length, 'requested-ms': requestedMs, 'observed-ms': result.observedMs },
|
|
265
|
+
summary: fact`${result.attached.length} frame(s) are attached in the final Page.getFrameTree; ${result.detached.length} detach event(s) were recorded after Page.enable during a requested ${requestedMs}ms window measured as ${result.observedMs.toFixed(1)}ms.`,
|
|
266
|
+
sections,
|
|
267
|
+
jsonSections: { frames: shown, collection: { requestedMs, observedMs: result.observedMs } },
|
|
268
|
+
}, { json: parsed.json });
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
interface ResourceRecord {
|
|
272
|
+
readonly name: string;
|
|
273
|
+
readonly initiatorType?: string;
|
|
274
|
+
readonly startTime?: number;
|
|
275
|
+
readonly duration?: number;
|
|
276
|
+
readonly responseEnd?: number;
|
|
277
|
+
readonly transferSize?: number;
|
|
278
|
+
readonly encodedBodySize?: number;
|
|
279
|
+
readonly decodedBodySize?: number;
|
|
280
|
+
readonly nextHopProtocol?: string;
|
|
281
|
+
readonly renderBlockingStatus?: string;
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
async function cmdResources(parsed: ParsedArgs): Promise<void> {
|
|
285
|
+
if (parsed.help) return void console.log(RESOURCES_USAGE);
|
|
286
|
+
const limit = countLimit(parsed);
|
|
287
|
+
const records = await withConnection(parsed, async (client) => {
|
|
288
|
+
const response = asRecord(await client.send('Runtime.evaluate', {
|
|
289
|
+
expression: 'performance.getEntriesByType("resource").map(({name,initiatorType,startTime,duration,responseEnd,transferSize,encodedBodySize,decodedBodySize,nextHopProtocol,renderBlockingStatus})=>({name,initiatorType,startTime,duration,responseEnd,transferSize,encodedBodySize,decodedBodySize,nextHopProtocol,renderBlockingStatus}))',
|
|
290
|
+
returnByValue: true,
|
|
291
|
+
}));
|
|
292
|
+
const value = asRecord(response.result).value;
|
|
293
|
+
if (!Array.isArray(value)) throw new Error('performance.getEntriesByType("resource") did not return an array.');
|
|
294
|
+
return value.flatMap((entry): ResourceRecord[] => {
|
|
295
|
+
const record = asRecord(entry);
|
|
296
|
+
if (typeof record.name !== 'string') return [];
|
|
297
|
+
return [{
|
|
298
|
+
name: record.name,
|
|
299
|
+
initiatorType: typeof record.initiatorType === 'string' ? record.initiatorType : undefined,
|
|
300
|
+
startTime: typeof record.startTime === 'number' ? record.startTime : undefined,
|
|
301
|
+
duration: typeof record.duration === 'number' ? record.duration : undefined,
|
|
302
|
+
responseEnd: typeof record.responseEnd === 'number' ? record.responseEnd : undefined,
|
|
303
|
+
transferSize: typeof record.transferSize === 'number' ? record.transferSize : undefined,
|
|
304
|
+
encodedBodySize: typeof record.encodedBodySize === 'number' ? record.encodedBodySize : undefined,
|
|
305
|
+
decodedBodySize: typeof record.decodedBodySize === 'number' ? record.decodedBodySize : undefined,
|
|
306
|
+
nextHopProtocol: typeof record.nextHopProtocol === 'string' ? record.nextHopProtocol : undefined,
|
|
307
|
+
renderBlockingStatus: typeof record.renderBlockingStatus === 'string' ? record.renderBlockingStatus : undefined,
|
|
308
|
+
}];
|
|
309
|
+
});
|
|
310
|
+
}, { settle: 0 });
|
|
311
|
+
const filtered = parsed.name === undefined ? records : records.filter(record => record.name.toLowerCase().includes(parsed.name!.toLowerCase()));
|
|
312
|
+
const shown = filtered.slice(0, limit);
|
|
313
|
+
const sections: FactLine[] = shown.length === 0 ? [] : [lineList(shown.map(record => fact`${record.name} initiator=${record.initiatorType ?? '(unknown)'} start=${record.startTime ?? '(unknown)'}ms duration=${record.duration ?? '(unknown)'}ms response-end=${record.responseEnd ?? '(unknown)'}ms transfer=${record.transferSize ?? '(unknown)'}B encoded=${record.encodedBodySize ?? '(unknown)'}B decoded=${record.decodedBodySize ?? '(unknown)'}B protocol=${record.nextHopProtocol ?? '(unknown)'} render-blocking=${record.renderBlockingStatus ?? '(unknown)'}`))];
|
|
314
|
+
if (shown.length < filtered.length) sections.push(fact`resources-truncated: listing capped at ${shown.length} of ${filtered.length} filtered entries (--limit).`);
|
|
315
|
+
emitResult({
|
|
316
|
+
tag: 'resources',
|
|
317
|
+
attrs: { count: filtered.length, displayed: shown.length, 'name-filter': parsed.name },
|
|
318
|
+
summary: fact`${filtered.length} current Performance Resource Timing entr${filtered.length === 1 ? 'y' : 'ies'} collected at command time.`,
|
|
319
|
+
sections,
|
|
320
|
+
jsonSections: { entries: shown, collection: 'performance.getEntriesByType("resource")' },
|
|
321
|
+
}, { json: parsed.json });
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
export async function inspectMain(parsed: ParsedArgs, _args: string[]): Promise<void> {
|
|
325
|
+
const leaf = parsed.positional[0];
|
|
326
|
+
const rest = { ...parsed, positional: parsed.positional.slice(1) };
|
|
327
|
+
switch (leaf) {
|
|
328
|
+
case 'listeners': return cmdListeners(rest);
|
|
329
|
+
case 'cardinality': return cmdCardinality(rest);
|
|
330
|
+
case 'frames': return cmdFrames(rest);
|
|
331
|
+
case 'resources': return cmdResources(rest);
|
|
332
|
+
case undefined:
|
|
333
|
+
console.log(INSPECT_USAGE);
|
|
334
|
+
return;
|
|
335
|
+
default:
|
|
336
|
+
throw invalidInput(`Unknown page inspect leaf: ${leaf}.`, 'unknown_command');
|
|
337
|
+
}
|
|
338
|
+
}
|