fix: harden web probe observe diagnostics (#684)
Co-authored-by: Codex <codex@noreply.local>
This commit is contained in:
+186
-12
@@ -77,6 +77,7 @@ interface NodeWebProbeObserveOptions {
|
||||
waitMs: number;
|
||||
tailLines: number;
|
||||
maxFiles: number;
|
||||
collectFile: string | null;
|
||||
full: boolean;
|
||||
stateDir: string | null;
|
||||
jobId: string | null;
|
||||
@@ -6903,6 +6904,7 @@ function parseNodeWebProbeObserveOptions(
|
||||
"--wait-ms",
|
||||
"--tail-lines",
|
||||
"--max-files",
|
||||
"--file",
|
||||
"--state-dir",
|
||||
"--job-id",
|
||||
"--type",
|
||||
@@ -6918,6 +6920,8 @@ function parseNodeWebProbeObserveOptions(
|
||||
const jobId = optionValue(args, "--job-id") ?? observeId ?? indexed?.id ?? null;
|
||||
if (stateDir !== null && !isSafeWebObserveStateDir(stateDir)) throw new Error(`unsafe web-probe observe --state-dir: ${stateDir}`);
|
||||
if (jobId !== null && !isSafeWebObserveJobId(jobId)) throw new Error(`unsafe web-probe observe --job-id: ${jobId}`);
|
||||
const collectFile = optionValue(args, "--file") ?? null;
|
||||
if (collectFile !== null && !isSafeWebObserveCollectFile(collectFile)) throw new Error(`unsafe web-probe observe --file: ${collectFile}`);
|
||||
if (observeActionRaw !== "start" && stateDir === null && jobId === null) {
|
||||
throw new Error("web-probe observe status|command|stop|collect|analyze requires --state-dir or --job-id");
|
||||
}
|
||||
@@ -6937,6 +6941,7 @@ function parseNodeWebProbeObserveOptions(
|
||||
waitMs: positiveIntegerOption(args, "--wait-ms", observeActionRaw === "command" || observeActionRaw === "stop" ? 30000 : 0, 600000),
|
||||
tailLines: positiveIntegerOption(args, "--tail-lines", 5, 200),
|
||||
maxFiles: positiveIntegerOption(args, "--max-files", 80, 5000),
|
||||
collectFile,
|
||||
full: args.includes("--full"),
|
||||
stateDir,
|
||||
jobId,
|
||||
@@ -7739,15 +7744,15 @@ function renderWebObserveCommandTable(value: Record<string, unknown>): string {
|
||||
return lines.join("\n");
|
||||
}
|
||||
|
||||
function runNodeWebProbeObserveCollect(options: NodeWebProbeObserveOptions, spec: HwlabRuntimeLaneSpec): Record<string, unknown> {
|
||||
function runNodeWebProbeObserveCollect(options: NodeWebProbeObserveOptions, spec: HwlabRuntimeLaneSpec): Record<string, unknown> | RenderedCliResult {
|
||||
const script = [
|
||||
"set -eu",
|
||||
nodeWebObserveResolveStateDirShell(options),
|
||||
nodeWebObserveCollectNodeScript(options.maxFiles),
|
||||
nodeWebObserveCollectNodeScript(options.maxFiles, options.collectFile),
|
||||
].join("\n");
|
||||
const result = runTransWorkspaceStdinScript(options.node, spec.workspace, script, options.commandTimeoutSeconds);
|
||||
const collect = parseJsonObject(result.stdout);
|
||||
return {
|
||||
return withWebObserveCollectRendered({
|
||||
ok: result.exitCode === 0 && collect?.ok !== false,
|
||||
status: result.exitCode === 0 ? "collected" : "blocked",
|
||||
command: webObserveCommandLabel("collect", options),
|
||||
@@ -7756,11 +7761,147 @@ function runNodeWebProbeObserveCollect(options: NodeWebProbeObserveOptions, spec
|
||||
lane: options.lane,
|
||||
workspace: spec.workspace,
|
||||
collect,
|
||||
result: compactCommandResultWithStdoutTail(result),
|
||||
result: collect === null ? compactCommandResultWithStdoutTail(result) : compactCommandResult(result),
|
||||
valuesRedacted: true,
|
||||
});
|
||||
}
|
||||
|
||||
function withWebObserveCollectRendered(value: Record<string, unknown>): RenderedCliResult {
|
||||
return {
|
||||
ok: value.ok !== false,
|
||||
command: typeof value.command === "string" ? value.command : "hwlab nodes web-probe observe collect",
|
||||
contentType: "text/plain",
|
||||
renderedText: renderWebObserveCollectTable(value),
|
||||
};
|
||||
}
|
||||
|
||||
function renderWebObserveCollectTable(value: Record<string, unknown>): string {
|
||||
const collect = record(value.collect);
|
||||
const result = record(value.result);
|
||||
const file = record(collect.file);
|
||||
const files = Array.isArray(collect.files) ? collect.files.map(record).filter((item): item is Record<string, unknown> => item !== null).slice(0, 20) : [];
|
||||
const jsonlTail = Array.isArray(file.jsonlTail) ? file.jsonlTail.map(record).filter((item): item is Record<string, unknown> => item !== null).slice(-8) : [];
|
||||
const jsonSummary = record(file.jsonSummary);
|
||||
const jsonRunnerErrors = Array.isArray(jsonSummary.runnerErrors) ? jsonSummary.runnerErrors.map(record).filter((item): item is Record<string, unknown> => item !== null).slice(-8) : [];
|
||||
const jsonFindings = Array.isArray(jsonSummary.findings) ? jsonSummary.findings.map(record).filter((item): item is Record<string, unknown> => item !== null).slice(0, 8) : [];
|
||||
const lines = [
|
||||
`hwlab nodes web-probe observe collect (${webObserveText(value.status)})`,
|
||||
"",
|
||||
webObserveTable(["ID", "NODE", "LANE", "MODE", "STATE_DIR"], [[
|
||||
webObserveText(value.id),
|
||||
webObserveText(value.node),
|
||||
webObserveText(value.lane),
|
||||
webObserveText(collect.mode ?? "files"),
|
||||
webObserveShort(webObserveText(collect.stateDir), 88),
|
||||
]]),
|
||||
"",
|
||||
];
|
||||
if (collect.mode === "file") {
|
||||
lines.push(
|
||||
"File:",
|
||||
webObserveTable(["RELATIVE", "BYTES", "TRUNC", "SHA256"], [[
|
||||
webObserveShort(webObserveText(file.relative), 64),
|
||||
webObserveText(file.byteCount),
|
||||
webObserveText(file.truncated),
|
||||
webObserveShort(webObserveText(file.sha256), 28),
|
||||
]]),
|
||||
"",
|
||||
);
|
||||
if (Object.keys(jsonSummary).length > 0) {
|
||||
lines.push(
|
||||
"JSON summary:",
|
||||
webObserveTable(["KEYS", "COUNTS", "PARSE_ERROR"], [[
|
||||
webObserveShort(webObserveText(jsonSummary.topLevelKeys), 80),
|
||||
webObserveShort(webObserveText(jsonSummary.counts), 80),
|
||||
webObserveShort(webObserveText(jsonSummary.parseError), 80),
|
||||
]]),
|
||||
"",
|
||||
);
|
||||
}
|
||||
if (jsonRunnerErrors.length > 0) {
|
||||
lines.push(
|
||||
"Runner errors:",
|
||||
webObserveTable(["TS", "TYPE", "ATTEMPTS", "READY", "MESSAGE"], jsonRunnerErrors.map((item) => [
|
||||
webObserveShort(webObserveText(item.ts), 24),
|
||||
webObserveShort(webObserveText(item.type), 20),
|
||||
webObserveText(item.attemptCount),
|
||||
webObserveShort(webObserveText(item.lastReadinessReason), 24),
|
||||
webObserveShort(webObserveText(item.message ?? item.lastError), 96),
|
||||
])),
|
||||
"",
|
||||
);
|
||||
}
|
||||
if (jsonFindings.length > 0) {
|
||||
lines.push(
|
||||
"Findings:",
|
||||
webObserveTable(["KIND", "SEVERITY", "COUNT", "SUMMARY"], jsonFindings.map((item) => [
|
||||
webObserveShort(webObserveText(item.kind ?? item.id ?? item.code), 48),
|
||||
webObserveText(item.severity ?? item.level),
|
||||
webObserveText(item.count ?? item.sampleCount),
|
||||
webObserveShort(webObserveText(item.summary ?? item.message), 96),
|
||||
])),
|
||||
"",
|
||||
);
|
||||
}
|
||||
if (jsonlTail.length > 0) {
|
||||
lines.push(
|
||||
"JSONL tail:",
|
||||
webObserveTable(["TS", "TYPE", "COMMAND", "ATTEMPTS", "READY", "DOM", "PATH", "MESSAGE"], jsonlTail.map((item) => {
|
||||
const error = record(item.error);
|
||||
const attempts = Array.isArray(error.attempts) ? error.attempts : [];
|
||||
const lastAttempt = attempts.length > 0 ? record(attempts[attempts.length - 1]) : {};
|
||||
const rawReadiness = record(lastAttempt.readiness ?? error.navigationReadiness);
|
||||
const readiness = record(rawReadiness.snapshot ?? rawReadiness);
|
||||
const domBits = [
|
||||
`shell=${readiness.workbenchShellVisible === true ? "Y" : "n"}`,
|
||||
`create=${readiness.sessionCreateVisible === true ? "Y" : "n"}`,
|
||||
`input=${readiness.commandInputPresent === true ? "Y" : "n"}`,
|
||||
`tab=${readiness.activeTabPresent === true ? "Y" : "n"}`,
|
||||
`login=${readiness.loginVisible === true ? "Y" : "n"}`,
|
||||
].join(" ");
|
||||
return [
|
||||
webObserveShort(webObserveText(item.ts), 24),
|
||||
webObserveShort(webObserveText(item.type), 20),
|
||||
webObserveShort(webObserveText(item.commandId), 28),
|
||||
webObserveText(attempts.length),
|
||||
webObserveShort(webObserveText(readiness.reason), 24),
|
||||
webObserveShort(domBits, 48),
|
||||
webObserveShort(webObserveText(item.path ?? item.urlPath ?? item.url ?? item.currentUrl ?? item.status), 60),
|
||||
webObserveShort(webObserveText(error.message ?? item.message ?? item.text ?? item.preview), 96),
|
||||
];
|
||||
})),
|
||||
"",
|
||||
);
|
||||
} else if (typeof file.content === "string" && file.content.length > 0) {
|
||||
lines.push("Content preview:", webObserveShort(file.content, 4000), "");
|
||||
}
|
||||
} else {
|
||||
lines.push(
|
||||
"Files:",
|
||||
webObserveTable(["RELATIVE", "BYTES", "SHA256"], files.length > 0 ? files.map((item) => [
|
||||
webObserveShort(webObserveText(item.relative), 72),
|
||||
webObserveText(item.byteCount),
|
||||
webObserveShort(webObserveText(item.sha256), 28),
|
||||
]) : [["-", "-", "-"]]),
|
||||
"",
|
||||
);
|
||||
}
|
||||
if (value.ok === false) {
|
||||
lines.push(
|
||||
"Blocked detail:",
|
||||
webObserveTable(["REASON", "EXIT", "TIMEOUT", "STDERR"], [[
|
||||
webObserveShort(webObserveText(collect.reason ?? value.degradedReason), 48),
|
||||
webObserveText(result.exitCode),
|
||||
webObserveText(result.timedOut),
|
||||
webObserveShort(webObserveText(result.stderr), 120),
|
||||
]]),
|
||||
"",
|
||||
);
|
||||
}
|
||||
lines.push("Disclosure:", " collect is bounded; use --file <relative> for id-specific drill-down instead of raw remote cat.");
|
||||
return lines.join("\n");
|
||||
}
|
||||
|
||||
function runNodeWebProbeObserveAnalyze(options: NodeWebProbeObserveOptions, spec: HwlabRuntimeLaneSpec): Record<string, unknown> | RenderedCliResult {
|
||||
const analyzerB64 = Buffer.from(nodeWebObserveAnalyzerSource(), "utf8").toString("base64");
|
||||
const script = [
|
||||
@@ -7796,6 +7937,8 @@ function runNodeWebProbeObserveAnalyze(options: NodeWebProbeObserveOptions, spec
|
||||
"const takeHead = (value, limit) => Array.isArray(value) ? value.slice(0, limit) : [];",
|
||||
"const takeTail = (value, limit) => Array.isArray(value) ? value.slice(-limit) : [];",
|
||||
"const firstArray = (...values) => { for (const value of values) if (Array.isArray(value)) return value; return []; };",
|
||||
"const firstNonEmptyArray = (...values) => { for (const value of values) if (Array.isArray(value) && value.length > 0) return value; return firstArray(...values); };",
|
||||
"const readJsonlTail = (path, limit) => readText(path).split(/\\r?\\n/).filter(Boolean).slice(-limit).map((line) => { try { return JSON.parse(line); } catch { return null; } }).filter(Boolean);",
|
||||
"const mergeArrays = (...values) => { const out = []; const seen = new Set(); for (const value of values) { if (!Array.isArray(value)) continue; for (const item of value) { const key = JSON.stringify([item?.id ?? item?.kind ?? item?.code ?? item?.columnLabel ?? item?.traceId ?? null, item?.path ?? item?.urlPath ?? null, item?.method ?? null, item?.status ?? null, item?.summary ?? item?.message ?? item?.fromSeq ?? item?.firstAt ?? null, item?.toSeq ?? item?.lastAt ?? null]); if (seen.has(key)) continue; seen.add(key); out.push(item); } } return out; };",
|
||||
"const clip = (value, limit = 160) => value === null || value === undefined ? null : String(value).slice(0, limit);",
|
||||
"const findingRank = (item) => { const id = String(item?.id ?? item?.kind ?? item?.code ?? ''); if (id === 'page-performance-slow-same-origin-api') return 0; if (id.startsWith('turn-timing-total-elapsed')) return 1; if (id.startsWith('turn-timing-recent-update')) return 2; if (id.includes('runtime-execution') || id.includes('prompt-chat-submit-failed')) return 3; return 10; };",
|
||||
@@ -7816,7 +7959,8 @@ function runNodeWebProbeObserveAnalyze(options: NodeWebProbeObserveOptions, spec
|
||||
"const slimDomSample = (item) => { const v = objectOrNull(item) || {}; return { seq: v.seq ?? null, ts: v.ts ?? null, source: clip(v.source, 32), diagnosticCode: clip(v.diagnosticCode, 48), traceId: clip(v.traceId, 64), httpStatus: v.httpStatus ?? null, idleSeconds: v.idleSeconds ?? null, waitingFor: clip(v.waitingFor, 48), lastEventLabel: clip(v.lastEventLabel, 80), text: clip(v.text ?? v.preview, 180) }; };",
|
||||
"const slimConsoleGroup = (item) => { const v = objectOrNull(item) || {}; return { count: v.count ?? null, type: clip(v.type, 24), status: v.status ?? null, path: clip(v.path ?? v.urlPath, 96), lastAt: v.lastAt ?? v.firstAt ?? null, firstAt: v.firstAt ?? null, traceIds: Array.isArray(v.traceIds) ? v.traceIds.slice(0, 3).map((x) => clip(x, 64)) : [] }; };",
|
||||
"const slimConsoleSample = (item) => { const v = objectOrNull(item) || {}; return { ts: v.ts ?? null, type: clip(v.type, 24), status: v.status ?? null, path: clip(v.path ?? v.urlPath, 96), traceId: clip(v.traceId, 64), text: clip(v.text ?? v.preview, 180) }; };",
|
||||
"const slimRunnerError = (item) => { const v = objectOrNull(item) || {}; return { ts: v.ts ?? null, type: clip(v.type, 32), commandId: clip(v.commandId, 80), sampleSeq: v.sampleSeq ?? null, message: clip(v.message, 240), retry: clip(v.retry, 24), retryExhausted: v.retryExhausted === true, lastError: clip(v.lastError, 180) }; };",
|
||||
"const slimRunnerError = (item) => { const v = objectOrNull(item) || {}; const readiness = objectOrNull(v.lastReadiness); return { ts: v.ts ?? null, type: clip(v.type, 32), commandId: clip(v.commandId, 80), sampleSeq: v.sampleSeq ?? null, message: clip(v.message, 240), retry: clip(v.retry, 24), retryExhausted: v.retryExhausted === true, lastError: clip(v.lastError, 180), attemptCount: v.attemptCount ?? null, lastFailureKind: clip(v.lastFailureKind, 48), lastReadinessReason: clip(v.lastReadinessReason, 48), lastReadiness: readiness ? { reason: clip(readiness.reason, 48), path: clip(readiness.path, 96), readyState: clip(readiness.readyState, 24), workbenchShellVisible: readiness.workbenchShellVisible === true, sessionCreateVisible: readiness.sessionCreateVisible === true, commandInputPresent: readiness.commandInputPresent === true, activeTabPresent: readiness.activeTabPresent === true, warningPresent: readiness.warningPresent === true, loginVisible: readiness.loginVisible === true, bodyTextHash: clip(readiness.bodyTextHash, 80) } : null }; };",
|
||||
"const slimRunnerErrorFromJsonl = (item) => { const v = objectOrNull(item) || {}; const error = objectOrNull(v.error) || {}; const attempts = Array.isArray(error.attempts) ? error.attempts : []; const lastAttempt = attempts.length > 0 ? objectOrNull(attempts[attempts.length - 1]) || {} : {}; const rawReadiness = objectOrNull(lastAttempt.readiness) || objectOrNull(error.navigationReadiness); const readiness = objectOrNull(rawReadiness?.snapshot) || rawReadiness; return { ts: v.ts ?? null, type: v.type ?? null, commandId: v.commandId ?? null, sampleSeq: v.sampleSeq ?? null, message: error.message ?? v.message ?? null, attemptCount: attempts.length, lastFailureKind: lastAttempt.failureKind ?? null, lastReadinessReason: rawReadiness?.reason ?? readiness?.reason ?? null, lastReadiness: readiness ?? null }; };",
|
||||
"const slimJump = (item) => { const v = objectOrNull(item) || {}; return { columnLabel: v.columnLabel ?? null, promptIndex: v.promptIndex ?? null, fromSeq: v.fromSeq ?? null, toSeq: v.toSeq ?? null, fromValue: v.fromValue ?? null, toValue: v.toValue ?? null, delta: v.delta ?? null, sampleDeltaSeconds: v.sampleDeltaSeconds ?? null, allowedIncreaseSeconds: v.allowedIncreaseSeconds ?? null, traceId: v.traceId ?? null }; };",
|
||||
"const slimLoadingOwner = (item) => { const v = objectOrNull(item) || {}; return { ownerKey: clip(v.ownerKey, 120), ownerKind: clip(v.ownerKind, 32), ownerLabel: clip(v.ownerLabel, 120), sampleCount: v.sampleCount ?? null, occurrenceCount: v.occurrenceCount ?? null, maxSimultaneousCount: v.maxSimultaneousCount ?? null, longestContinuousSeconds: v.longestContinuousSeconds ?? null, firstSeq: v.firstSeq ?? null, lastSeq: v.lastSeq ?? null, promptIndexes: Array.isArray(v.promptIndexes) ? v.promptIndexes.slice(0, 8) : [] }; };",
|
||||
"const slimLoadingSegment = (item) => { const v = objectOrNull(item) || {}; return { firstAt: v.firstAt ?? null, lastAt: v.lastAt ?? null, endedAt: v.endedAt ?? null, firstSeq: v.firstSeq ?? null, lastSeq: v.lastSeq ?? null, durationSeconds: v.durationSeconds ?? null, upperBoundSeconds: v.upperBoundSeconds ?? null, endedGapSeconds: v.endedGapSeconds ?? null, sampleCount: v.sampleCount ?? null, maxCount: v.maxCount ?? null, ownerCount: v.ownerCount ?? null, ongoing: v.ongoing === true, owners: Array.isArray(v.owners) ? v.owners.slice(0, 6).map((owner) => ({ ownerKind: clip(owner?.ownerKind, 32), ownerLabel: clip(owner?.ownerLabel, 120), count: owner?.count ?? null })) : [] }; };",
|
||||
@@ -7858,6 +8002,7 @@ function runNodeWebProbeObserveAnalyze(options: NodeWebProbeObserveOptions, spec
|
||||
"const combinedFindings = sortFindings(firstArray(source?.findings, fullRecentWindow?.findings, fullSource?.findings));",
|
||||
"const srcPromptNetwork = objectOrNull(source?.promptNetwork);",
|
||||
"const promptNetwork = srcPromptNetwork ? { promptSegments: srcPromptNetwork.promptSegments ?? null } : null;",
|
||||
"const runnerErrorsFromJsonl = readJsonlTail(reportJsonPath.replace(/\\/analysis\\/report\\.json$/u, '/errors.jsonl'), 8).filter((item) => item?.type === 'runner-error').map(slimRunnerErrorFromJsonl);",
|
||||
"const compact = source ? {",
|
||||
" ok: analyzerExit === 0,",
|
||||
" counts: source.counts ?? null,",
|
||||
@@ -7875,7 +8020,7 @@ function runNodeWebProbeObserveAnalyze(options: NodeWebProbeObserveOptions, spec
|
||||
" domDiagnosticSamples: takeHead(firstArray(source.domDiagnosticSamples, fullRecentWindow?.runtimeAlerts?.domDiagnostics, fullSource?.domDiagnosticSamples, fullSource?.runtimeAlerts?.domDiagnostics), 5).map(slimDomSample),",
|
||||
" consoleAlertGroups: takeHead(firstArray(source.consoleAlertGroups, fullRecentWindow?.runtimeAlerts?.consoleAlertsByPath, fullSource?.consoleAlertGroups, fullSource?.runtimeAlerts?.consoleAlertsByPath), 5).map(slimConsoleGroup),",
|
||||
" consoleAlertSamples: takeHead(firstArray(source.consoleAlertSamples, fullRecentWindow?.runtimeAlerts?.consoleAlerts, fullSource?.consoleAlertSamples, fullSource?.runtimeAlerts?.consoleAlerts), 5).map(slimConsoleSample),",
|
||||
" runnerErrors: takeTail(firstArray(source.runnerErrors, fullSource?.runnerErrors), 8).map(slimRunnerError),",
|
||||
" runnerErrors: takeTail(firstNonEmptyArray(source.runnerErrors, fullSource?.runnerErrors, runnerErrorsFromJsonl), 8).map(slimRunnerError),",
|
||||
" turnTimingRecentUpdateJumps: takeHead(firstArray(source.turnTimingRecentUpdateJumps, srcMetrics?.turnTimingRecentUpdateJumps, srcMetrics?.turnTimingRecentUpdateSawtoothJumps, fullRecentWindow?.sampleMetrics?.turnTimingRecentUpdateSawtoothJumps, fullRecentWindow?.turnTimingRecentUpdateJumps, fullSource?.sampleMetrics?.turnTimingRecentUpdateSawtoothJumps, fullSource?.turnTimingRecentUpdateJumps), 5).map(slimJump),",
|
||||
" turnTimingElapsedZeroResets: takeHead(firstArray(source.turnTimingElapsedZeroResets, srcMetrics?.turnTimingElapsedZeroResets, fullRecentWindow?.sampleMetrics?.turnTimingElapsedZeroResets, fullRecentWindow?.turnTimingElapsedZeroResets, fullSource?.sampleMetrics?.turnTimingElapsedZeroResets, fullSource?.turnTimingElapsedZeroResets), 5).map(slimJump),",
|
||||
" turnTimingTotalElapsedForwardJumps: takeHead(firstArray(source.turnTimingTotalElapsedForwardJumps, srcMetrics?.turnTimingTotalElapsedForwardJumps, fullRecentWindow?.sampleMetrics?.turnTimingTotalElapsedForwardJumps, fullRecentWindow?.turnTimingTotalElapsedForwardJumps, fullSource?.sampleMetrics?.turnTimingTotalElapsedForwardJumps, fullSource?.turnTimingTotalElapsedForwardJumps), 5).map(slimJump),",
|
||||
@@ -8301,13 +8446,16 @@ function renderWebObserveAnalyzeTable(value: Record<string, unknown>): string {
|
||||
]]),
|
||||
"",
|
||||
"Runner errors:",
|
||||
webObserveTable(["TS", "TYPE", "RETRY", "EXHAUSTED", "MESSAGE"], runnerErrors.length > 0 ? runnerErrors.map((item) => [
|
||||
webObserveTable(["TS", "TYPE", "RETRY", "EXH", "ATTEMPTS", "LAST_KIND", "READY", "MESSAGE"], runnerErrors.length > 0 ? runnerErrors.map((item) => [
|
||||
webObserveShort(webObserveText(item.ts), 24),
|
||||
webObserveShort(webObserveText(item.type), 18),
|
||||
webObserveText(item.retry),
|
||||
webObserveText(item.retryExhausted),
|
||||
webObserveText(item.attemptCount),
|
||||
webObserveShort(webObserveText(item.lastFailureKind), 24),
|
||||
webObserveShort(webObserveText(item.lastReadinessReason), 24),
|
||||
webObserveShort(webObserveText(item.message || item.lastError), 96),
|
||||
]) : [["-", "-", "-", "-", "-"]]),
|
||||
]) : [["-", "-", "-", "-", "-", "-", "-", "-"]]),
|
||||
"",
|
||||
"HTTP error groups:",
|
||||
webObserveTable(["COUNT", "METHOD", "STATUS", "PATH", "LAST", "FAILURE"], httpErrorRows.length > 0 ? httpErrorRows : [["-", "-", "-", "-", "-", "-"]]),
|
||||
@@ -9060,16 +9208,31 @@ console.log(JSON.stringify({ok:true,command:'web-probe-observe status',stateDir:
|
||||
`)} "$state_dir" ${shellQuote(node)} ${shellQuote(lane)}`;
|
||||
}
|
||||
|
||||
function nodeWebObserveCollectNodeScript(maxFiles: number): string {
|
||||
function nodeWebObserveCollectNodeScript(maxFiles: number, collectFile: string | null): string {
|
||||
return `node -e ${shellQuote(`
|
||||
const fs=require('fs'),path=require('path'),crypto=require('crypto');
|
||||
const dir=process.argv[1]; const maxFiles=${maxFiles};
|
||||
const dir=process.argv[1]; const selected=process.argv[2]||''; const maxFiles=${maxFiles}; const maxReadBytes=64*1024;
|
||||
const shaFile=(file)=>'sha256:'+crypto.createHash('sha256').update(fs.readFileSync(file)).digest('hex');
|
||||
const safeRel=(value)=>Boolean(value)&&!path.isAbsolute(value)&&!value.includes('..')&&!value.includes('\\\\')&&value.split('/').every((part)=>part&&part!=='.'&&part!=='..');
|
||||
if(selected){
|
||||
if(!safeRel(selected)){console.log(JSON.stringify({ok:false,command:'web-probe-observe collect',stateDir:dir,mode:'file',reason:'unsafe-file',file:selected,valuesRedacted:true},null,2)); process.exit(2);}
|
||||
const file=path.join(dir,selected); const relative=path.relative(dir,file);
|
||||
if(relative.startsWith('..')||path.isAbsolute(relative)){console.log(JSON.stringify({ok:false,command:'web-probe-observe collect',stateDir:dir,mode:'file',reason:'file-outside-state-dir',file:selected,valuesRedacted:true},null,2)); process.exit(2);}
|
||||
if(!fs.existsSync(file)||!fs.statSync(file).isFile()){console.log(JSON.stringify({ok:false,command:'web-probe-observe collect',stateDir:dir,mode:'file',reason:'file-not-found',file:selected,valuesRedacted:true},null,2)); process.exit(1);}
|
||||
const st=fs.statSync(file); const raw=fs.readFileSync(file); const text=raw.slice(0,Math.min(raw.length,maxReadBytes)).toString('utf8'); const truncated=raw.length>maxReadBytes;
|
||||
const lines=text.split(/\\r?\\n/).filter(Boolean); const isJsonl=selected.endsWith('.jsonl');
|
||||
let jsonSummary=null;
|
||||
if(selected.endsWith('.json')&&!truncated){try{const parsed=JSON.parse(text); jsonSummary={topLevelKeys:parsed&&typeof parsed==='object'&&!Array.isArray(parsed)?Object.keys(parsed).slice(0,40):[],counts:parsed&&parsed.counts||null,runnerErrors:Array.isArray(parsed&&parsed.runnerErrors)?parsed.runnerErrors.slice(-8):[],findings:Array.isArray(parsed&&parsed.findings)?parsed.findings.slice(0,8):[]}}catch(error){jsonSummary={parseError:String(error&&error.message||error).slice(0,160)}}}
|
||||
const jsonlTail=isJsonl?lines.slice(-20).map((line,index)=>{try{return JSON.parse(line)}catch(error){return {parseError:true,index,lineTail:line.slice(-500),error:String(error&&error.message||error).slice(0,160)}}}):null;
|
||||
console.log(JSON.stringify({ok:true,command:'web-probe-observe collect',stateDir:dir,mode:'file',file:{path:file,relative,byteCount:st.size,sha256:shaFile(file),truncated,content:isJsonl||jsonSummary?undefined:text,jsonlTail,jsonSummary,lineCount:lines.length},valuesRedacted:true},null,2));
|
||||
process.exit(0);
|
||||
}
|
||||
const out=[]; const walk=(p)=>{for(const ent of fs.readdirSync(p,{withFileTypes:true})){const full=path.join(p,ent.name); if(ent.isDirectory()) walk(full); else out.push(full); if(out.length>=maxFiles) return;}};
|
||||
walk(dir);
|
||||
const files=out.slice(0,maxFiles).map(file=>{const st=fs.statSync(file); const hash=crypto.createHash('sha256').update(fs.readFileSync(file)).digest('hex'); return {path:file,relative:path.relative(dir,file),byteCount:st.size,sha256:'sha256:'+hash}});
|
||||
const files=out.slice(0,maxFiles).map(file=>{const st=fs.statSync(file); return {path:file,relative:path.relative(dir,file),byteCount:st.size,sha256:shaFile(file)}});
|
||||
const totalBytes=files.reduce((sum,item)=>sum+item.byteCount,0);
|
||||
console.log(JSON.stringify({ok:true,command:'web-probe-observe collect',stateDir:dir,fileCount:files.length,totalBytes,files,valuesRedacted:true},null,2));
|
||||
`)} "$state_dir"`;
|
||||
`)} "$state_dir" ${shellQuote(collectFile ?? "")}`;
|
||||
}
|
||||
|
||||
function nodeWebObserveWaitCommandShell(commandId: string, waitMs: number): string {
|
||||
@@ -9113,6 +9276,17 @@ function isSafeWebObserveStateDir(value: string): boolean {
|
||||
&& /^\.state\/web-observe\/[A-Za-z0-9_.-]+\/[A-Za-z0-9_.-]+\/\d{4}\/\d{2}\/\d{2}\/[A-Za-z0-9_.TZ-]+_[A-Za-z0-9_.-]+_webobs-[A-Za-z0-9_.-]+$/u.test(value);
|
||||
}
|
||||
|
||||
function isSafeWebObserveCollectFile(value: string): boolean {
|
||||
return value.length > 0
|
||||
&& value.length <= 240
|
||||
&& !value.includes("\0")
|
||||
&& !value.includes("..")
|
||||
&& !value.startsWith("/")
|
||||
&& !value.includes("\\")
|
||||
&& value.split("/").every((part) => part.length > 0 && part !== "." && part !== "..")
|
||||
&& /^[A-Za-z0-9_.\/-]+$/u.test(value);
|
||||
}
|
||||
|
||||
function isSafeWebObserveJobId(value: string): boolean {
|
||||
return /^webobs-[A-Za-z0-9_.-]+$/u.test(value);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user