@saptools/cf-inspector 0.3.8 → 0.3.11
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 +13 -29
- package/dist/cli.js +58 -23
- package/dist/cli.js.map +1 -1
- package/dist/index.d.ts +1 -0
- package/dist/index.js +54 -21
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -46,20 +46,6 @@ pnpm add @saptools/cf-inspector
|
|
|
46
46
|
|
|
47
47
|
## 🚀 Quick Start
|
|
48
48
|
|
|
49
|
-
### Local Node process
|
|
50
|
-
|
|
51
|
-
```bash
|
|
52
|
-
# Terminal 1 — run any Node app with the inspector enabled
|
|
53
|
-
node --inspect=9229 my-app.js
|
|
54
|
-
|
|
55
|
-
# Terminal 2 — capture a snapshot when handler.ts:42 hits
|
|
56
|
-
cf-inspector snapshot \
|
|
57
|
-
--port 9229 \
|
|
58
|
-
--bp src/handler.ts:42 \
|
|
59
|
-
--capture 'this.user, req.body' \
|
|
60
|
-
--timeout 30
|
|
61
|
-
```
|
|
62
|
-
|
|
63
49
|
### Cloud Foundry app (auto-tunnel)
|
|
64
50
|
|
|
65
51
|
```bash
|
|
@@ -72,7 +58,7 @@ cf-inspector snapshot \
|
|
|
72
58
|
--remote-root 'regex:^/(home/vcap/app|example-root-.*)$'
|
|
73
59
|
```
|
|
74
60
|
|
|
75
|
-
|
|
61
|
+
This command internally calls `@saptools/cf-debugger` to open the SSH tunnel, runs the snapshot through it, and tears the tunnel down on exit.
|
|
76
62
|
|
|
77
63
|
---
|
|
78
64
|
|
|
@@ -111,24 +97,21 @@ cf-inspector snapshot --port 9229 \
|
|
|
111
97
|
| `--condition <expr>` | Only pause when this JS expression evaluates truthy in the paused frame. Errors in the condition are silently treated as `false` by V8 |
|
|
112
98
|
| `--capture <expr,…>` | Top-level comma-separated expressions to evaluate in the paused frame; nested commas inside objects, arrays, calls, or strings are preserved. Object results are materialized to JSON strings when serializable, with fallback to CDP descriptions for non-serializable values |
|
|
113
99
|
| `--timeout <seconds>` | How long to wait for the breakpoint to hit (default: `30`) |
|
|
100
|
+
| `--max-value-length <chars>` | Maximum characters per captured value before truncation (default: `4096`) |
|
|
114
101
|
| `--remote-root <value>` | Optional path-mapping anchor: literal path or `regex:<pattern>` / `/pattern/flags` |
|
|
115
102
|
| `--include-scopes` | Include expanded paused-frame scopes under `topFrame.scopes`. Omitted by default to keep targeted captures concise |
|
|
116
103
|
| `--no-json` | Print a human-readable summary instead of JSON |
|
|
117
|
-
| `--keep-paused` | Skip `Debugger.resume` after capture
|
|
104
|
+
| `--keep-paused` | Skip `Debugger.resume` after capture |
|
|
118
105
|
| `--fail-on-unmatched-pause` | Fail immediately if the target pauses somewhere else instead of waiting cooperatively |
|
|
119
106
|
|
|
120
|
-
JSON
|
|
121
|
-
is only present with `--include-scopes
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
client-observed time from receiving
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
`pausedDurationMs` is `null` because `cf-inspector` intentionally skips
|
|
129
|
-
`Debugger.resume`. Node may resume execution when this one-shot CLI disconnects,
|
|
130
|
-
so treat `--keep-paused` as a low-level diagnostic escape hatch, not a durable
|
|
131
|
-
paused-session mode.
|
|
107
|
+
Snapshot JSON includes frame metadata and `captures` by default. `topFrame.scopes`
|
|
108
|
+
is only present with `--include-scopes` because scope objects can be large and
|
|
109
|
+
drown out targeted captures. Values are raw debugger values, so be careful when
|
|
110
|
+
sharing logs.
|
|
111
|
+
|
|
112
|
+
`pausedDurationMs` measures the client-observed time from receiving the matching
|
|
113
|
+
pause event until `Debugger.resume` completes. With `--keep-paused`, it is `null`
|
|
114
|
+
because resume is intentionally skipped.
|
|
132
115
|
|
|
133
116
|
If the target pauses somewhere else first, for example another debugger's
|
|
134
117
|
breakpoint or a `debugger;` statement, `snapshot` does not resume it by default.
|
|
@@ -221,6 +204,7 @@ const bp = await setBreakpoint(session, {
|
|
|
221
204
|
const pause = await waitForPause(session, { timeoutMs: 30_000 });
|
|
222
205
|
const snapshot = await captureSnapshot(session, pause, {
|
|
223
206
|
captures: ["this.user"],
|
|
207
|
+
maxValueLength: 4096,
|
|
224
208
|
});
|
|
225
209
|
const topFrame = pause.callFrames[0];
|
|
226
210
|
if (topFrame === undefined) {
|
|
@@ -242,7 +226,7 @@ console.log({ bp, snapshot, customValue });
|
|
|
242
226
|
| `setBreakpoint(session, location)` | Set a breakpoint by file/line + optional remote root |
|
|
243
227
|
| `removeBreakpoint(session, id)` | Remove a breakpoint by id |
|
|
244
228
|
| `waitForPause(session, options)` | Resolve when the next `Debugger.paused` event fires |
|
|
245
|
-
| `captureSnapshot(session, pause, options)` | Build a structured snapshot of the paused frame. Pass `includeScopes: true` to expand scopes |
|
|
229
|
+
| `captureSnapshot(session, pause, options)` | Build a structured snapshot of the paused frame. Pass `includeScopes: true` to expand scopes or `maxValueLength` to override the default captured value limit |
|
|
246
230
|
| `evaluateOnFrame(session, frameId, expression)` | Evaluate in a paused frame |
|
|
247
231
|
| `evaluateGlobal(session, expression)` | Evaluate against the global Runtime |
|
|
248
232
|
| `listScripts(session)` | Return the scripts the V8 instance knows about |
|
package/dist/cli.js
CHANGED
|
@@ -1067,11 +1067,12 @@ async function waitForStop(session, options) {
|
|
|
1067
1067
|
}
|
|
1068
1068
|
|
|
1069
1069
|
// src/snapshot.ts
|
|
1070
|
+
init_types();
|
|
1070
1071
|
var MAX_SCOPES = 3;
|
|
1071
1072
|
var MAX_SCOPE_VARIABLES = 20;
|
|
1072
1073
|
var MAX_CHILD_VARIABLES = 8;
|
|
1073
1074
|
var MAX_VARIABLE_DEPTH = 2;
|
|
1074
|
-
var
|
|
1075
|
+
var DEFAULT_MAX_VALUE_LENGTH = 4096;
|
|
1075
1076
|
var PRIORITY_BY_TYPE = {
|
|
1076
1077
|
local: 0,
|
|
1077
1078
|
arguments: 1,
|
|
@@ -1132,16 +1133,28 @@ function formatPrimitive(value) {
|
|
|
1132
1133
|
}
|
|
1133
1134
|
return String(value);
|
|
1134
1135
|
}
|
|
1135
|
-
function
|
|
1136
|
-
if (
|
|
1136
|
+
function resolveMaxValueLength(value) {
|
|
1137
|
+
if (value === void 0) {
|
|
1138
|
+
return DEFAULT_MAX_VALUE_LENGTH;
|
|
1139
|
+
}
|
|
1140
|
+
if (!Number.isInteger(value) || value <= 0) {
|
|
1141
|
+
throw new CfInspectorError(
|
|
1142
|
+
"INVALID_ARGUMENT",
|
|
1143
|
+
`Invalid maxValueLength: ${value.toString()} \u2014 expected a positive integer`
|
|
1144
|
+
);
|
|
1145
|
+
}
|
|
1146
|
+
return value;
|
|
1147
|
+
}
|
|
1148
|
+
function limitValueLength(raw, maxValueLength = DEFAULT_MAX_VALUE_LENGTH) {
|
|
1149
|
+
if (raw.length <= maxValueLength) {
|
|
1137
1150
|
return raw;
|
|
1138
1151
|
}
|
|
1139
|
-
return `${raw.slice(0,
|
|
1152
|
+
return `${raw.slice(0, maxValueLength)}...`;
|
|
1140
1153
|
}
|
|
1141
1154
|
function isExpandable(type) {
|
|
1142
1155
|
return type === "object" || type === "function";
|
|
1143
1156
|
}
|
|
1144
|
-
async function captureProperties(session, objectId, limit, depth) {
|
|
1157
|
+
async function captureProperties(session, objectId, limit, depth, maxValueLength) {
|
|
1145
1158
|
const properties = await getProperties(session, objectId);
|
|
1146
1159
|
const limited = properties.slice(0, limit);
|
|
1147
1160
|
const variables = await Promise.all(
|
|
@@ -1155,7 +1168,8 @@ async function captureProperties(session, objectId, limit, depth) {
|
|
|
1155
1168
|
session,
|
|
1156
1169
|
described.objectId,
|
|
1157
1170
|
MAX_CHILD_VARIABLES,
|
|
1158
|
-
depth - 1
|
|
1171
|
+
depth - 1,
|
|
1172
|
+
maxValueLength
|
|
1159
1173
|
);
|
|
1160
1174
|
if (nested.length > 0) {
|
|
1161
1175
|
children = nested;
|
|
@@ -1163,7 +1177,7 @@ async function captureProperties(session, objectId, limit, depth) {
|
|
|
1163
1177
|
} catch {
|
|
1164
1178
|
}
|
|
1165
1179
|
}
|
|
1166
|
-
const sanitizedValue = limitValueLength(described.value);
|
|
1180
|
+
const sanitizedValue = limitValueLength(described.value, maxValueLength);
|
|
1167
1181
|
const base = { name, value: sanitizedValue };
|
|
1168
1182
|
const withType = described.type === void 0 ? base : { ...base, type: described.type };
|
|
1169
1183
|
return children === void 0 ? withType : { ...withType, children };
|
|
@@ -1178,7 +1192,7 @@ function selectScopes(scopeChain) {
|
|
|
1178
1192
|
function priorityOf(type) {
|
|
1179
1193
|
return PRIORITY_BY_TYPE[type] ?? Number.MAX_SAFE_INTEGER;
|
|
1180
1194
|
}
|
|
1181
|
-
async function captureScopes(session, frame) {
|
|
1195
|
+
async function captureScopes(session, frame, maxValueLength) {
|
|
1182
1196
|
const scopes = selectScopes(frame.scopeChain);
|
|
1183
1197
|
return await Promise.all(
|
|
1184
1198
|
scopes.map(async (scope) => {
|
|
@@ -1187,7 +1201,13 @@ async function captureScopes(session, frame) {
|
|
|
1187
1201
|
return { type: scope.type, variables: [] };
|
|
1188
1202
|
}
|
|
1189
1203
|
try {
|
|
1190
|
-
const variables = await captureProperties(
|
|
1204
|
+
const variables = await captureProperties(
|
|
1205
|
+
session,
|
|
1206
|
+
objectId,
|
|
1207
|
+
MAX_SCOPE_VARIABLES,
|
|
1208
|
+
MAX_VARIABLE_DEPTH,
|
|
1209
|
+
maxValueLength
|
|
1210
|
+
);
|
|
1191
1211
|
return { type: scope.type, variables };
|
|
1192
1212
|
} catch {
|
|
1193
1213
|
return { type: scope.type, variables: [] };
|
|
@@ -1195,10 +1215,10 @@ async function captureScopes(session, frame) {
|
|
|
1195
1215
|
})
|
|
1196
1216
|
);
|
|
1197
1217
|
}
|
|
1198
|
-
function evalResultToCaptured(expression, result) {
|
|
1218
|
+
function evalResultToCaptured(expression, result, maxValueLength = DEFAULT_MAX_VALUE_LENGTH) {
|
|
1199
1219
|
if (result.exceptionDetails !== void 0) {
|
|
1200
1220
|
const text = typeof result.exceptionDetails.exception?.description === "string" ? result.exceptionDetails.exception.description : typeof result.exceptionDetails.text === "string" ? result.exceptionDetails.text : "evaluation failed";
|
|
1201
|
-
return { expression, error: limitValueLength(text) };
|
|
1221
|
+
return { expression, error: limitValueLength(text, maxValueLength) };
|
|
1202
1222
|
}
|
|
1203
1223
|
const inner = result.result;
|
|
1204
1224
|
if (!inner) {
|
|
@@ -1206,7 +1226,7 @@ function evalResultToCaptured(expression, result) {
|
|
|
1206
1226
|
}
|
|
1207
1227
|
const type = typeof inner.type === "string" ? inner.type : void 0;
|
|
1208
1228
|
const buildCaptured = (rendered) => {
|
|
1209
|
-
const sanitized = limitValueLength(rendered);
|
|
1229
|
+
const sanitized = limitValueLength(rendered, maxValueLength);
|
|
1210
1230
|
const base = { expression, value: sanitized };
|
|
1211
1231
|
return type === void 0 ? base : { ...base, type };
|
|
1212
1232
|
};
|
|
@@ -1301,9 +1321,15 @@ function toStructuredValue(variable) {
|
|
|
1301
1321
|
}
|
|
1302
1322
|
return out;
|
|
1303
1323
|
}
|
|
1304
|
-
async function renderObjectCapture(session, objectId) {
|
|
1324
|
+
async function renderObjectCapture(session, objectId, maxValueLength) {
|
|
1305
1325
|
try {
|
|
1306
|
-
const properties = await captureProperties(
|
|
1326
|
+
const properties = await captureProperties(
|
|
1327
|
+
session,
|
|
1328
|
+
objectId,
|
|
1329
|
+
MAX_SCOPE_VARIABLES,
|
|
1330
|
+
MAX_VARIABLE_DEPTH,
|
|
1331
|
+
maxValueLength
|
|
1332
|
+
);
|
|
1307
1333
|
const structured = {};
|
|
1308
1334
|
for (const variable of properties) {
|
|
1309
1335
|
structured[variable.name] = toStructuredValue(variable);
|
|
@@ -1322,7 +1348,7 @@ function normalizeRenderedObjectCapture(rendered, original) {
|
|
|
1322
1348
|
}
|
|
1323
1349
|
return rendered;
|
|
1324
1350
|
}
|
|
1325
|
-
async function withSerializedObjectCapture(session, expression, evalResult, captured) {
|
|
1351
|
+
async function withSerializedObjectCapture(session, expression, evalResult, captured, maxValueLength) {
|
|
1326
1352
|
if (captured.error !== void 0 || captured.value === void 0) {
|
|
1327
1353
|
return captured;
|
|
1328
1354
|
}
|
|
@@ -1330,7 +1356,7 @@ async function withSerializedObjectCapture(session, expression, evalResult, capt
|
|
|
1330
1356
|
if (objectId === void 0) {
|
|
1331
1357
|
return captured;
|
|
1332
1358
|
}
|
|
1333
|
-
const rendered = await renderObjectCapture(session, objectId);
|
|
1359
|
+
const rendered = await renderObjectCapture(session, objectId, maxValueLength);
|
|
1334
1360
|
if (rendered === void 0) {
|
|
1335
1361
|
return captured;
|
|
1336
1362
|
}
|
|
@@ -1338,10 +1364,11 @@ async function withSerializedObjectCapture(session, expression, evalResult, capt
|
|
|
1338
1364
|
if (normalized === void 0) {
|
|
1339
1365
|
return captured;
|
|
1340
1366
|
}
|
|
1341
|
-
const value = limitValueLength(normalized);
|
|
1367
|
+
const value = limitValueLength(normalized, maxValueLength);
|
|
1342
1368
|
return captured.type === void 0 ? { expression, value } : { expression, value, type: captured.type };
|
|
1343
1369
|
}
|
|
1344
1370
|
async function captureSnapshot(session, pause, options = {}) {
|
|
1371
|
+
const maxValueLength = resolveMaxValueLength(options.maxValueLength);
|
|
1345
1372
|
const top = pause.callFrames[0];
|
|
1346
1373
|
let topFrame;
|
|
1347
1374
|
let captures = [];
|
|
@@ -1353,7 +1380,7 @@ async function captureSnapshot(session, pause, options = {}) {
|
|
|
1353
1380
|
column: top.columnNumber + 1
|
|
1354
1381
|
};
|
|
1355
1382
|
if (options.includeScopes === true) {
|
|
1356
|
-
const scopes = await captureScopes(session, top);
|
|
1383
|
+
const scopes = await captureScopes(session, top, maxValueLength);
|
|
1357
1384
|
topFrame = { ...topFrame, scopes };
|
|
1358
1385
|
}
|
|
1359
1386
|
if (options.captures !== void 0 && options.captures.length > 0) {
|
|
@@ -1361,11 +1388,17 @@ async function captureSnapshot(session, pause, options = {}) {
|
|
|
1361
1388
|
options.captures.map(async (expression) => {
|
|
1362
1389
|
try {
|
|
1363
1390
|
const result = await evaluateOnFrame(session, top.callFrameId, expression);
|
|
1364
|
-
const captured = evalResultToCaptured(expression, result);
|
|
1365
|
-
return await withSerializedObjectCapture(
|
|
1391
|
+
const captured = evalResultToCaptured(expression, result, maxValueLength);
|
|
1392
|
+
return await withSerializedObjectCapture(
|
|
1393
|
+
session,
|
|
1394
|
+
expression,
|
|
1395
|
+
result,
|
|
1396
|
+
captured,
|
|
1397
|
+
maxValueLength
|
|
1398
|
+
);
|
|
1366
1399
|
} catch (err) {
|
|
1367
1400
|
const message = err instanceof Error ? err.message : String(err);
|
|
1368
|
-
return { expression, error: limitValueLength(message) };
|
|
1401
|
+
return { expression, error: limitValueLength(message, maxValueLength) };
|
|
1369
1402
|
}
|
|
1370
1403
|
})
|
|
1371
1404
|
);
|
|
@@ -1643,6 +1676,7 @@ async function handleSnapshot(opts) {
|
|
|
1643
1676
|
const remoteRoot = parseRemoteRoot(opts.remoteRoot);
|
|
1644
1677
|
const captures = parseCaptureList(opts.capture);
|
|
1645
1678
|
const timeoutSec = parsePositiveInt(opts.timeout, "--timeout") ?? DEFAULT_BREAKPOINT_TIMEOUT_SEC;
|
|
1679
|
+
const maxValueLength = parsePositiveInt(opts.maxValueLength, "--max-value-length");
|
|
1646
1680
|
const timeoutMs = timeoutSec * 1e3;
|
|
1647
1681
|
const condition = opts.condition !== void 0 && opts.condition.trim().length > 0 ? opts.condition.trim() : void 0;
|
|
1648
1682
|
const result = await withSession(target, async (session) => {
|
|
@@ -1677,7 +1711,8 @@ async function handleSnapshot(opts) {
|
|
|
1677
1711
|
const pausedStartedAt = pause.receivedAtMs ?? performance2.now();
|
|
1678
1712
|
const snapshot = await captureSnapshot(session, pause, {
|
|
1679
1713
|
captures,
|
|
1680
|
-
includeScopes: opts.includeScopes === true
|
|
1714
|
+
includeScopes: opts.includeScopes === true,
|
|
1715
|
+
...maxValueLength === void 0 ? {} : { maxValueLength }
|
|
1681
1716
|
});
|
|
1682
1717
|
if (opts.keepPaused === true) {
|
|
1683
1718
|
return withPausedDuration(snapshot, null);
|
|
@@ -1848,7 +1883,7 @@ async function main(argv) {
|
|
|
1848
1883
|
"Breakpoint location (repeatable; first hit wins), e.g. src/handler.ts:42",
|
|
1849
1884
|
collectStrings,
|
|
1850
1885
|
[]
|
|
1851
|
-
).option("--capture <expr,\u2026>", "Top-level comma-separated expressions to evaluate in the paused frame").option("--timeout <seconds>", "How long to wait for the breakpoint to hit (default: 30)").option("--remote-root <value>", "Path-mapping anchor: literal path or regex:<pattern> / /pattern/flags").option(
|
|
1886
|
+
).option("--capture <expr,\u2026>", "Top-level comma-separated expressions to evaluate in the paused frame").option("--timeout <seconds>", "How long to wait for the breakpoint to hit (default: 30)").option("--max-value-length <chars>", "Maximum characters per captured value before truncation (default: 4096)").option("--remote-root <value>", "Path-mapping anchor: literal path or regex:<pattern> / /pattern/flags").option(
|
|
1852
1887
|
"--condition <expr>",
|
|
1853
1888
|
"Only pause when this JS expression evaluates truthy in the paused frame"
|
|
1854
1889
|
).option("--include-scopes", "Include expanded paused-frame scopes in the snapshot").option("--no-json", "Print a human-readable summary instead of JSON").option("--keep-paused", "Skip Debugger.resume after capture; Node may resume when this CLI disconnects").option("--fail-on-unmatched-pause", "Fail immediately if the target pauses somewhere else").action(async (opts) => {
|