2128 lines
159 KiB
TypeScript
2128 lines
159 KiB
TypeScript
// SPEC: PJ2026-01060307 控制面模块化 draft-2026-06-25-p0. web-probe-observe module for scripts/src/hwlab-node-impl.ts.
|
|
|
|
// Moved mechanically from scripts/src/hwlab-node-impl.ts:7427-9000 for #903.
|
|
|
|
// SPEC: PJ2026-01060505 Workbench Performance draft-2026-06-17-p0.
|
|
// SPEC: PJ2026-01060508 Web哨兵 draft-2026-06-25-p0-web-probe-sentinel.
|
|
// SPEC: PJ2026-01060508 Web哨兵 draft-2026-06-26-p9-multi-web-probe-sentinel.
|
|
// Responsibility: YAML-first node/lane operations, including Workbench observability control commands.
|
|
import { createHash, randomBytes } from "node:crypto";
|
|
import { existsSync, mkdirSync, readFileSync, renameSync, writeFileSync } from "node:fs";
|
|
import { dirname, join } from "node:path";
|
|
import { repoRoot, rootPath, type Config } from "../config";
|
|
import { runCommand, type CommandResult } from "../command";
|
|
import { startJob } from "../jobs";
|
|
import { classifySshTcpPoolFailure } from "../ssh";
|
|
import { HWLAB_NODE_CONTROL_PLANE_CONFIG_PATH, hwlabNodeControlPlaneInfraHelp, runHwlabNodeControlPlaneInfra } from "../hwlab-node-control-plane";
|
|
import { hwlabRuntimeLaneConfigPath, hwlabRuntimeLaneIds, hwlabRuntimeLaneSpec, hwlabRuntimeLaneSpecForNode, hwlabRuntimeNodeIds, isHwlabRuntimeLane, type HwlabRuntimeLane, type HwlabRuntimeLaneSpec, type HwlabRuntimeObservabilityRecordingRuleSpec, type HwlabRuntimeObservabilitySpec, type HwlabRuntimeObservabilityWarningAlertSpec, type HwlabRuntimePublicExposureSpec, type HwlabRuntimeWebProbeAlertThresholdsSpec, type HwlabRuntimeWebProbeProjectManagementSpec } from "../hwlab-node-lanes";
|
|
import { nodeWebProbeScriptRunnerSource } from "../hwlab-node-web-probe-runner-source";
|
|
import { nodeWebObserveAnalyzerSource } from "../hwlab-node-web-observe-analyzer-source";
|
|
import { nodeWebObserveRunnerSource } from "../hwlab-node-web-observe-runner-source";
|
|
import { nodeWebObserveCollectViewNodeScript, parseNodeWebProbeObserveCollectView, type NodeWebProbeObserveCollectView } from "../hwlab-node-web-observe-collect";
|
|
import { withWebObserveCollectRendered, withWebObserveCommandRendered, withWebObserveStatusRendered } from "../hwlab-node-web-observe-render";
|
|
import { buildWebObserveWrapperForObserveOptions, webObserveWrapperStateDirFromStatus } from "../hwlab-node-web-observe-wrapper";
|
|
import { renderWebObserveWrapperContract } from "../hwlab-node-web-observe-wrapper-render";
|
|
import { runWebProbeSentinelCommand, type WebProbeSentinelOptions, type WebProbeSentinelReportView } from "../hwlab-node-web-sentinel-cicd";
|
|
import { hwlabNodeHelp, hwlabNodeObservabilityHelp, hwlabNodeWebProbeHelp } from "../hwlab-node-help";
|
|
import { compactWebProbeResult, compactWebProbeScriptResult } from "../hwlab-node-web-probe-summary";
|
|
import { nodeObservabilityRecordingRuleExpression, nodeObservabilityRecordingRuleSummaries, nodeObservabilityWarningAlertExpression, nodeObservabilityWarningAlertSummaries } from "../hwlab-node-observability-promql";
|
|
import { runDelegatedHwlabNodeCommand, type DelegatedNodeDomain } from "../hwlab-node-transport";
|
|
import type { RenderedCliResult } from "../output";
|
|
|
|
import type { BootstrapAdminPasswordMaterial, NodeWebProbeObserveCommandType, NodeWebProbeObserveOptions, NodeWebProbeOptions, NodeWebProbeRunOptions, NodeWebProbeScreenshotOptions, NodeWebProbeSentinelOptions, RuntimeSecretSpec, WebObserveIndexEntry, WebProbeBrowserProxyMode } from "./entry";
|
|
import { runTransWorkspaceStdinScript, runtimeSecretSpec } from "./public-exposure";
|
|
import { transPath } from "./runtime-common";
|
|
import { assertLane, assertNodeId, compactCommandResult, compactCommandResultRedacted, compactCommandResultWithStdoutTail, nullableRecord, optionValue, parseJsonObject, positiveIntegerOption, record, requiredOption, shellQuote } from "./utils";
|
|
import { nodeWebObserveResolveStateDirShell, recoverWebObserveAnalyzeTurnDetails, renderWebObserveStartResult, renderWebProbeRunResult, upsertWebObserveIndexEntry, webObserveCommandLabel, webObserveIdFromOptions, webObserveIdFromStatus, webObserveIndexEntryFromOptions, webObserveNextCommands, withWebObserveAnalyzeRendered, withWebObserveShortcuts } from "./web-observe-render";
|
|
import { commandSummaryForOutput, isSafeWebObserveArchivePrefix, isSafeWebObserveCollectFile, isSafeWebObserveFindingId, isSafeWebObserveJobId, isSafeWebObserveStateDir, isSafeWebObserveTraceId, nodeWebObserveCollectNodeScript, nodeWebObserveForceStopNodeScript, nodeWebObserveStatusNodeScript, nodeWebObserveWaitCommandShell, runNodeWebProbeScript, safeWebObserveSegment, safeWebObserveTargetSegment } from "./web-observe-scripts";
|
|
import { displayRepoPath, readBootstrapAdminPasswordMaterial, sleepSync } from "./web-probe";
|
|
|
|
export function parseNodeWebProbeSentinelOptions(args: string[]): NodeWebProbeSentinelOptions {
|
|
const [sentinelActionRaw] = args;
|
|
if (
|
|
sentinelActionRaw !== "plan"
|
|
&& sentinelActionRaw !== "status"
|
|
&& sentinelActionRaw !== "image"
|
|
&& sentinelActionRaw !== "control-plane"
|
|
&& sentinelActionRaw !== "validate"
|
|
&& sentinelActionRaw !== "maintenance"
|
|
&& sentinelActionRaw !== "report"
|
|
) {
|
|
throw new Error("web-probe sentinel usage: sentinel plan|status|image|control-plane|validate|maintenance|report --node NODE --lane vNN [--dry-run|--confirm]");
|
|
}
|
|
assertKnownOptions(args, new Set([
|
|
"--node",
|
|
"--lane",
|
|
"--timeout-seconds",
|
|
"--release-id",
|
|
"--reason",
|
|
"--view",
|
|
"--run",
|
|
"--run-id",
|
|
"--trace-id",
|
|
"--sample-seq",
|
|
"--sentinel",
|
|
"--sentinel-id",
|
|
]), new Set(["--dry-run", "--confirm", "--wait", "--quick-verify", "--raw", "--latest"]));
|
|
const node = requiredOption(args, "--node");
|
|
assertNodeId(node);
|
|
const lane = requiredOption(args, "--lane");
|
|
assertLane(lane);
|
|
const sentinelId = optionValue(args, "--sentinel") ?? optionValue(args, "--sentinel-id") ?? null;
|
|
if (sentinelId !== null && !/^[a-z0-9][a-z0-9-]{1,80}$/u.test(sentinelId)) throw new Error(`--sentinel must be a stable lowercase sentinel id, got ${sentinelId}`);
|
|
if (!isHwlabRuntimeLane(lane)) throw new Error(`web-probe only supports HWLAB runtime lanes, got ${lane}`);
|
|
const confirm = args.includes("--confirm");
|
|
const dryRun = args.includes("--dry-run");
|
|
if (confirm && dryRun) throw new Error("web-probe sentinel accepts only one of --confirm or --dry-run");
|
|
const timeoutSeconds = positiveIntegerOption(args, "--timeout-seconds", 900, 3600);
|
|
let sentinel: WebProbeSentinelOptions;
|
|
if (sentinelActionRaw === "plan" || sentinelActionRaw === "status") {
|
|
sentinel = { kind: "config", action: sentinelActionRaw, node, lane, sentinelId, dryRun };
|
|
} else if (sentinelActionRaw === "image") {
|
|
const imageAction = args[1];
|
|
if (imageAction !== "status" && imageAction !== "build") throw new Error("web-probe sentinel image usage: image status|build --node NODE --lane vNN [--dry-run|--confirm]");
|
|
sentinel = { kind: "image", action: imageAction, node, lane, sentinelId, dryRun: imageAction === "build" ? dryRun || !confirm : dryRun, confirm, wait: args.includes("--wait"), timeoutSeconds };
|
|
} else if (sentinelActionRaw === "control-plane") {
|
|
const controlPlaneAction = args[1];
|
|
if (controlPlaneAction !== "plan" && controlPlaneAction !== "apply" && controlPlaneAction !== "status" && controlPlaneAction !== "trigger-current") {
|
|
throw new Error("web-probe sentinel control-plane usage: control-plane plan|apply|status|trigger-current --node NODE --lane vNN [--dry-run|--confirm]");
|
|
}
|
|
sentinel = { kind: "control-plane", action: controlPlaneAction, node, lane, sentinelId, dryRun: controlPlaneAction === "apply" || controlPlaneAction === "trigger-current" ? dryRun || !confirm : dryRun, confirm, wait: args.includes("--wait"), timeoutSeconds };
|
|
} else if (sentinelActionRaw === "maintenance") {
|
|
const maintenanceAction = args[1];
|
|
if (maintenanceAction !== "status" && maintenanceAction !== "start" && maintenanceAction !== "stop") {
|
|
throw new Error("web-probe sentinel maintenance usage: maintenance status|start|stop --node NODE --lane vNN [--dry-run|--confirm]");
|
|
}
|
|
sentinel = {
|
|
kind: "maintenance",
|
|
action: maintenanceAction,
|
|
node,
|
|
lane,
|
|
sentinelId,
|
|
dryRun: maintenanceAction === "status" ? dryRun : dryRun || !confirm,
|
|
confirm,
|
|
wait: args.includes("--wait"),
|
|
timeoutSeconds,
|
|
releaseId: optionValue(args, "--release-id") ?? null,
|
|
reason: optionValue(args, "--reason") ?? null,
|
|
quickVerify: maintenanceAction === "stop" || args.includes("--quick-verify"),
|
|
};
|
|
} else if (sentinelActionRaw === "validate") {
|
|
sentinel = { kind: "validate", action: "validate", node, lane, sentinelId, dryRun, confirm, wait: args.includes("--wait"), timeoutSeconds, quickVerify: args.includes("--quick-verify") };
|
|
} else {
|
|
const view = parseWebProbeSentinelReportView(optionValue(args, "--view") ?? "summary");
|
|
const latest = args.includes("--latest");
|
|
const runId = optionValue(args, "--run") ?? optionValue(args, "--run-id") ?? null;
|
|
if (latest && runId !== null) throw new Error("web-probe sentinel report accepts --latest or --run/--run-id, not both");
|
|
const sampleSeqRaw = optionValue(args, "--sample-seq") ?? null;
|
|
const sampleSeq = sampleSeqRaw === null ? null : Number(sampleSeqRaw);
|
|
if (sampleSeq !== null && (!Number.isInteger(sampleSeq) || sampleSeq < 1)) throw new Error("web-probe sentinel report --sample-seq must be a positive integer");
|
|
sentinel = {
|
|
kind: "report",
|
|
action: "report",
|
|
node,
|
|
lane,
|
|
sentinelId,
|
|
view,
|
|
runId,
|
|
latest,
|
|
traceId: optionValue(args, "--trace-id") ?? null,
|
|
sampleSeq,
|
|
raw: args.includes("--raw"),
|
|
timeoutSeconds,
|
|
};
|
|
}
|
|
return {
|
|
action: "sentinel",
|
|
sentinel,
|
|
node,
|
|
lane,
|
|
};
|
|
}
|
|
|
|
function parseWebProbeSentinelReportView(value: string): WebProbeSentinelReportView {
|
|
if (value === "summary" || value === "turn-summary" || value === "findings" || value === "trace-frame" || value === "auth-session-switch-summary") return value;
|
|
throw new Error(`web-probe sentinel report --view must be summary, turn-summary, findings, trace-frame, or auth-session-switch-summary; got ${value}`);
|
|
}
|
|
|
|
export function normalizeNodeWebProbeObserveArgs(args: string[]): { args: string[]; id: string | null } {
|
|
const [observeActionRaw, maybeId, ...rest] = args;
|
|
if (observeActionRaw !== "start" && maybeId !== undefined && !maybeId.startsWith("--")) {
|
|
if (!isSafeWebObserveJobId(maybeId)) throw new Error(`unsafe web-probe observe id: ${maybeId}`);
|
|
return { args: [observeActionRaw, ...rest], id: maybeId };
|
|
}
|
|
return { args, id: null };
|
|
}
|
|
|
|
export function parseNodeWebProbeObserveOptions(
|
|
args: string[],
|
|
node: string,
|
|
lane: string,
|
|
spec: HwlabRuntimeLaneSpec,
|
|
observeId: string | null,
|
|
indexed: WebObserveIndexEntry | null,
|
|
): NodeWebProbeObserveOptions {
|
|
const [observeActionRaw] = args;
|
|
if (
|
|
observeActionRaw !== "start"
|
|
&& observeActionRaw !== "status"
|
|
&& observeActionRaw !== "command"
|
|
&& observeActionRaw !== "stop"
|
|
&& observeActionRaw !== "collect"
|
|
&& observeActionRaw !== "analyze"
|
|
) {
|
|
throw new Error("web-probe observe usage: observe start --node NODE --lane vNN [...]; observe status|command|stop|collect|analyze <id> [...]");
|
|
}
|
|
assertKnownOptions(args.slice(1), new Set([
|
|
"--node",
|
|
"--lane",
|
|
"--url",
|
|
"--target-path",
|
|
"--viewport",
|
|
"--browser-proxy-mode",
|
|
"--sample-interval-ms",
|
|
"--screenshot-interval-ms",
|
|
"--observer-refresh-interval-ms",
|
|
"--max-samples",
|
|
"--command-timeout-seconds",
|
|
"--wait-ms",
|
|
"--tail-lines",
|
|
"--max-files",
|
|
"--view",
|
|
"--file",
|
|
"--finding",
|
|
"--grep",
|
|
"--trace-id",
|
|
"--sample-seq",
|
|
"--timestamp",
|
|
"--turn",
|
|
"--command-id",
|
|
"--window-ms",
|
|
"--compact-raw",
|
|
"--archive-prefix",
|
|
"--tail-samples",
|
|
"--state-dir",
|
|
"--job-id",
|
|
"--type",
|
|
"--text",
|
|
"--path",
|
|
"--label",
|
|
"--session-id",
|
|
"--provider",
|
|
"--account-id",
|
|
"--account",
|
|
"--from-account-id",
|
|
"--to-account-id",
|
|
"--after-round",
|
|
"--severity",
|
|
"--alternate-session-strategy",
|
|
"--expected-sentinel-range",
|
|
"--finding-id",
|
|
"--source-id",
|
|
"--file-ref",
|
|
"--filename",
|
|
"--task-ref",
|
|
"--task",
|
|
"--task-id",
|
|
"--field",
|
|
"--link",
|
|
"--title",
|
|
"--body",
|
|
"--status",
|
|
"--hwpod-id",
|
|
"--node-id",
|
|
"--workspace-root",
|
|
"--workspace-root-ref",
|
|
"--root",
|
|
]), new Set(["--force", "--full", "--raw", "--text-stdin", "--require-composer-ready", "--blocking", "--non-blocking"]));
|
|
const commandTypeRaw = optionValue(args, "--type") ?? null;
|
|
const commandType = commandTypeRaw === null ? null : parseNodeWebProbeObserveCommandType(commandTypeRaw);
|
|
const stateDir = optionValue(args, "--state-dir") ?? indexed?.stateDir ?? null;
|
|
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 collectView = parseNodeWebProbeObserveCollectView(optionValue(args, "--view") ?? "files");
|
|
const collectFile = optionValue(args, "--file") ?? null;
|
|
if (collectFile !== null && !isSafeWebObserveCollectFile(collectFile)) throw new Error(`unsafe web-probe observe --file: ${collectFile}`);
|
|
const collectFinding = optionValue(args, "--finding") ?? null;
|
|
if (collectFinding !== null && !isSafeWebObserveFindingId(collectFinding)) throw new Error(`unsafe web-probe observe --finding: ${collectFinding}`);
|
|
const collectGrep = optionValue(args, "--grep") ?? null;
|
|
if (collectGrep !== null && (collectGrep.includes("\0") || collectGrep.length > 200)) throw new Error("unsafe web-probe observe --grep: expected 1-200 non-NUL chars");
|
|
const collectTraceId = optionValue(args, "--trace-id") ?? null;
|
|
if (collectTraceId !== null && !isSafeWebObserveTraceId(collectTraceId)) throw new Error(`unsafe web-probe observe --trace-id: ${collectTraceId}`);
|
|
const collectSampleSeqRaw = optionValue(args, "--sample-seq") ?? null;
|
|
const collectSampleSeq = collectSampleSeqRaw === null ? null : Number(collectSampleSeqRaw);
|
|
if (collectSampleSeq !== null && (!Number.isInteger(collectSampleSeq) || collectSampleSeq < 1)) throw new Error("unsafe web-probe observe --sample-seq: expected positive integer");
|
|
const collectTimestamp = optionValue(args, "--timestamp") ?? null;
|
|
if (collectTimestamp !== null && (collectTimestamp.includes("\0") || collectTimestamp.length > 80 || !Number.isFinite(Date.parse(collectTimestamp)))) throw new Error("unsafe web-probe observe --timestamp: expected parseable timestamp");
|
|
const collectTurnRaw = optionValue(args, "--turn") ?? null;
|
|
const collectTurn = collectTurnRaw === null ? null : Number(collectTurnRaw);
|
|
if (collectTurn !== null && (!Number.isInteger(collectTurn) || collectTurn < 1)) throw new Error("unsafe web-probe observe --turn: expected positive integer");
|
|
const collectCommandId = optionValue(args, "--command-id") ?? null;
|
|
if (collectCommandId !== null && (!/^[A-Za-z0-9_.:-]+$/u.test(collectCommandId) || collectCommandId.length > 120)) throw new Error("unsafe web-probe observe --command-id: expected 1-120 safe command id chars");
|
|
const collectWindowMsRaw = optionValue(args, "--window-ms") ?? null;
|
|
const collectWindowMs = collectWindowMsRaw === null ? null : Number(collectWindowMsRaw);
|
|
if (collectWindowMs !== null && (!Number.isInteger(collectWindowMs) || collectWindowMs < 1000 || collectWindowMs > 86_400_000)) throw new Error("unsafe web-probe observe --window-ms: expected integer 1000-86400000");
|
|
const analyzeArchivePrefix = optionValue(args, "--archive-prefix") ?? null;
|
|
if (analyzeArchivePrefix !== null && !isSafeWebObserveArchivePrefix(analyzeArchivePrefix)) throw new Error(`unsafe web-probe observe --archive-prefix: ${analyzeArchivePrefix}`);
|
|
const analyzeTailSamplesRaw = optionValue(args, "--tail-samples") ?? null;
|
|
const analyzeTailSamples = analyzeTailSamplesRaw === null ? null : Number(analyzeTailSamplesRaw);
|
|
if (analyzeTailSamples !== null && (!Number.isInteger(analyzeTailSamples) || analyzeTailSamples < 0)) {
|
|
throw new Error("unsafe web-probe observe --tail-samples: expected a non-negative integer; use 0 for all samples");
|
|
}
|
|
if (observeActionRaw !== "start" && stateDir === null && jobId === null) {
|
|
throw new Error("web-probe observe status|command|stop|collect|analyze requires --state-dir or --job-id");
|
|
}
|
|
const commandTextOption = optionValue(args, "--text") ?? null;
|
|
const commandTextFromStdin = args.includes("--text-stdin");
|
|
if (commandTextFromStdin && observeActionRaw !== "command") {
|
|
throw new Error("web-probe observe --text-stdin is only supported for observe command");
|
|
}
|
|
if (commandTextFromStdin && commandTextOption !== null) {
|
|
throw new Error("web-probe observe command accepts either --text or --text-stdin, not both");
|
|
}
|
|
const commandText = commandTextFromStdin ? readFileSync(0, "utf8") : commandTextOption;
|
|
const commandSourceId = optionValue(args, "--source-id") ?? null;
|
|
const commandAccountId = optionValue(args, "--account-id") ?? optionValue(args, "--account") ?? null;
|
|
const commandFromAccountId = optionValue(args, "--from-account-id") ?? null;
|
|
const commandToAccountId = optionValue(args, "--to-account-id") ?? null;
|
|
const commandFileRef = optionValue(args, "--file-ref") ?? null;
|
|
const commandFilename = optionValue(args, "--filename") ?? null;
|
|
const commandTaskRef = optionValue(args, "--task-ref") ?? null;
|
|
const commandTaskId = optionValue(args, "--task-id") ?? optionValue(args, "--task") ?? null;
|
|
const commandField = optionValue(args, "--field") ?? null;
|
|
const commandLink = optionValue(args, "--link") ?? null;
|
|
const commandTitle = optionValue(args, "--title") ?? null;
|
|
const commandBody = optionValue(args, "--body") ?? null;
|
|
const commandStatus = optionValue(args, "--status") ?? null;
|
|
const commandHwpodId = optionValue(args, "--hwpod-id") ?? null;
|
|
const commandNodeId = optionValue(args, "--node-id") ?? null;
|
|
const commandWorkspaceRoot = optionValue(args, "--workspace-root") ?? optionValue(args, "--workspace-root-ref") ?? null;
|
|
const commandRoot = optionValue(args, "--root") ?? null;
|
|
const commandAfterRoundRaw = optionValue(args, "--after-round") ?? null;
|
|
const commandAfterRound = commandAfterRoundRaw === null ? null : Number(commandAfterRoundRaw);
|
|
if (commandAfterRound !== null && (!Number.isInteger(commandAfterRound) || commandAfterRound < 0 || commandAfterRound > 1000)) {
|
|
throw new Error("unsafe web-probe observe --after-round: expected integer 0-1000");
|
|
}
|
|
const commandSeverity = optionValue(args, "--severity") ?? null;
|
|
const commandAlternateSessionStrategy = optionValue(args, "--alternate-session-strategy") ?? null;
|
|
const commandExpectedSentinelRange = optionValue(args, "--expected-sentinel-range") ?? null;
|
|
const commandFindingId = optionValue(args, "--finding-id") ?? null;
|
|
const commandBlocking = args.includes("--blocking") ? true : args.includes("--non-blocking") ? false : null;
|
|
for (const [label, value] of [
|
|
["--severity", commandSeverity],
|
|
["--alternate-session-strategy", commandAlternateSessionStrategy],
|
|
["--expected-sentinel-range", commandExpectedSentinelRange],
|
|
["--finding-id", commandFindingId],
|
|
["--source-id", commandSourceId],
|
|
["--account-id/--account", commandAccountId],
|
|
["--from-account-id", commandFromAccountId],
|
|
["--to-account-id", commandToAccountId],
|
|
["--file-ref", commandFileRef],
|
|
["--filename", commandFilename],
|
|
["--task-ref", commandTaskRef],
|
|
["--task/--task-id", commandTaskId],
|
|
["--field", commandField],
|
|
["--link", commandLink],
|
|
["--title", commandTitle],
|
|
["--body", commandBody],
|
|
["--status", commandStatus],
|
|
["--hwpod-id", commandHwpodId],
|
|
["--node-id", commandNodeId],
|
|
["--workspace-root/--workspace-root-ref", commandWorkspaceRoot],
|
|
["--root", commandRoot],
|
|
] as const) {
|
|
if (value !== null && (value.includes("\0") || value.length > 500)) throw new Error(`unsafe web-probe observe ${label}: expected 1-500 non-NUL chars`);
|
|
}
|
|
return {
|
|
action: "observe",
|
|
observeAction: observeActionRaw,
|
|
id: observeId ?? jobId,
|
|
node,
|
|
lane,
|
|
url: optionValue(args, "--url") ?? (observeActionRaw === "start" ? nodeWebProbeDefaultUrl(spec) : indexed?.url ?? spec.publicWebUrl),
|
|
targetPath: optionValue(args, "--target-path") ?? "/workbench",
|
|
viewport: optionValue(args, "--viewport") ?? "1440x900",
|
|
browserProxyMode: parseWebProbeBrowserProxyMode(optionValue(args, "--browser-proxy-mode") ?? spec.webProbe?.browserProxyMode),
|
|
sampleIntervalMs: positiveIntegerOption(args, "--sample-interval-ms", 5000, 600000),
|
|
screenshotIntervalMs: positiveIntegerOption(args, "--screenshot-interval-ms", 300000, 86_400_000),
|
|
observerRefreshIntervalMs: positiveIntegerOption(args, "--observer-refresh-interval-ms", 180000, 86_400_000),
|
|
maxSamples: positiveIntegerOption(args, "--max-samples", 0, 10_000_000),
|
|
commandTimeoutSeconds: positiveIntegerOption(args, "--command-timeout-seconds", 55, 3600),
|
|
waitMs: positiveIntegerOption(args, "--wait-ms", 0, 600000),
|
|
tailLines: positiveIntegerOption(args, "--tail-lines", 5, 200),
|
|
maxFiles: positiveIntegerOption(args, "--max-files", 80, 5000),
|
|
collectView,
|
|
collectFile,
|
|
collectFinding,
|
|
collectGrep,
|
|
collectTraceId,
|
|
collectSampleSeq,
|
|
collectTimestamp,
|
|
collectTurn,
|
|
collectCommandId,
|
|
collectWindowMs,
|
|
analyzeArchivePrefix,
|
|
analyzeTailSamples,
|
|
full: args.includes("--full"),
|
|
raw: args.includes("--raw"),
|
|
compactRaw: args.includes("--compact-raw"),
|
|
stateDir,
|
|
jobId,
|
|
force: args.includes("--force"),
|
|
commandType,
|
|
commandText,
|
|
commandPath: optionValue(args, "--path") ?? null,
|
|
commandLabel: optionValue(args, "--label") ?? null,
|
|
commandSessionId: optionValue(args, "--session-id") ?? null,
|
|
commandProvider: optionValue(args, "--provider") ?? null,
|
|
commandAfterRound,
|
|
commandSeverity,
|
|
commandAlternateSessionStrategy,
|
|
commandExpectedSentinelRange,
|
|
commandRequireComposerReady: args.includes("--require-composer-ready"),
|
|
commandFindingId,
|
|
commandBlocking,
|
|
commandAccountId,
|
|
commandFromAccountId,
|
|
commandToAccountId,
|
|
commandSourceId,
|
|
commandFileRef,
|
|
commandFilename,
|
|
commandTaskRef,
|
|
commandTaskId,
|
|
commandField,
|
|
commandLink,
|
|
commandTitle,
|
|
commandBody,
|
|
commandStatus,
|
|
commandHwpodId,
|
|
commandNodeId,
|
|
commandWorkspaceRoot,
|
|
commandRoot,
|
|
};
|
|
}
|
|
|
|
export function parseNodeWebProbeObserveCommandType(value: string): NodeWebProbeObserveCommandType {
|
|
if (
|
|
value === "login"
|
|
|| value === "loginAccount"
|
|
|| value === "logout"
|
|
|| value === "listSessions"
|
|
|| value === "switchSessions"
|
|
|| value === "preflight"
|
|
|| value === "goto"
|
|
|| value === "newSession"
|
|
|| value === "sendPrompt"
|
|
|| value === "steer"
|
|
|| value === "cancel"
|
|
|| value === "selectProvider"
|
|
|| value === "clickSession"
|
|
|| value === "refreshCurrentSession"
|
|
|| value === "switchAwayAndBack"
|
|
|| value === "assertSessionInvariant"
|
|
|| value === "gotoProjectMdtodo"
|
|
|| value === "selectProjectSource"
|
|
|| value === "selectMdtodoSource"
|
|
|| value === "selectMdtodoFile"
|
|
|| value === "selectMdtodoTask"
|
|
|| value === "expandMdtodoTask"
|
|
|| value === "openMdtodoReportPreview"
|
|
|| value === "toggleMdtodoReportFullscreen"
|
|
|| value === "openMdtodoSourceConfig"
|
|
|| value === "configureMdtodoHwpodSource"
|
|
|| value === "probeMdtodoSource"
|
|
|| value === "reindexMdtodoSource"
|
|
|| value === "editMdtodoTaskInline"
|
|
|| value === "editMdtodoTaskTitle"
|
|
|| value === "editMdtodoTaskBody"
|
|
|| value === "toggleMdtodoTaskStatus"
|
|
|| value === "addMdtodoRootTask"
|
|
|| value === "addMdtodoSubTask"
|
|
|| value === "continueMdtodoTask"
|
|
|| value === "deleteMdtodoTask"
|
|
|| value === "launchWorkbenchFromTask"
|
|
|| value === "launchWorkbenchFromMdtodo"
|
|
|| value === "screenshot"
|
|
|| value === "mark"
|
|
|| value === "stop"
|
|
) return value;
|
|
throw new Error(`web-probe observe command --type must be login, loginAccount, logout, listSessions, switchSessions, preflight, goto, gotoProjectMdtodo, newSession, sendPrompt, steer, cancel, selectProvider, clickSession, refreshCurrentSession, switchAwayAndBack, assertSessionInvariant, selectProjectSource, selectMdtodoSource, selectMdtodoFile, selectMdtodoTask, expandMdtodoTask, openMdtodoReportPreview, toggleMdtodoReportFullscreen, openMdtodoSourceConfig, configureMdtodoHwpodSource, probeMdtodoSource, reindexMdtodoSource, editMdtodoTaskInline, editMdtodoTaskTitle, editMdtodoTaskBody, toggleMdtodoTaskStatus, addMdtodoRootTask, addMdtodoSubTask, continueMdtodoTask, deleteMdtodoTask, launchWorkbenchFromTask, launchWorkbenchFromMdtodo, screenshot, mark, or stop; got ${value}`);
|
|
}
|
|
|
|
export function parseWebProbeBrowserProxyMode(value: string | undefined): WebProbeBrowserProxyMode {
|
|
if (value === undefined || value === "auto") return "auto";
|
|
if (value === "direct") return "direct";
|
|
throw new Error(`web-probe --browser-proxy-mode must be auto or direct, got ${value}`);
|
|
}
|
|
|
|
export function nodeWebProbeDefaultUrl(spec: HwlabRuntimeLaneSpec): string {
|
|
const origin = spec.webProbe?.defaultOrigin;
|
|
if (origin === undefined || origin.mode === "public") return origin?.baseUrl ?? spec.publicWebUrl;
|
|
const clusterIp = resolveKubernetesServiceClusterIp(spec, origin.namespace, origin.serviceName, new Map());
|
|
return `${origin.scheme}://${clusterIp}:${origin.port}`;
|
|
}
|
|
|
|
export function nodeWebProbeAutoCommandTimeoutSeconds(input: {
|
|
timeoutMs: number;
|
|
waitAfterSubmitMs: number;
|
|
waitMessagesMs: number;
|
|
waitAgentTerminalMs: number;
|
|
traceSampleCount: number;
|
|
traceSampleIntervalMs: number;
|
|
freshSession: boolean;
|
|
hasMessage: boolean;
|
|
}): number {
|
|
const traceWindowMs = input.traceSampleCount > 0
|
|
? Math.max(0, input.traceSampleCount - 1) * input.traceSampleIntervalMs
|
|
: 0;
|
|
const startupBudgetMs = input.timeoutMs + 30_000;
|
|
const freshnessBudgetMs = input.freshSession ? Math.min(input.timeoutMs, 30_000) : 0;
|
|
const submitBudgetMs = input.hasMessage ? input.waitAfterSubmitMs + input.waitMessagesMs + 15_000 : 0;
|
|
const terminalBudgetMs = input.waitAgentTerminalMs > 0 ? input.waitAgentTerminalMs : 0;
|
|
const totalMs = startupBudgetMs + freshnessBudgetMs + submitBudgetMs + terminalBudgetMs + traceWindowMs + 15_000;
|
|
return Math.min(3600, Math.max(60, Math.ceil(totalMs / 1000)));
|
|
}
|
|
|
|
export function assertKnownOptions(args: string[], valueOptions: Set<string>, flagOptions: Set<string>): void {
|
|
for (let index = 0; index < args.length; index += 1) {
|
|
const arg = args[index] ?? "";
|
|
if (!arg.startsWith("--")) continue;
|
|
if (flagOptions.has(arg)) continue;
|
|
if (valueOptions.has(arg)) {
|
|
index += 1;
|
|
continue;
|
|
}
|
|
throw new Error(`unknown option: ${arg}`);
|
|
}
|
|
}
|
|
|
|
export function runNodeWebProbe(options: NodeWebProbeOptions): Record<string, unknown> | RenderedCliResult {
|
|
const lane = options.lane;
|
|
if (!isHwlabRuntimeLane(lane)) throw new Error(`web-probe only supports HWLAB runtime lanes, got ${lane}`);
|
|
const spec = hwlabRuntimeLaneSpecForNode(lane, options.node);
|
|
if (options.action === "sentinel") return runWebProbeSentinelCommand(spec, options.sentinel);
|
|
if (options.action === "screenshot") return runNodeWebProbeScreenshot(options, spec);
|
|
if (options.action === "observe" && options.observeAction !== "start") return runNodeWebProbeObserve(options, spec, null, null, null);
|
|
const secretSpec = runtimeSecretSpec({ node: options.node, lane });
|
|
const material = readBootstrapAdminPasswordMaterial(secretSpec);
|
|
const credential = webProbeCredential(secretSpec, material);
|
|
if (!material.ok || material.password === null) {
|
|
return {
|
|
ok: false,
|
|
status: "blocked",
|
|
command: options.action === "observe"
|
|
? `web-probe observe ${options.observeAction} --node ${options.node} --lane ${options.lane}`
|
|
: `web-probe ${options.action} --node ${options.node} --lane ${options.lane}`,
|
|
node: options.node,
|
|
lane: options.lane,
|
|
workspace: spec.workspace,
|
|
url: options.url,
|
|
degradedReason: "web_login_secret_missing",
|
|
credential,
|
|
next: { secretStatus: `bun scripts/cli.ts hwlab nodes secret status --node ${options.node} --lane ${options.lane} --name ${secretSpec.bootstrapAdminSecret}` },
|
|
};
|
|
}
|
|
if (options.action === "observe") return runNodeWebProbeObserve(options, spec, secretSpec, material, credential);
|
|
if (options.action === "script") return runNodeWebProbeScript(options, spec, secretSpec, material, credential);
|
|
if (options.commandTimeoutSeconds > 55) return runNodeWebProbeAsync(options, spec, secretSpec, material, credential);
|
|
const probeArgs = nodeWebProbeRunArgs(options, "run");
|
|
const webProbeProxy = nodeWebProbeHostProxyEnv(spec);
|
|
const script = [
|
|
"set -eu",
|
|
`${[...webProbeProxy.envAssignments, `HWLAB_WEB_USER=${shellQuote(secretSpec.bootstrapAdminUsername)}`, `HWLAB_WEB_PASS=${shellQuote(material.password)}`].join(" ")} ${probeArgs.map(shellQuote).join(" ")}`,
|
|
].join("\n");
|
|
const result = runTransWorkspaceStdinScript(options.node, spec.workspace, script, options.commandTimeoutSeconds);
|
|
const probe = compactWebProbeResult(parseJsonObject(result.stdout));
|
|
const passed = result.exitCode === 0 && probe?.status === "pass";
|
|
const summary = nullableRecord(probe?.summary);
|
|
const degradedReason = result.timedOut
|
|
? "web-probe-command-timeout"
|
|
: typeof probe?.degradedReason === "string"
|
|
? probe.degradedReason
|
|
: null;
|
|
return renderWebProbeRunResult({
|
|
ok: passed,
|
|
status: passed ? "pass" : "blocked",
|
|
command: `web-probe run --node ${options.node} --lane ${options.lane}`,
|
|
node: options.node,
|
|
lane: options.lane,
|
|
workspace: spec.workspace,
|
|
url: options.url,
|
|
network: webProbeProxy.summary,
|
|
credential,
|
|
commandTimeout: {
|
|
seconds: options.commandTimeoutSeconds,
|
|
autoSeconds: options.commandTimeoutAutoSeconds,
|
|
userProvided: options.commandTimeoutUserProvided,
|
|
timedOut: result.timedOut,
|
|
},
|
|
degradedReason,
|
|
failureKind: typeof summary?.failureKind === "string" ? summary.failureKind : null,
|
|
summary,
|
|
probe,
|
|
result: compactCommandResult(result),
|
|
valuesRedacted: true,
|
|
});
|
|
}
|
|
|
|
export function runNodeWebProbeScreenshot(options: NodeWebProbeScreenshotOptions, spec: HwlabRuntimeLaneSpec): Record<string, unknown> {
|
|
const route = `${options.node}:${spec.workspace}`;
|
|
const script = webProbeScreenshotRemoteScript(options);
|
|
const result = runCommand([
|
|
transPath(),
|
|
route,
|
|
"playwright",
|
|
"--local-dir",
|
|
options.localDir,
|
|
"--wait-timeout-ms",
|
|
String(options.waitTimeoutMs),
|
|
"--inactivity-timeout-ms",
|
|
"30000",
|
|
...(options.keepRemote ? ["--keep-remote"] : []),
|
|
], repoRoot, { input: script, timeoutMs: options.commandTimeoutSeconds * 1000 });
|
|
const transport = record(parseJsonObject(result.stdout));
|
|
const transportParsed = Object.keys(transport).length > 0;
|
|
const artifacts = Array.isArray(transport.artifacts) ? transport.artifacts.map(record) : [];
|
|
const screenshot = artifacts.find((artifact) => {
|
|
const remotePath = typeof artifact.remotePath === "string" ? artifact.remotePath : "";
|
|
const localPath = typeof artifact.localPath === "string" ? artifact.localPath : "";
|
|
return remotePath.endsWith(".png") || localPath.endsWith(".png");
|
|
}) ?? null;
|
|
const remoteSummary = parseWebProbeScreenshotSummary(record(transport.remote).stdoutTail);
|
|
const compactScreenshot = screenshot === null ? null : compactWebProbeScreenshotArtifact(screenshot);
|
|
const compactArtifacts = artifacts.map(compactWebProbeScreenshotArtifact);
|
|
const remoteRecord = record(transport.remote);
|
|
const ok = result.exitCode === 0 && transport.ok === true && screenshot !== null && screenshot.verified !== false;
|
|
const degradedReason = ok
|
|
? null
|
|
: result.timedOut
|
|
? "web-probe-screenshot-command-timeout"
|
|
: !transportParsed
|
|
? "web-probe-screenshot-transport-unparseable"
|
|
: transport.ok === false
|
|
? "web-probe-screenshot-remote-failed"
|
|
: screenshot === null
|
|
? "web-probe-screenshot-artifact-missing"
|
|
: "web-probe-screenshot-download-unverified";
|
|
return {
|
|
ok,
|
|
status: ok ? "pass" : "blocked",
|
|
command: `web-probe screenshot --node ${options.node} --lane ${options.lane}`,
|
|
node: options.node,
|
|
lane: options.lane,
|
|
workspace: spec.workspace,
|
|
route,
|
|
url: options.url,
|
|
viewport: options.viewport,
|
|
localDir: options.localDir,
|
|
screenshot: compactScreenshot,
|
|
artifacts: compactArtifacts,
|
|
artifactCount: artifacts.length,
|
|
remote: {
|
|
exitCode: remoteRecord.exitCode ?? null,
|
|
remoteDir: remoteRecord.remoteDir ?? null,
|
|
defaultScreenshot: remoteRecord.defaultScreenshot ?? null,
|
|
stdoutTail: ok ? "" : typeof remoteRecord.stdoutTail === "string" ? remoteRecord.stdoutTail.slice(-1200) : "",
|
|
stderrTail: ok ? "" : typeof remoteRecord.stderrTail === "string" ? remoteRecord.stderrTail.slice(-1200) : "",
|
|
},
|
|
page: compactWebProbeScreenshotPageSummary(remoteSummary),
|
|
transport: {
|
|
runId: transport.runId ?? null,
|
|
artifactCount: transport.artifactCount ?? null,
|
|
expectedArtifactCount: transport.expectedArtifactCount ?? null,
|
|
cleanup: compactWebProbeScreenshotCleanup(transport.cleanup),
|
|
downloadFailure: transport.downloadFailure ?? null,
|
|
},
|
|
result: compactCommandResult(result),
|
|
degradedReason,
|
|
valuesRedacted: true,
|
|
};
|
|
}
|
|
|
|
function compactWebProbeScreenshotArtifact(artifact: Record<string, unknown>): Record<string, unknown> {
|
|
const transfer = record(artifact.transfer);
|
|
return {
|
|
remotePath: typeof artifact.remotePath === "string" ? artifact.remotePath : null,
|
|
localPath: typeof artifact.localPath === "string" ? artifact.localPath : null,
|
|
bytes: Number.isFinite(Number(artifact.bytes)) ? Number(artifact.bytes) : null,
|
|
sha256: typeof artifact.sha256 === "string" ? artifact.sha256 : null,
|
|
verified: artifact.verified === true,
|
|
transfer: Object.keys(transfer).length === 0 ? null : {
|
|
strategy: transfer.strategy ?? null,
|
|
transport: transfer.transport ?? null,
|
|
chunks: transfer.chunks ?? null,
|
|
elapsedMs: transfer.elapsedMs ?? null,
|
|
throughputBytesPerSecond: transfer.throughputBytesPerSecond ?? null,
|
|
},
|
|
};
|
|
}
|
|
|
|
function compactWebProbeScreenshotPageSummary(value: Record<string, unknown> | null): Record<string, unknown> | null {
|
|
if (value === null) return null;
|
|
const layout = record(value.layout);
|
|
return {
|
|
ok: value.ok === true,
|
|
status: value.status ?? null,
|
|
title: value.title ?? null,
|
|
finalUrl: value.finalUrl ?? null,
|
|
executablePath: value.executablePath ?? null,
|
|
viewport: value.viewport ?? null,
|
|
fullPage: value.fullPage ?? null,
|
|
selector: value.selector ?? null,
|
|
layout: {
|
|
viewport: record(layout.viewport),
|
|
documentSize: record(layout.documentSize),
|
|
horizontalOverflow: layout.horizontalOverflow === true,
|
|
overflowCount: layout.overflowCount ?? null,
|
|
overflow: Array.isArray(layout.overflow) ? layout.overflow.slice(0, 5).map(record) : [],
|
|
},
|
|
consoleCount: value.consoleCount ?? null,
|
|
requestFailureCount: value.requestFailureCount ?? null,
|
|
};
|
|
}
|
|
|
|
function compactWebProbeScreenshotCleanup(value: unknown): Record<string, unknown> | null {
|
|
const cleanup = record(value);
|
|
if (Object.keys(cleanup).length === 0) return null;
|
|
return {
|
|
attempted: cleanup.attempted ?? null,
|
|
kept: cleanup.kept ?? null,
|
|
remoteDir: cleanup.remoteDir ?? null,
|
|
exitCode: cleanup.exitCode ?? null,
|
|
ok: cleanup.ok ?? null,
|
|
};
|
|
}
|
|
|
|
function webProbeScreenshotRemoteScript(options: NodeWebProbeScreenshotOptions): string {
|
|
const [widthRaw, heightRaw] = options.viewport.split("x");
|
|
return [
|
|
"set -eu",
|
|
`export UNIDESK_WEB_PROBE_SCREENSHOT_URL=${shellQuote(options.url)}`,
|
|
`export UNIDESK_WEB_PROBE_SCREENSHOT_PATH="$UNIDESK_PLAYWRIGHT_REMOTE_DIR"/${shellQuote(options.name)}`,
|
|
`export UNIDESK_WEB_PROBE_SCREENSHOT_WIDTH=${shellQuote(widthRaw ?? "1440")}`,
|
|
`export UNIDESK_WEB_PROBE_SCREENSHOT_HEIGHT=${shellQuote(heightRaw ?? "900")}`,
|
|
`export UNIDESK_WEB_PROBE_SCREENSHOT_TIMEOUT_MS=${shellQuote(String(options.timeoutMs))}`,
|
|
`export UNIDESK_WEB_PROBE_SCREENSHOT_WAIT_UNTIL=${shellQuote(options.waitUntil)}`,
|
|
`export UNIDESK_WEB_PROBE_SCREENSHOT_FULL_PAGE=${shellQuote(options.fullPage ? "1" : "0")}`,
|
|
`export UNIDESK_WEB_PROBE_SCREENSHOT_SELECTOR=${shellQuote(options.selector ?? "")}`,
|
|
"if command -v chromium >/dev/null 2>&1; then",
|
|
" export UNIDESK_WEB_PROBE_SCREENSHOT_EXECUTABLE_PATH=$(command -v chromium)",
|
|
"elif command -v chromium-browser >/dev/null 2>&1; then",
|
|
" export UNIDESK_WEB_PROBE_SCREENSHOT_EXECUTABLE_PATH=$(command -v chromium-browser)",
|
|
"elif command -v google-chrome >/dev/null 2>&1; then",
|
|
" export UNIDESK_WEB_PROBE_SCREENSHOT_EXECUTABLE_PATH=$(command -v google-chrome)",
|
|
"else",
|
|
" export UNIDESK_WEB_PROBE_SCREENSHOT_EXECUTABLE_PATH=",
|
|
"fi",
|
|
"cat > \"$UNIDESK_PLAYWRIGHT_REMOTE_DIR/web-probe-screenshot.mjs\" <<'WEB_PROBE_SCREENSHOT_JS'",
|
|
webProbeScreenshotRemoteModule(),
|
|
"WEB_PROBE_SCREENSHOT_JS",
|
|
"bun \"$UNIDESK_PLAYWRIGHT_REMOTE_DIR/web-probe-screenshot.mjs\"",
|
|
].join("\n");
|
|
}
|
|
|
|
function webProbeScreenshotRemoteModule(): string {
|
|
return String.raw`import { chromium } from "playwright";
|
|
|
|
const url = process.env.UNIDESK_WEB_PROBE_SCREENSHOT_URL;
|
|
const screenshotPath = process.env.UNIDESK_WEB_PROBE_SCREENSHOT_PATH;
|
|
const width = Number(process.env.UNIDESK_WEB_PROBE_SCREENSHOT_WIDTH || 1440);
|
|
const height = Number(process.env.UNIDESK_WEB_PROBE_SCREENSHOT_HEIGHT || 900);
|
|
const timeout = Number(process.env.UNIDESK_WEB_PROBE_SCREENSHOT_TIMEOUT_MS || 30000);
|
|
const waitUntil = process.env.UNIDESK_WEB_PROBE_SCREENSHOT_WAIT_UNTIL || "networkidle";
|
|
const fullPage = process.env.UNIDESK_WEB_PROBE_SCREENSHOT_FULL_PAGE !== "0";
|
|
const selector = process.env.UNIDESK_WEB_PROBE_SCREENSHOT_SELECTOR || "";
|
|
const executablePath = process.env.UNIDESK_WEB_PROBE_SCREENSHOT_EXECUTABLE_PATH || "";
|
|
|
|
if (!url || !screenshotPath) throw new Error("missing screenshot URL or path");
|
|
|
|
const consoleMessages = [];
|
|
const requestFailures = [];
|
|
const launchOptions = {
|
|
headless: true,
|
|
args: ["--disable-gpu", "--no-sandbox"],
|
|
...(executablePath ? { executablePath } : {}),
|
|
};
|
|
const browser = await chromium.launch(launchOptions);
|
|
const context = await browser.newContext({ viewport: { width, height }, deviceScaleFactor: 1, isMobile: width <= 560 });
|
|
const page = await context.newPage();
|
|
page.on("console", (message) => {
|
|
if (consoleMessages.length < 20) consoleMessages.push({ type: message.type(), text: message.text().slice(0, 240) });
|
|
});
|
|
page.on("requestfailed", (request) => {
|
|
if (requestFailures.length < 20) requestFailures.push({ url: request.url().slice(0, 240), method: request.method(), failure: request.failure()?.errorText || null });
|
|
});
|
|
|
|
let status = null;
|
|
try {
|
|
const response = await page.goto(url, { timeout, waitUntil });
|
|
status = response?.status() ?? null;
|
|
await page.waitForTimeout(350);
|
|
if (selector) await page.locator(selector).screenshot({ path: screenshotPath, timeout });
|
|
else await page.screenshot({ path: screenshotPath, fullPage, animations: "disabled" });
|
|
const layout = await page.evaluate(() => {
|
|
const doc = document.documentElement;
|
|
const body = document.body;
|
|
const viewport = { width: window.innerWidth, height: window.innerHeight };
|
|
const documentSize = {
|
|
width: Math.max(doc.scrollWidth, body?.scrollWidth || 0),
|
|
height: Math.max(doc.scrollHeight, body?.scrollHeight || 0),
|
|
};
|
|
const overflow = [];
|
|
let overflowCount = 0;
|
|
for (const element of Array.from(document.querySelectorAll("body *"))) {
|
|
const rect = element.getBoundingClientRect();
|
|
const right = rect.right;
|
|
const bottom = rect.bottom;
|
|
const overflowRight = right - viewport.width;
|
|
const overflowLeft = -rect.left;
|
|
if (overflowRight > 1 || overflowLeft > 1) {
|
|
overflowCount += 1;
|
|
if (overflow.length < 5) {
|
|
overflow.push({
|
|
tag: element.tagName.toLowerCase(),
|
|
className: String(element.className || "").slice(0, 80),
|
|
text: String(element.textContent || "").replace(/\s+/g, " ").trim().slice(0, 80),
|
|
x: Math.round(rect.x),
|
|
y: Math.round(rect.y),
|
|
width: Math.round(rect.width),
|
|
height: Math.round(rect.height),
|
|
overflowRight: Math.max(0, Math.round(overflowRight)),
|
|
overflowLeft: Math.max(0, Math.round(overflowLeft)),
|
|
bottom: Math.round(bottom),
|
|
});
|
|
}
|
|
}
|
|
}
|
|
return {
|
|
title: document.title,
|
|
finalUrl: window.location.href,
|
|
viewport,
|
|
documentSize,
|
|
horizontalOverflow: documentSize.width > viewport.width + 1,
|
|
overflowCount,
|
|
overflow,
|
|
};
|
|
});
|
|
console.log("__WEB_PROBE_SCREENSHOT_JSON__" + JSON.stringify({
|
|
ok: true,
|
|
url,
|
|
finalUrl: page.url(),
|
|
status,
|
|
title: await page.title(),
|
|
screenshotPath,
|
|
executablePath: executablePath || null,
|
|
viewport: { width, height },
|
|
fullPage,
|
|
selector: selector || null,
|
|
layout,
|
|
consoleCount: consoleMessages.length,
|
|
requestFailureCount: requestFailures.length,
|
|
consoleMessages: consoleMessages.slice(0, 5),
|
|
requestFailures: requestFailures.slice(0, 5),
|
|
valuesRedacted: true,
|
|
}));
|
|
} finally {
|
|
await context.close().catch(() => {});
|
|
await browser.close().catch(() => {});
|
|
}
|
|
`;
|
|
}
|
|
|
|
function parseWebProbeScreenshotSummary(value: unknown): Record<string, unknown> | null {
|
|
if (typeof value !== "string" || value.length === 0) return null;
|
|
const marker = "__WEB_PROBE_SCREENSHOT_JSON__";
|
|
const index = value.lastIndexOf(marker);
|
|
if (index < 0) return null;
|
|
try {
|
|
return record(JSON.parse(value.slice(index + marker.length).trim()));
|
|
} catch {
|
|
return null;
|
|
}
|
|
}
|
|
|
|
export function nodeWebProbeRunArgs(options: NodeWebProbeRunOptions, command: "run" | "start"): string[] {
|
|
const probeArgs = [
|
|
"node",
|
|
"scripts/web-live-dom-probe.mjs",
|
|
command,
|
|
"--url", options.url,
|
|
"--timeout-ms", String(options.timeoutMs),
|
|
"--wait-after-submit-ms", String(options.waitAfterSubmitMs),
|
|
"--wait-messages-ms", String(options.waitMessagesMs),
|
|
];
|
|
if (options.waitAgentTerminalMs > 0) probeArgs.push("--wait-agent-terminal-ms", String(options.waitAgentTerminalMs));
|
|
if (options.traceSampleCount > 0) probeArgs.push("--trace-sample-count", String(options.traceSampleCount), "--trace-sample-interval-ms", String(options.traceSampleIntervalMs));
|
|
if (options.freshSession) probeArgs.push("--fresh-session");
|
|
if (!options.cancelRunning) probeArgs.push("--no-cancel-running");
|
|
if (options.conversationId !== null) probeArgs.push("--conversation-id", options.conversationId);
|
|
if (options.message !== null) probeArgs.push("--message", options.message);
|
|
return probeArgs;
|
|
}
|
|
|
|
export function runNodeWebProbeAsync(
|
|
options: NodeWebProbeRunOptions,
|
|
spec: HwlabRuntimeLaneSpec,
|
|
secretSpec: RuntimeSecretSpec,
|
|
material: BootstrapAdminPasswordMaterial,
|
|
credential: Record<string, unknown>,
|
|
): Record<string, unknown> {
|
|
const startArgs = nodeWebProbeRunArgs(options, "start");
|
|
const webProbeProxy = nodeWebProbeHostProxyEnv(spec);
|
|
const startScript = [
|
|
"set -eu",
|
|
`${[...webProbeProxy.envAssignments, `HWLAB_WEB_USER=${shellQuote(secretSpec.bootstrapAdminUsername)}`, `HWLAB_WEB_PASS=${shellQuote(material.password ?? "")}`].join(" ")} ${startArgs.map(shellQuote).join(" ")}`,
|
|
].join("\n");
|
|
const startResult = runTransWorkspaceStdinScript(options.node, spec.workspace, startScript, 55);
|
|
const start = parseJsonObject(startResult.stdout);
|
|
const jobId = typeof start?.jobId === "string" ? start.jobId : null;
|
|
if (startResult.exitCode !== 0 || jobId === null) {
|
|
return renderWebProbeRunResult({
|
|
ok: false,
|
|
status: "blocked",
|
|
command: `web-probe run --node ${options.node} --lane ${options.lane}`,
|
|
node: options.node,
|
|
lane: options.lane,
|
|
workspace: spec.workspace,
|
|
url: options.url,
|
|
network: webProbeProxy.summary,
|
|
credential,
|
|
mode: "async-start",
|
|
commandTimeout: webProbeCommandTimeoutSummary(options, false),
|
|
degradedReason: startResult.timedOut ? "web-probe-command-timeout" : "web-probe-async-start-failed",
|
|
start: start ?? null,
|
|
result: compactCommandResultRedacted(startResult, [material.password ?? ""]),
|
|
valuesRedacted: true,
|
|
});
|
|
}
|
|
const poll = pollNodeWebProbeJob(options, spec, jobId);
|
|
const statusReport = record(record(poll.status).report);
|
|
const reportPath = typeof start.reportPath === "string" ? start.reportPath : null;
|
|
const reportLoad = Object.keys(statusReport).length > 0
|
|
? { source: "status", report: statusReport, result: null as CommandResult | null, degradedReason: null as string | null, path: null as string | null }
|
|
: readNodeWebProbeReport(options, spec, reportPath);
|
|
const report = reportLoad.report ?? {};
|
|
const reportRecovered = Object.keys(report).length > 0;
|
|
const probe = compactWebProbeResult(Object.keys(report).length > 0 ? report : null);
|
|
const passed = probe?.status === "pass";
|
|
const summary = nullableRecord(probe?.summary);
|
|
const degradedReason = poll.timedOut
|
|
? "web-probe-command-timeout"
|
|
: typeof probe?.degradedReason === "string"
|
|
? probe.degradedReason
|
|
: poll.status === null
|
|
? reportRecovered
|
|
? reportLoad.degradedReason
|
|
: reportLoad.degradedReason ?? "web-probe-async-status-failed"
|
|
: reportLoad.degradedReason;
|
|
return renderWebProbeRunResult({
|
|
ok: passed,
|
|
status: passed ? "pass" : "blocked",
|
|
command: `web-probe run --node ${options.node} --lane ${options.lane}`,
|
|
node: options.node,
|
|
lane: options.lane,
|
|
workspace: spec.workspace,
|
|
url: options.url,
|
|
network: webProbeProxy.summary,
|
|
credential,
|
|
mode: "async",
|
|
commandTimeout: webProbeCommandTimeoutSummary(options, poll.timedOut),
|
|
degradedReason,
|
|
failureKind: typeof summary?.failureKind === "string" ? summary.failureKind : null,
|
|
summary,
|
|
job: {
|
|
jobId,
|
|
startedAt: start.startedAt ?? null,
|
|
polls: poll.polls,
|
|
elapsedMs: poll.elapsedMs,
|
|
statusCommand: start.statusCommand ?? `node scripts/web-live-dom-probe.mjs status ${jobId}`,
|
|
},
|
|
probe,
|
|
start: {
|
|
ok: start.ok === true,
|
|
status: start.status ?? null,
|
|
jobId,
|
|
traceSampling: start.traceSampling ?? null,
|
|
reportPath: start.reportPath ?? null,
|
|
screenshotPath: start.screenshotPath ?? null,
|
|
},
|
|
statusResult: poll.result === null ? null : compactCommandResult(poll.result),
|
|
reportLoad: {
|
|
source: reportLoad.source,
|
|
path: reportLoad.path,
|
|
result: reportLoad.result === null ? null : compactCommandResult(reportLoad.result),
|
|
degradedReason: reportLoad.degradedReason,
|
|
},
|
|
valuesRedacted: true,
|
|
});
|
|
}
|
|
|
|
export function readNodeWebProbeReport(
|
|
options: NodeWebProbeRunOptions,
|
|
spec: HwlabRuntimeLaneSpec,
|
|
reportPath: string | null,
|
|
): { source: string; report: Record<string, unknown> | null; result: CommandResult | null; degradedReason: string | null; path: string | null } {
|
|
if (!reportPath) return { source: "missing", report: null, result: null, degradedReason: "web-probe-report-path-missing", path: null };
|
|
if (!isSafeWebProbeReportPath(reportPath)) return { source: "unsafe-path", report: null, result: null, degradedReason: "web-probe-report-path-invalid", path: reportPath };
|
|
const script = [
|
|
"set -eu",
|
|
`test -f ${shellQuote(reportPath)}`,
|
|
`node - ${shellQuote(reportPath)} <<'NODE'`,
|
|
"const fs = require('fs');",
|
|
"const reportPath = process.argv[2];",
|
|
"const report = JSON.parse(fs.readFileSync(reportPath, 'utf8'));",
|
|
"function rec(value) { return value && typeof value === 'object' && !Array.isArray(value) ? value : {}; }",
|
|
"function compact(value, depth = 0) {",
|
|
" if (value === null || value === undefined) return value ?? null;",
|
|
" if (typeof value === 'string') return value.replace(/\\s+/gu, ' ').trim().slice(0, 600);",
|
|
" if (typeof value === 'number' || typeof value === 'boolean') return value;",
|
|
" if (depth >= 6) return '[max-depth]';",
|
|
" if (Array.isArray(value)) return value.slice(0, 16).map((item) => compact(item, depth + 1));",
|
|
" if (typeof value === 'object') {",
|
|
" const out = {};",
|
|
" for (const [key, nested] of Object.entries(value).slice(0, 32)) out[key] = compact(nested, depth + 1);",
|
|
" return out;",
|
|
" }",
|
|
" return String(value).slice(0, 600);",
|
|
"}",
|
|
"const artifacts = rec(report.artifacts);",
|
|
"const compactReport = {",
|
|
" ok: report.ok === true,",
|
|
" status: typeof report.status === 'string' ? report.status : null,",
|
|
" finalUrl: typeof report.finalUrl === 'string' ? report.finalUrl : null,",
|
|
" error: typeof report.error === 'string' ? report.error : null,",
|
|
" degradedReason: typeof report.degradedReason === 'string' ? report.degradedReason : null,",
|
|
" actions: compact(report.actions),",
|
|
" session: compact(report.session),",
|
|
" trace: compact(report.trace),",
|
|
" promptValidation: compact(report.promptValidation),",
|
|
" performance: compact(report.performance),",
|
|
" traceSamples: compact(report.traceSamples),",
|
|
" dom: compact(report.dom),",
|
|
" failureDom: compact(report.failureDom),",
|
|
" artifacts: {",
|
|
" ...compact(artifacts),",
|
|
" reportPath: typeof artifacts.reportPath === 'string' ? artifacts.reportPath : reportPath,",
|
|
" },",
|
|
" safety: compact(report.safety),",
|
|
"};",
|
|
"console.log(JSON.stringify(compactReport));",
|
|
"NODE",
|
|
].join("\n");
|
|
const result = runTransWorkspaceStdinScript(options.node, spec.workspace, script, 55);
|
|
if (result.exitCode !== 0 || result.timedOut) return { source: "report-file", report: null, result, degradedReason: result.timedOut ? "web-probe-command-timeout" : "web-probe-report-read-failed", path: reportPath };
|
|
const report = parseJsonObject(result.stdout);
|
|
return {
|
|
source: "report-file",
|
|
report,
|
|
result,
|
|
degradedReason: report === null ? "web-probe-report-parse-failed" : null,
|
|
path: reportPath,
|
|
};
|
|
}
|
|
|
|
export function isSafeWebProbeReportPath(reportPath: string): boolean {
|
|
return reportPath.includes("/.state/web-live-dom-probe/") && reportPath.endsWith(".result.json") && !reportPath.includes("\0");
|
|
}
|
|
|
|
export function pollNodeWebProbeJob(options: NodeWebProbeRunOptions, spec: HwlabRuntimeLaneSpec, jobId: string): {
|
|
status: Record<string, unknown> | null;
|
|
result: CommandResult | null;
|
|
polls: number;
|
|
elapsedMs: number;
|
|
timedOut: boolean;
|
|
} {
|
|
const startedAt = Date.now();
|
|
const deadline = startedAt + options.commandTimeoutSeconds * 1000;
|
|
let lastStatus: Record<string, unknown> | null = null;
|
|
let lastResult: CommandResult | null = null;
|
|
let polls = 0;
|
|
while (Date.now() < deadline) {
|
|
polls += 1;
|
|
const script = `node scripts/web-live-dom-probe.mjs status ${shellQuote(jobId)}`;
|
|
const result = runTransWorkspaceStdinScript(options.node, spec.workspace, script, 55);
|
|
lastResult = result;
|
|
lastStatus = parseJsonObject(result.stdout);
|
|
const status = typeof lastStatus?.status === "string" ? lastStatus.status : null;
|
|
if (result.exitCode === 0 && status !== "running") return { status: lastStatus, result, polls, elapsedMs: Date.now() - startedAt, timedOut: false };
|
|
if (result.exitCode !== 0 && status !== "running") return { status: lastStatus, result, polls, elapsedMs: Date.now() - startedAt, timedOut: false };
|
|
sleepSync(Math.min(5000, Math.max(500, deadline - Date.now())));
|
|
}
|
|
return { status: lastStatus, result: lastResult, polls, elapsedMs: Date.now() - startedAt, timedOut: true };
|
|
}
|
|
|
|
export function webProbeCommandTimeoutSummary(options: NodeWebProbeRunOptions, timedOut: boolean): Record<string, unknown> {
|
|
return {
|
|
seconds: options.commandTimeoutSeconds,
|
|
autoSeconds: options.commandTimeoutAutoSeconds,
|
|
userProvided: options.commandTimeoutUserProvided,
|
|
transportMode: options.commandTimeoutSeconds > 55 ? "async-start-status" : "direct",
|
|
timedOut,
|
|
};
|
|
}
|
|
|
|
export function webProbeCredential(secretSpec: RuntimeSecretSpec, material: BootstrapAdminPasswordMaterial): Record<string, unknown> {
|
|
return {
|
|
username: secretSpec.bootstrapAdminUsername,
|
|
sourceRef: material.sourceRef,
|
|
sourceKey: material.sourceKey,
|
|
sourcePath: material.sourcePath === null ? null : displayRepoPath(material.sourcePath),
|
|
sourcePresent: material.sourcePresent,
|
|
sourceFingerprint: material.sourceFingerprint,
|
|
injectedVia: material.ok ? "stdin-env" : null,
|
|
valuesRedacted: true,
|
|
error: material.error,
|
|
};
|
|
}
|
|
|
|
export function nodeWebProbeAlertThresholds(spec: HwlabRuntimeLaneSpec): HwlabRuntimeWebProbeAlertThresholdsSpec {
|
|
const thresholds = spec.webProbe?.alertThresholds;
|
|
if (thresholds === undefined) {
|
|
throw new Error(`${hwlabRuntimeLaneConfigPath()} node=${spec.nodeId} lane=${spec.lane} requires webProbe.alertThresholds for web-probe observe`);
|
|
}
|
|
return thresholds;
|
|
}
|
|
|
|
export function nodeWebProbeProjectManagementConfig(spec: HwlabRuntimeLaneSpec): HwlabRuntimeWebProbeProjectManagementSpec | null {
|
|
return spec.webProbe?.projectManagement ?? null;
|
|
}
|
|
|
|
export interface NodeWebProbeHostProxyEnv {
|
|
readonly envAssignments: string[];
|
|
readonly summary: Record<string, unknown>;
|
|
}
|
|
|
|
export function nodeWebProbeHostProxyEnv(spec: HwlabRuntimeLaneSpec, browserProxyMode: WebProbeBrowserProxyMode = "auto"): NodeWebProbeHostProxyEnv {
|
|
if (browserProxyMode === "direct") {
|
|
return {
|
|
envAssignments: [],
|
|
summary: {
|
|
source: "option",
|
|
mode: "direct",
|
|
networkProfileId: spec.networkProfileId,
|
|
proxy: { enabled: false },
|
|
valuesPrinted: false,
|
|
},
|
|
};
|
|
}
|
|
const proxy = spec.networkProfile.proxy;
|
|
const serviceCache = new Map<string, string>();
|
|
const http = resolveNodeWebProbeHostProxyUrl(spec, proxy.http, serviceCache);
|
|
const https = resolveNodeWebProbeHostProxyUrl(spec, proxy.https, serviceCache);
|
|
const all = resolveNodeWebProbeHostProxyUrl(spec, proxy.all, serviceCache);
|
|
const noProxy = proxy.noProxy.join(",");
|
|
return {
|
|
envAssignments: [
|
|
["HTTP_PROXY", http.url],
|
|
["HTTPS_PROXY", https.url],
|
|
["ALL_PROXY", all.url],
|
|
["http_proxy", http.url],
|
|
["https_proxy", https.url],
|
|
["all_proxy", all.url],
|
|
["NO_PROXY", noProxy],
|
|
["no_proxy", noProxy],
|
|
].map(([key, value]) => `${key}=${shellQuote(value)}`),
|
|
summary: {
|
|
source: "yaml",
|
|
mode: "host-env",
|
|
networkProfileId: spec.networkProfileId,
|
|
proxy: {
|
|
http: http.summary,
|
|
https: https.summary,
|
|
all: all.summary,
|
|
noProxyCount: proxy.noProxy.length,
|
|
},
|
|
valuesPrinted: false,
|
|
},
|
|
};
|
|
}
|
|
|
|
export function resolveNodeWebProbeHostProxyUrl(
|
|
spec: HwlabRuntimeLaneSpec,
|
|
rawUrl: string,
|
|
serviceCache: Map<string, string>,
|
|
): { url: string; summary: Record<string, unknown> } {
|
|
let parsed: URL;
|
|
try {
|
|
parsed = new URL(rawUrl);
|
|
} catch (error) {
|
|
throw new Error(`config/hwlab-node-lanes.yaml networkProfiles.${spec.networkProfileId}.proxy contains invalid proxy URL: ${error instanceof Error ? error.message : String(error)}`);
|
|
}
|
|
const service = parseKubernetesServiceDnsHost(parsed.hostname);
|
|
if (service === null) {
|
|
return {
|
|
url: rawUrl,
|
|
summary: {
|
|
mode: "host-url",
|
|
host: parsed.hostname,
|
|
port: parsed.port || null,
|
|
valuesPrinted: false,
|
|
},
|
|
};
|
|
}
|
|
const clusterIp = resolveKubernetesServiceClusterIp(spec, service.namespace, service.name, serviceCache);
|
|
const originalHost = parsed.hostname;
|
|
parsed.hostname = clusterIp;
|
|
const resolvedUrl = normalizedProxyUrl(parsed);
|
|
return {
|
|
url: resolvedUrl,
|
|
summary: {
|
|
mode: "k8s-service-cluster-ip",
|
|
service: service.name,
|
|
namespace: service.namespace,
|
|
originalHost,
|
|
resolvedHost: clusterIp,
|
|
port: parsed.port || null,
|
|
valuesPrinted: false,
|
|
},
|
|
};
|
|
}
|
|
|
|
export function parseKubernetesServiceDnsHost(hostname: string): { name: string; namespace: string } | null {
|
|
const match = hostname.toLowerCase().match(/^([a-z0-9]([-a-z0-9]*[a-z0-9])?)\.([a-z0-9]([-a-z0-9]*[a-z0-9])?)\.svc(?:\.cluster\.local)?$/u);
|
|
if (match === null) return null;
|
|
return { name: match[1] ?? "", namespace: match[3] ?? "" };
|
|
}
|
|
|
|
export function resolveKubernetesServiceClusterIp(
|
|
spec: HwlabRuntimeLaneSpec,
|
|
namespace: string,
|
|
serviceName: string,
|
|
serviceCache: Map<string, string>,
|
|
): string {
|
|
const cacheKey = `${namespace}/${serviceName}`;
|
|
const cached = serviceCache.get(cacheKey);
|
|
if (cached !== undefined) return cached;
|
|
const result = runCommand([transPath(), spec.nodeKubeRoute, "get", "svc", "-n", namespace, serviceName, "-o", "jsonpath={.spec.clusterIP}"], repoRoot, { timeoutMs: 20_000 });
|
|
const clusterIp = result.stdout.trim();
|
|
if (result.exitCode !== 0 || clusterIp.length === 0) {
|
|
const reason = result.stderr.trim().slice(-500) || result.stdout.trim().slice(-500) || `exitCode=${result.exitCode}`;
|
|
throw new Error(`web-probe proxy service resolution failed for ${spec.nodeId}/${spec.lane} ${namespace}/${serviceName}: ${reason}`);
|
|
}
|
|
serviceCache.set(cacheKey, clusterIp);
|
|
return clusterIp;
|
|
}
|
|
|
|
export function normalizedProxyUrl(parsed: URL): string {
|
|
const value = parsed.toString();
|
|
if (parsed.pathname === "/" && parsed.search === "" && parsed.hash === "") return value.replace(/\/$/u, "");
|
|
return value;
|
|
}
|
|
|
|
function webProbeAccountEnvAssignments(): string[] {
|
|
return Object.entries(process.env)
|
|
.filter(([key, value]) => value !== undefined && /^HWLAB_WEB_(?:ACCOUNT_)?[A-Z0-9_]+_(?:JSON|USER|PASS)$/u.test(key))
|
|
.map(([key, value]) => `${key}=${shellQuote(value ?? "")}`);
|
|
}
|
|
|
|
export function runNodeWebProbeObserve(
|
|
options: NodeWebProbeObserveOptions,
|
|
spec: HwlabRuntimeLaneSpec,
|
|
secretSpec: RuntimeSecretSpec | null,
|
|
material: BootstrapAdminPasswordMaterial | null,
|
|
credential: Record<string, unknown> | null,
|
|
): Record<string, unknown> {
|
|
if (options.observeAction === "start") {
|
|
if (secretSpec === null || material === null || credential === null || material.password === null) throw new Error("web-probe observe start requires bootstrap admin credential material");
|
|
return runNodeWebProbeObserveStart(options, spec, secretSpec, material, credential);
|
|
}
|
|
if (options.observeAction === "status") return runNodeWebProbeObserveStatus(options, spec);
|
|
if (options.observeAction === "command") return runNodeWebProbeObserveCommand(options, spec, false);
|
|
if (options.observeAction === "stop") return runNodeWebProbeObserveCommand({ ...options, commandType: "stop" }, spec, true);
|
|
if (options.observeAction === "collect") return runNodeWebProbeObserveCollect(options, spec);
|
|
return runNodeWebProbeObserveAnalyze(options, spec);
|
|
}
|
|
|
|
export function runNodeWebProbeObserveStart(
|
|
options: NodeWebProbeObserveOptions,
|
|
spec: HwlabRuntimeLaneSpec,
|
|
secretSpec: RuntimeSecretSpec,
|
|
material: BootstrapAdminPasswordMaterial,
|
|
credential: Record<string, unknown>,
|
|
): Record<string, unknown> | RenderedCliResult {
|
|
const jobId = `webobs-${Date.now().toString(36)}-${randomBytes(3).toString("hex")}`;
|
|
const timestamp = new Date().toISOString().replace(/[-:]/gu, "").replace(/[.]\d{3}Z$/u, "Z");
|
|
const day = timestamp.slice(0, 8);
|
|
const defaultStateDir = `.state/web-observe/${safeWebObserveSegment(options.node)}/${safeWebObserveSegment(options.lane)}/${day.slice(0, 4)}/${day.slice(4, 6)}/${day.slice(6, 8)}/${timestamp}_${safeWebObserveTargetSegment(options.targetPath)}_${jobId}`;
|
|
const stateDir = options.stateDir ?? defaultStateDir;
|
|
const runnerB64 = Buffer.from(nodeWebObserveRunnerSource(), "utf8").toString("base64");
|
|
const runnerB64Body = runnerB64.match(/.{1,76}/gu)?.join("\n") ?? runnerB64;
|
|
const webProbeProxy = nodeWebProbeHostProxyEnv(spec, options.browserProxyMode);
|
|
const alertThresholds = nodeWebProbeAlertThresholds(spec);
|
|
const projectManagement = nodeWebProbeProjectManagementConfig(spec);
|
|
const runnerEnvAssignments = [
|
|
...webProbeProxy.envAssignments,
|
|
...webProbeAccountEnvAssignments(),
|
|
`HWLAB_WEB_BASE_URL=${shellQuote(options.url)}`,
|
|
`HWLAB_WEB_USER=${shellQuote(secretSpec.bootstrapAdminUsername)}`,
|
|
`HWLAB_WEB_PASS=${shellQuote(material.password)}`,
|
|
`UNIDESK_WEB_OBSERVE_STATE_DIR=${shellQuote(stateDir)}`,
|
|
`UNIDESK_WEB_OBSERVE_JOB_ID=${shellQuote(jobId)}`,
|
|
`UNIDESK_WEB_OBSERVE_TARGET_PATH=${shellQuote(options.targetPath)}`,
|
|
`UNIDESK_WEB_OBSERVE_SAMPLE_INTERVAL_MS=${shellQuote(String(options.sampleIntervalMs))}`,
|
|
`UNIDESK_WEB_OBSERVE_SCREENSHOT_INTERVAL_MS=${shellQuote(String(options.screenshotIntervalMs))}`,
|
|
`UNIDESK_WEB_OBSERVE_OBSERVER_REFRESH_INTERVAL_MS=${shellQuote(String(options.observerRefreshIntervalMs))}`,
|
|
`UNIDESK_WEB_OBSERVE_MAX_SAMPLES=${shellQuote(String(options.maxSamples))}`,
|
|
`UNIDESK_WEB_OBSERVE_VIEWPORT=${shellQuote(options.viewport)}`,
|
|
`UNIDESK_WEB_OBSERVE_BROWSER_PROXY_MODE=${shellQuote(options.browserProxyMode)}`,
|
|
`UNIDESK_WEB_OBSERVE_ALERT_THRESHOLDS_JSON=${shellQuote(JSON.stringify(alertThresholds))}`,
|
|
`UNIDESK_WEB_OBSERVE_PROJECT_MANAGEMENT_JSON=${shellQuote(JSON.stringify(projectManagement))}`,
|
|
].join(" ");
|
|
const script = [
|
|
"set -eu",
|
|
`state_dir=${shellQuote(stateDir)}`,
|
|
"mkdir -p \"$state_dir\"",
|
|
"chmod 700 \"$state_dir\"",
|
|
"runner=\"$state_dir/observer-runner.mjs\"",
|
|
"runner_b64=\"$state_dir/observer-runner.mjs.b64\"",
|
|
"cat >\"$runner_b64\" <<'UNIDESK_WEB_OBSERVE_RUNNER_B64'",
|
|
runnerB64Body,
|
|
"UNIDESK_WEB_OBSERVE_RUNNER_B64",
|
|
"node -e \"const fs=require('fs'); fs.writeFileSync(process.argv[1], Buffer.from(fs.readFileSync(process.argv[2], 'utf8').replace(/\\s+/g, ''), 'base64'))\" \"$runner\" \"$runner_b64\"",
|
|
"rm -f \"$runner_b64\"",
|
|
"chmod 700 \"$runner\"",
|
|
`if command -v setsid >/dev/null 2>&1; then setsid env ${runnerEnvAssignments} node "$runner" >"$state_dir/stdout.log" 2>"$state_dir/stderr.log" </dev/null & else nohup env ${runnerEnvAssignments} node "$runner" >"$state_dir/stdout.log" 2>"$state_dir/stderr.log" </dev/null & fi`,
|
|
"pid=$!",
|
|
"printf '%s\\n' \"$pid\" >\"$state_dir/pid\"",
|
|
"sleep 1",
|
|
`node -e ${shellQuote("const fs=require('fs'); const dir=process.argv[1]; const read=(n)=>{try{return JSON.parse(fs.readFileSync(dir+'/'+n,'utf8'))}catch{return null}}; const pid=fs.existsSync(dir+'/pid')?fs.readFileSync(dir+'/pid','utf8').trim():null; console.log(JSON.stringify({ok:true,command:'web-probe-observe start',jobId:process.argv[2],stateDir:dir,pid:Number(pid)||null,manifestPath:dir+'/manifest.json',heartbeat:read('heartbeat.json'),manifest:read('manifest.json'),statusCommand:'bun scripts/cli.ts web-probe observe status --node '+process.argv[3]+' --lane '+process.argv[4]+' --state-dir '+dir,stopCommand:'bun scripts/cli.ts web-probe observe stop --node '+process.argv[3]+' --lane '+process.argv[4]+' --state-dir '+dir,valuesRedacted:true},null,2))")} "$state_dir" ${shellQuote(jobId)} ${shellQuote(options.node)} ${shellQuote(options.lane)}`,
|
|
].join("\n");
|
|
const result = runTransWorkspaceStdinScript(options.node, spec.workspace, script, options.commandTimeoutSeconds);
|
|
const started = parseJsonObject(result.stdout);
|
|
const observerId = typeof started?.jobId === "string" ? started.jobId : jobId;
|
|
const index = result.exitCode === 0 && started?.ok === true
|
|
? upsertWebObserveIndexEntry({
|
|
id: observerId,
|
|
node: options.node,
|
|
lane: options.lane,
|
|
workspace: spec.workspace,
|
|
stateDir,
|
|
url: options.url,
|
|
targetPath: options.targetPath,
|
|
status: "running",
|
|
pid: typeof started.pid === "number" ? started.pid : null,
|
|
startedAt: new Date().toISOString(),
|
|
updatedAt: new Date().toISOString(),
|
|
})
|
|
: null;
|
|
return renderWebObserveStartResult({
|
|
ok: result.exitCode === 0 && started?.ok === true,
|
|
status: result.exitCode === 0 && started?.ok === true ? "started" : "blocked",
|
|
command: `web-probe observe start --node ${options.node} --lane ${options.lane}`,
|
|
node: options.node,
|
|
lane: options.lane,
|
|
workspace: spec.workspace,
|
|
url: options.url,
|
|
network: webProbeProxy.summary,
|
|
alertThresholds,
|
|
projectManagement,
|
|
targetPath: options.targetPath,
|
|
id: observerId,
|
|
credential,
|
|
observer: withWebObserveShortcuts(started, observerId),
|
|
wrapper: buildWebObserveWrapperForObserveOptions("start", options, spec.workspace, { id: observerId, jobId: observerId, stateDir }),
|
|
index,
|
|
next: webObserveNextCommands(observerId),
|
|
result: compactCommandResultRedacted(result, [material.password ?? ""]),
|
|
valuesRedacted: true,
|
|
});
|
|
}
|
|
|
|
export function runNodeWebProbeObserveStatus(options: NodeWebProbeObserveOptions, spec: HwlabRuntimeLaneSpec): Record<string, unknown> | RenderedCliResult {
|
|
const { result, status } = readNodeWebProbeObserveRemoteStatus(options, spec, options.tailLines, options.commandTimeoutSeconds);
|
|
const observerId = webObserveIdFromStatus(status, options);
|
|
const statusReadable = status !== null;
|
|
const ok = result.exitCode === 0 && statusReadable && status.ok !== false;
|
|
const degradedReason = result.timedOut
|
|
? "web-probe-command-timeout"
|
|
: result.exitCode !== 0
|
|
? "web-probe-observe-status-failed"
|
|
: !statusReadable
|
|
? "web-probe-observe-status-unreadable"
|
|
: typeof status.degradedReason === "string"
|
|
? status.degradedReason
|
|
: null;
|
|
const index = ok && observerId !== null && options.stateDir !== null
|
|
? upsertWebObserveIndexEntry(webObserveIndexEntryFromOptions(options, spec, observerId, status))
|
|
: null;
|
|
return withWebObserveStatusRendered({
|
|
ok,
|
|
status: ok ? "observed" : "blocked",
|
|
command: webObserveCommandLabel("status", options),
|
|
id: observerId,
|
|
node: options.node,
|
|
lane: options.lane,
|
|
workspace: spec.workspace,
|
|
degradedReason,
|
|
observer: withWebObserveShortcuts(status, observerId),
|
|
wrapper: buildWebObserveWrapperForObserveOptions("status", options, spec.workspace, { id: observerId, stateDir: webObserveWrapperStateDirFromStatus(status, options.stateDir) }),
|
|
index,
|
|
next: observerId === null ? null : webObserveNextCommands(observerId),
|
|
result: compactCommandResultWithStdoutTail(result),
|
|
valuesRedacted: true,
|
|
});
|
|
}
|
|
|
|
export function readNodeWebProbeObserveRemoteStatus(
|
|
options: NodeWebProbeObserveOptions,
|
|
spec: HwlabRuntimeLaneSpec,
|
|
tailLines: number,
|
|
timeoutSeconds: number,
|
|
): { result: ReturnType<typeof runTransWorkspaceStdinScript>; status: Record<string, unknown> | null } {
|
|
const script = [
|
|
"set -eu",
|
|
nodeWebObserveResolveStateDirShell(options),
|
|
nodeWebObserveStatusNodeScript(tailLines, options.node, options.lane),
|
|
].join("\n");
|
|
const result = runTransWorkspaceStdinScript(options.node, spec.workspace, script, timeoutSeconds);
|
|
return { result, status: parseJsonObject(result.stdout) };
|
|
}
|
|
|
|
export function webObserveText(value: unknown): string {
|
|
if (value === null || value === undefined || value === "") return "-";
|
|
if (typeof value === "string") return value;
|
|
if (typeof value === "number" || typeof value === "boolean") return String(value);
|
|
return JSON.stringify(value);
|
|
}
|
|
|
|
export function webObserveShort(value: string, maxLength: number): string {
|
|
if (value.length <= maxLength) return value;
|
|
if (maxLength <= 1) return value.slice(0, maxLength);
|
|
return `${value.slice(0, maxLength - 1)}~`;
|
|
}
|
|
|
|
export function runNodeWebProbeObserveCommand(options: NodeWebProbeObserveOptions, spec: HwlabRuntimeLaneSpec, stopCommand: boolean): Record<string, unknown> | RenderedCliResult {
|
|
const type = options.commandType ?? (stopCommand ? "stop" : null);
|
|
if (type === null) throw new Error("web-probe observe command requires --type");
|
|
const commandId = `cmd-${Date.now().toString(36)}-${randomBytes(3).toString("hex")}`;
|
|
const payload = {
|
|
id: commandId,
|
|
type,
|
|
createdAt: new Date().toISOString(),
|
|
source: "cli",
|
|
path: options.commandPath,
|
|
text: options.commandText,
|
|
label: options.commandLabel,
|
|
sessionId: options.commandSessionId,
|
|
provider: options.commandProvider,
|
|
afterRound: options.commandAfterRound,
|
|
severity: options.commandSeverity,
|
|
alternateSessionStrategy: options.commandAlternateSessionStrategy,
|
|
expectedSentinelRange: options.commandExpectedSentinelRange,
|
|
requireComposerReady: options.commandRequireComposerReady,
|
|
findingId: options.commandFindingId,
|
|
blocking: options.commandBlocking,
|
|
accountId: options.commandAccountId,
|
|
fromAccountId: options.commandFromAccountId,
|
|
toAccountId: options.commandToAccountId,
|
|
sourceId: options.commandSourceId,
|
|
fileRef: options.commandFileRef,
|
|
filename: options.commandFilename,
|
|
taskRef: options.commandTaskRef,
|
|
taskId: options.commandTaskId,
|
|
field: options.commandField,
|
|
link: options.commandLink,
|
|
title: options.commandTitle,
|
|
body: options.commandBody,
|
|
status: options.commandStatus,
|
|
hwpodId: options.commandHwpodId,
|
|
nodeId: options.commandNodeId,
|
|
workspaceRoot: options.commandWorkspaceRoot,
|
|
root: options.commandRoot,
|
|
};
|
|
const preStopStatus = options.force && stopCommand
|
|
? readNodeWebProbeObserveRemoteStatus(options, spec, 1, Math.min(options.commandTimeoutSeconds, 30))
|
|
: null;
|
|
const preStopDiagnostics = record(preStopStatus?.status?.diagnostics);
|
|
const preStopCommands = record(preStopStatus?.status?.commands);
|
|
const preStopPending = Number(preStopCommands?.pendingCount ?? 0);
|
|
const preStopProcessing = Number(preStopCommands?.processingCount ?? 0);
|
|
const forceBeforeQueueReason = options.force && stopCommand
|
|
? preStopDiagnostics?.heartbeatStale === true
|
|
? "heartbeat-stale"
|
|
: preStopPending > 0 || preStopProcessing > 0
|
|
? "command-backlog"
|
|
: null
|
|
: null;
|
|
if (forceBeforeQueueReason !== null) {
|
|
return runNodeWebProbeObserveForceStop(options, spec, payload, commandId, forceBeforeQueueReason, preStopStatus?.result ?? null, preStopStatus?.status ?? null, null);
|
|
}
|
|
const payloadB64 = Buffer.from(JSON.stringify(payload), "utf8").toString("base64");
|
|
const waitMs = options.force && stopCommand ? Math.max(options.waitMs, 5000) : options.waitMs;
|
|
const script = [
|
|
"set -eu",
|
|
nodeWebObserveResolveStateDirShell(options),
|
|
"mkdir -p \"$state_dir/commands/pending\"",
|
|
`node -e "const fs=require('fs'),path=require('path'); const dir=process.argv[1], id=process.argv[2], payload=Buffer.from(process.argv[3], 'base64').toString('utf8'); fs.writeFileSync(path.join(dir,'commands','pending',id+'.json'), payload+'\\n', {mode:0o600});" "$state_dir" ${shellQuote(commandId)} ${shellQuote(payloadB64)}`,
|
|
nodeWebObserveWaitCommandShell(commandId, waitMs),
|
|
].join("\n");
|
|
const result = runTransWorkspaceStdinScript(options.node, spec.workspace, script, options.commandTimeoutSeconds);
|
|
const commandResult = parseJsonObject(result.stdout);
|
|
if (options.force && stopCommand && (result.exitCode !== 0 || commandResult?.waitTimedOut === true || commandResult?.queued === true)) {
|
|
const reason = result.exitCode !== 0
|
|
? "graceful-stop-failed"
|
|
: commandResult?.waitTimedOut === true
|
|
? "graceful-stop-not-consumed"
|
|
: "graceful-stop-queued";
|
|
return runNodeWebProbeObserveForceStop(options, spec, payload, commandId, reason, preStopStatus?.result ?? null, preStopStatus?.status ?? null, result);
|
|
}
|
|
return withWebObserveCommandRendered({
|
|
ok: result.exitCode === 0 && commandResult?.ok !== false,
|
|
status: result.exitCode === 0 ? (waitMs > 0 ? "completed-or-queued" : "queued") : "blocked",
|
|
command: webObserveCommandLabel(stopCommand ? "stop" : "command", options),
|
|
id: webObserveIdFromOptions(options),
|
|
node: options.node,
|
|
lane: options.lane,
|
|
workspace: spec.workspace,
|
|
commandId,
|
|
observerCommand: commandSummaryForOutput(payload),
|
|
observer: commandResult,
|
|
wrapper: buildWebObserveWrapperForObserveOptions(stopCommand ? "stop" : "command", options, spec.workspace, { commandType: type }),
|
|
result: compactCommandResult(result),
|
|
full: options.full,
|
|
valuesRedacted: true,
|
|
});
|
|
}
|
|
|
|
export function runNodeWebProbeObserveForceStop(
|
|
options: NodeWebProbeObserveOptions,
|
|
spec: HwlabRuntimeLaneSpec,
|
|
payload: Record<string, unknown>,
|
|
commandId: string,
|
|
reason: string,
|
|
preflightResult: ReturnType<typeof runTransWorkspaceStdinScript> | null,
|
|
preflightStatus: Record<string, unknown> | null,
|
|
gracefulResult: ReturnType<typeof runTransWorkspaceStdinScript> | null,
|
|
): Record<string, unknown> | RenderedCliResult {
|
|
const killResult = runTransWorkspaceStdinScript(options.node, spec.workspace, [
|
|
"set -eu",
|
|
nodeWebObserveResolveStateDirShell(options),
|
|
nodeWebObserveForceStopNodeScript(reason, commandId),
|
|
].join("\n"), 55);
|
|
const forcePayload = parseJsonObject(killResult.stdout);
|
|
return withWebObserveCommandRendered({
|
|
ok: killResult.exitCode === 0 && forcePayload?.ok !== false,
|
|
status: killResult.exitCode === 0 && forcePayload?.ok !== false ? "forced-stopped" : "blocked",
|
|
command: webObserveCommandLabel("stop", options),
|
|
id: webObserveIdFromOptions(options),
|
|
node: options.node,
|
|
lane: options.lane,
|
|
workspace: spec.workspace,
|
|
commandId,
|
|
observerCommand: commandSummaryForOutput(payload),
|
|
observer: forcePayload,
|
|
wrapper: buildWebObserveWrapperForObserveOptions("stop", options, spec.workspace, { commandType: "stop" }),
|
|
forceReason: reason,
|
|
preflightObserver: preflightStatus,
|
|
preflightResult: preflightResult === null ? null : compactCommandResult(preflightResult),
|
|
gracefulResult: gracefulResult === null ? null : compactCommandResult(gracefulResult),
|
|
forceResult: compactCommandResultWithStdoutTail(killResult),
|
|
full: options.full,
|
|
valuesRedacted: true,
|
|
});
|
|
}
|
|
|
|
export function runNodeWebProbeObserveCollect(options: NodeWebProbeObserveOptions, spec: HwlabRuntimeLaneSpec): Record<string, unknown> | RenderedCliResult {
|
|
const collectScript = options.collectView === "files"
|
|
? nodeWebObserveCollectNodeScript(options.maxFiles, options.collectFile, options.collectFinding, options.collectGrep)
|
|
: nodeWebObserveCollectViewNodeScript({
|
|
maxFiles: options.maxFiles,
|
|
view: options.collectView,
|
|
traceId: options.collectTraceId,
|
|
sampleSeq: options.collectSampleSeq,
|
|
timestamp: options.collectTimestamp,
|
|
turn: options.collectTurn,
|
|
commandId: options.collectCommandId,
|
|
windowMs: options.collectWindowMs,
|
|
});
|
|
const script = [
|
|
"set -eu",
|
|
nodeWebObserveResolveStateDirShell(options),
|
|
collectScript,
|
|
].join("\n");
|
|
const result = runTransWorkspaceStdinScript(options.node, spec.workspace, script, options.commandTimeoutSeconds);
|
|
const collect = parseJsonObject(result.stdout);
|
|
const compactRaw = options.raw && options.compactRaw;
|
|
const payload = {
|
|
ok: result.exitCode === 0 && collect !== null && collect.ok !== false,
|
|
status: result.exitCode === 0 && collect !== null ? "collected" : "blocked",
|
|
command: webObserveCommandLabel("collect", options),
|
|
id: webObserveIdFromOptions(options),
|
|
node: options.node,
|
|
lane: options.lane,
|
|
workspace: spec.workspace,
|
|
view: options.collectView,
|
|
requestedFile: options.collectFile,
|
|
requestedGrep: options.collectGrep,
|
|
requestedCommandId: options.collectCommandId,
|
|
requestedWindowMs: options.collectWindowMs,
|
|
degradedReason: collect === null ? "collect-json-parse-failed" : null,
|
|
collect: compactRaw ? compactObserveCollectForRaw(collect) : collect,
|
|
wrapper: compactRaw
|
|
? { mode: "wrapper-only", action: "collect", node: options.node, lane: options.lane, id: webObserveIdFromOptions(options), stateDir: options.stateDir, valuesRedacted: true }
|
|
: buildWebObserveWrapperForObserveOptions("collect", options, spec.workspace),
|
|
result: compactRaw ? { exitCode: result.exitCode, timedOut: result.timedOut, stdoutBytes: Buffer.byteLength(result.stdout), stderrBytes: Buffer.byteLength(result.stderr) } : collect === null ? compactCommandResultWithStdoutTail(result) : compactCommandResult(result),
|
|
valuesRedacted: true,
|
|
};
|
|
return options.raw ? payload : withWebObserveCollectRendered(payload);
|
|
}
|
|
|
|
function compactObserveCollectForRaw(collect: Record<string, unknown> | null): Record<string, unknown> | null {
|
|
if (collect === null) return null;
|
|
const rows = Array.isArray(collect.rows) ? collect.rows.map((item) => {
|
|
const row = observeRecord(item);
|
|
const finalResponse = observeRecord(row.finalResponse);
|
|
return {
|
|
round: row.round ?? null,
|
|
commandId: row.commandId ?? null,
|
|
userHash: row.userHash ?? null,
|
|
userBytes: row.userBytes ?? null,
|
|
traceId: row.traceId ?? null,
|
|
status: row.status ?? null,
|
|
elapsedSeconds: row.elapsedSeconds ?? null,
|
|
recentUpdateSeconds: row.recentUpdateSeconds ?? null,
|
|
marks: row.marks ?? null,
|
|
firstSeq: row.firstSeq ?? null,
|
|
lastSeq: row.lastSeq ?? null,
|
|
lastTs: row.lastTs ?? null,
|
|
finalResponse: {
|
|
preview: finalResponse.preview ?? null,
|
|
textHash: finalResponse.textHash ?? null,
|
|
textBytes: finalResponse.textBytes ?? null,
|
|
empty: finalResponse.empty === true,
|
|
},
|
|
valuesRedacted: true,
|
|
};
|
|
}) : undefined;
|
|
const timelineRows = Array.isArray(collect.timelineRows) ? collect.timelineRows.slice(0, 12).map((item) => {
|
|
const row = observeRecord(item);
|
|
return {
|
|
ts: row.ts ?? null,
|
|
seq: row.seq ?? null,
|
|
kind: row.kind ?? null,
|
|
phase: row.phase ?? null,
|
|
type: row.type ?? null,
|
|
commandId: row.commandId ?? null,
|
|
sessionId: row.sessionId ?? null,
|
|
traceId: row.traceId ?? null,
|
|
summary: row.summary ?? null,
|
|
valuesRedacted: true,
|
|
};
|
|
}) : undefined;
|
|
return {
|
|
ok: collect.ok !== false,
|
|
command: collect.command,
|
|
view: collect.view,
|
|
stateDir: collect.stateDir,
|
|
turnCount: collect.turnCount,
|
|
anchor: observeRecord(collect.anchor),
|
|
window: observeRecord(collect.window),
|
|
counts: observeRecord(collect.counts),
|
|
...(rows === undefined ? {} : { rows }),
|
|
...(timelineRows === undefined ? {} : { timelineRows }),
|
|
renderedText: collect.view === "timeline" ? undefined : typeof collect.renderedText === "string" ? collect.renderedText : undefined,
|
|
sourceFiles: Array.isArray(collect.sourceFiles) ? collect.sourceFiles : undefined,
|
|
blocker: collect.blocker,
|
|
sampleSeq: collect.sampleSeq,
|
|
traceId: collect.traceId,
|
|
finalResponse: collect.finalResponse,
|
|
valuesRedacted: true,
|
|
};
|
|
}
|
|
|
|
function observeRecord(value: unknown): Record<string, unknown> {
|
|
return typeof value === "object" && value !== null && !Array.isArray(value) ? value as Record<string, unknown> : {};
|
|
}
|
|
|
|
export function runNodeWebProbeObserveAnalyze(options: NodeWebProbeObserveOptions, spec: HwlabRuntimeLaneSpec): Record<string, unknown> | RenderedCliResult {
|
|
const analyzerB64 = Buffer.from(nodeWebObserveAnalyzerSource(), "utf8").toString("base64");
|
|
const alertThresholds = nodeWebProbeAlertThresholds(spec);
|
|
const projectManagement = nodeWebProbeProjectManagementConfig(spec);
|
|
const script = [
|
|
"set -eu",
|
|
nodeWebObserveResolveStateDirShell(options),
|
|
"analyzer=\"$state_dir/observer-analyzer.mjs\"",
|
|
"analyzer_b64=\"$state_dir/observer-analyzer.mjs.b64\"",
|
|
"cat >\"$analyzer_b64\" <<'UNIDESK_WEB_OBSERVE_ANALYZER_B64'",
|
|
analyzerB64,
|
|
"UNIDESK_WEB_OBSERVE_ANALYZER_B64",
|
|
"node -e \"const fs=require('fs'); fs.writeFileSync(process.argv[1], Buffer.from(fs.readFileSync(process.argv[2], 'utf8').replace(/\\s+/g, ''), 'base64'))\" \"$analyzer\" \"$analyzer_b64\"",
|
|
"rm -f \"$analyzer_b64\"",
|
|
"chmod 700 \"$analyzer\"",
|
|
"mkdir -p \"$state_dir/analysis\"",
|
|
"analysis_stdout=\"$state_dir/analysis/analyzer-stdout.json\"",
|
|
"analysis_stderr=\"$state_dir/analysis/analyzer-stderr.log\"",
|
|
"set +e",
|
|
`UNIDESK_WEB_OBSERVE_ANALYZE_ARCHIVE_PREFIX=${shellQuote(options.analyzeArchivePrefix ?? "")}`,
|
|
`UNIDESK_WEB_OBSERVE_ANALYZE_TAIL_SAMPLES=${shellQuote(options.analyzeTailSamples === null ? "" : String(options.analyzeTailSamples))}`,
|
|
`UNIDESK_WEB_OBSERVE_ALERT_THRESHOLDS_JSON=${shellQuote(JSON.stringify(alertThresholds))}`,
|
|
`UNIDESK_WEB_OBSERVE_PROJECT_MANAGEMENT_JSON=${shellQuote(JSON.stringify(projectManagement))}`,
|
|
"UNIDESK_WEB_OBSERVE_STATE_DIR=\"$state_dir\" UNIDESK_WEB_OBSERVE_ANALYZE_ARCHIVE_PREFIX=\"$UNIDESK_WEB_OBSERVE_ANALYZE_ARCHIVE_PREFIX\" UNIDESK_WEB_OBSERVE_ANALYZE_TAIL_SAMPLES=\"$UNIDESK_WEB_OBSERVE_ANALYZE_TAIL_SAMPLES\" UNIDESK_WEB_OBSERVE_ALERT_THRESHOLDS_JSON=\"$UNIDESK_WEB_OBSERVE_ALERT_THRESHOLDS_JSON\" UNIDESK_WEB_OBSERVE_PROJECT_MANAGEMENT_JSON=\"$UNIDESK_WEB_OBSERVE_PROJECT_MANAGEMENT_JSON\" node \"$analyzer\" >\"$analysis_stdout\" 2>\"$analysis_stderr\"",
|
|
"analyzer_exit=$?",
|
|
"set -e",
|
|
"report_json=\"$state_dir/analysis/report.json\"",
|
|
"report_md=\"$state_dir/analysis/report.md\"",
|
|
"node - \"$analysis_stdout\" \"$analysis_stderr\" \"$report_json\" \"$report_md\" \"$analyzer_exit\" <<'UNIDESK_WEB_OBSERVE_ANALYZE_COMPACT'",
|
|
"const fs = require('fs');",
|
|
"const crypto = require('crypto');",
|
|
"const [stdoutPath, stderrPath, reportJsonPath, reportMdPath, analyzerExitRaw] = process.argv.slice(2);",
|
|
"const analyzerExit = Number(analyzerExitRaw);",
|
|
"const readText = (path) => { try { return fs.readFileSync(path, 'utf8'); } catch { return ''; } };",
|
|
"const readJson = (path) => { const text = readText(path); if (!text.trim()) return null; try { return JSON.parse(text); } catch { return null; } };",
|
|
"const sha256 = (path) => { const text = readText(path); return text ? 'sha256:' + crypto.createHash('sha256').update(text).digest('hex') : null; };",
|
|
"const statSize = (path) => { try { return fs.statSync(path).size; } catch { return 0; } };",
|
|
"const tail = (text, limit = 1200) => String(text || '').slice(-limit);",
|
|
"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.startsWith('project-management-') || id.startsWith('mdtodo-') || id === 'workbench-launch-button-unavailable') return 0; if (id === 'observer-command-failed') return 0.2; if (id === 'page-performance-slow-same-origin-api') return 1; if (id === 'session-rail-title-fallback-majority') return 2; if (id.startsWith('turn-timing-total-elapsed')) return 3; if (id.startsWith('turn-timing-recent-update')) return 4; if (id.includes('runtime-execution') || id.includes('prompt-chat-submit-failed')) return 5; return 10; };",
|
|
"const severityRank = (item) => { const severity = String(item?.severity ?? item?.level ?? '').toLowerCase(); if (severity === 'red') return 0; if (severity === 'amber' || severity === 'warning') return 1; if (severity === 'info') return 3; return 2; };",
|
|
"const sortFindings = (items) => (Array.isArray(items) ? items : []).slice().sort((a, b) => findingRank(a) - findingRank(b) || severityRank(a) - severityRank(b));",
|
|
"const stdoutJson = readJson(stdoutPath);",
|
|
"const reportJson = readJson(reportJsonPath);",
|
|
"const source = (stdoutJson && stdoutJson.ok !== false ? stdoutJson : null) || reportJson || stdoutJson || null;",
|
|
"const fullSource = reportJson || source;",
|
|
"const objectOrNull = (value) => value && typeof value === 'object' && !Array.isArray(value) ? value : null;",
|
|
"const slimRound = (item) => { const v = objectOrNull(item) || {}; return { promptIndex: v.promptIndex ?? null, sampleCount: v.sampleCount ?? null, loadingSamples: v.loadingSamples ?? null, maxLoadingCount: v.maxLoadingCount ?? null, loadingOwnerCount: v.loadingOwnerCount ?? null, lastTotalElapsedSeconds: v.lastTotalElapsedSeconds ?? null, lastRecentUpdateSeconds: v.lastRecentUpdateSeconds ?? null, diagnosticSamples: v.diagnosticSamples ?? null, terminalSamples: v.terminalSamples ?? null, turnTimingTotalElapsedForwardJumpCount: v.turnTimingTotalElapsedForwardJumpCount ?? null, turnTimingTotalElapsedForwardJumpMaxSeconds: v.turnTimingTotalElapsedForwardJumpMaxSeconds ?? null, promptTextHash: clip(v.promptTextHash, 80) }; };",
|
|
"const slimTurnColumn = (item) => { const v = objectOrNull(item) || {}; return { label: clip(v.label, 24), pageRole: clip(v.pageRole, 24), pageId: clip(v.pageId, 32), pageEpoch: v.pageEpoch ?? null, promptIndex: v.promptIndex ?? null, lastPromptIndex: v.lastPromptIndex ?? null, traceId: clip(v.traceId, 48), firstSeq: v.firstSeq ?? null, lastSeq: v.lastSeq ?? null, source: clip(v.source, 48) }; };",
|
|
"const slimSlowSample = (item) => { const v = objectOrNull(item) || {}; return { ts: v.ts ?? null, seq: v.seq ?? null, path: clip(v.path ?? v.rawPath, 96), initiatorType: clip(v.initiatorType, 24), durationMs: v.durationMs ?? null, requestToResponseStartMs: v.requestToResponseStartMs ?? v.streamOpenMs ?? null, responseTransferMs: v.responseTransferMs ?? null, nextHopProtocol: clip(v.nextHopProtocol, 24), timingStatus: clip(v.timingStatus, 16), serverTimingNames: Array.isArray(v.serverTimingNames) ? v.serverTimingNames.slice(0, 4).map((x) => clip(x, 32)) : [], otelTraceId: clip(v.otelTraceId, 32) }; };",
|
|
"const slimSlowApi = (item) => { const v = objectOrNull(item) || {}; return { path: clip(v.path ?? v.route, 96), route: clip(v.route ?? v.path, 96), sampleCount: v.sampleCount ?? null, p95Ms: v.p95Ms ?? v.p95 ?? null, maxMs: v.maxMs ?? v.max ?? null, budgetMs: v.budgetMs ?? null, overBudgetCount: v.overBudgetCount ?? null, overFiveSecondCount: v.overFiveSecondCount ?? null, slowSamples: Array.isArray(v.slowSamples) ? v.slowSamples.slice(0, 3).map(slimSlowSample) : [] }; };",
|
|
"const slimFinding = (item) => { const v = objectOrNull(item) || {}; return { kind: clip(v.kind ?? v.id ?? v.code, 48), code: clip(v.code ?? v.id ?? v.kind, 48), severity: clip(v.severity ?? v.level, 24), level: clip(v.level ?? v.severity, 24), count: v.count ?? v.sampleCount ?? null, sampleCount: v.sampleCount ?? v.count ?? null, summary: clip(v.summary ?? v.message, 180), message: clip(v.message ?? v.summary, 180) }; };",
|
|
"const slimProjectManagement = (value) => { const v = objectOrNull(value); if (!v) return null; const s = objectOrNull(v.summary) || v; return { summary: { enabled: s.enabled === true, projectSampleCount: s.projectSampleCount ?? null, mdtodoSampleCount: s.mdtodoSampleCount ?? null, latestPageKind: clip(s.latestPageKind, 48), latestPath: clip(s.latestPath, 96), latestSourceCount: s.latestSourceCount ?? null, latestFileCount: s.latestFileCount ?? null, latestTaskCount: s.latestTaskCount ?? null, latestSelectedTaskRefHash: clip(s.latestSelectedTaskRefHash, 80), launchCommandCount: s.launchCommandCount ?? null, launchSuccessCount: s.launchSuccessCount ?? null, launchFailureCount: s.launchFailureCount ?? null, launchWithOtelTraceHeaderCount: s.launchWithOtelTraceHeaderCount ?? null, projectApiResponseCount: s.projectApiResponseCount ?? null, projectApiFailureCount: s.projectApiFailureCount ?? null, projectApiSlowPathCount: s.projectApiSlowPathCount ?? null, slowApiBudgetMs: s.slowApiBudgetMs ?? null }, commands: takeTail(v.commands, 8).map((item) => { const row = objectOrNull(item) || {}; return { ts: row.ts ?? null, phase: clip(row.phase, 16), type: clip(row.type, 32), commandId: clip(row.commandId, 80), launchStatus: row.launchStatus ?? null, sessionId: clip(row.sessionId, 80), workbenchUrl: clip(row.workbenchUrl, 120), otelTraceId: clip(row.otelTraceId, 32), selectedTaskRefHash: clip(row.selectedTaskRefHash, 80) }; }), samples: takeTail(v.samples, 8).map((item) => { const row = objectOrNull(item) || {}; return { seq: row.seq ?? null, ts: row.ts ?? null, pageRole: clip(row.pageRole, 24), path: clip(row.path, 96), pageKind: clip(row.pageKind, 48), sourceCount: row.sourceCount ?? null, fileCount: row.fileCount ?? null, taskCount: row.taskCount ?? null, selectedTaskRefHash: clip(row.selectedTaskRefHash, 80), launchButtonEnabled: row.launchButtonEnabled === true, workbenchLinkCount: row.workbenchLinkCount ?? null }; }), projectApiByPath: takeHead(v.projectApiByPath, 8).map(slimNetworkGroup), valuesRedacted: true }; };",
|
|
"const slimDomGroup = (item) => { const v = objectOrNull(item) || {}; return { count: v.count ?? null, firstAt: v.firstAt ?? null, lastAt: v.lastAt ?? null, text: clip(v.text ?? v.preview, 180) }; };",
|
|
"const slimNetworkGroup = (item) => { const v = objectOrNull(item) || {}; return { count: v.count ?? null, method: clip(v.method, 12), status: v.status ?? null, path: clip(v.path ?? v.urlPath, 96), firstAt: v.firstAt ?? null, lastAt: v.lastAt ?? null, promptIndexes: Array.isArray(v.promptIndexes) ? v.promptIndexes.slice(0, 6) : [], failureKinds: Array.isArray(v.failureKinds) ? v.failureKinds.slice(0, 4).map((x) => clip(x, 48)) : [] }; };",
|
|
"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) || {}; 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 slimCommandFailure = (item) => { const v = objectOrNull(item) || {}; return { ts: v.ts ?? null, commandId: clip(v.commandId, 80), type: clip(v.type, 32), source: clip(v.source, 24), durationMs: v.durationMs ?? null, beforePath: clip(v.beforePath, 80), afterPath: clip(v.afterPath, 80), name: clip(v.name, 48), failureKind: clip(v.failureKind, 48), sampleSeq: v.sampleSeq ?? null, failureSampleOk: v.failureSampleOk === true, message: clip(v.message, 240) }; };",
|
|
"const slimJump = (item) => { const v = objectOrNull(item) || {}; return { columnLabel: v.columnLabel ?? null, pageRole: clip(v.pageRole, 24), pageId: clip(v.pageId, 32), pageEpoch: v.pageEpoch ?? 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 slimTraceOrderAnomaly = (item) => { const v = objectOrNull(item) || {}; return { sampleIndex: v.sampleIndex ?? v.seq ?? null, seq: v.seq ?? null, timestamp: v.timestamp ?? v.ts ?? null, pageRole: clip(v.pageRole, 24), traceId: clip(v.traceId, 64), previousRowIndex: v.previousRowIndex ?? null, currentRowIndex: v.currentRowIndex ?? null, reasons: Array.isArray(v.reasons) ? v.reasons.slice(0, 6).map((x) => clip(x, 48)) : [], previousTotalSeconds: v.previousTotalSeconds ?? null, currentTotalSeconds: v.currentTotalSeconds ?? null, previousClockSeconds: v.previousClockSeconds ?? null, currentClockSeconds: v.currentClockSeconds ?? null, previousPreview: clip(v.previousPreview, 180), currentPreview: clip(v.currentPreview, 180) }; };",
|
|
"const slimTraceOrderMetrics = (value) => { const v = objectOrNull(value); if (!v) return null; const s = objectOrNull(v.summary); return { summary: { sampleCount: v.sampleCount ?? s?.sampleCount ?? null, traceRowCount: v.traceRowCount ?? s?.traceRowCount ?? null, orderAnomalyCount: v.orderAnomalyCount ?? s?.orderAnomalyCount ?? (Array.isArray(v.orderAnomalies) ? v.orderAnomalies.length : null), completionNotLastCount: v.completionNotLastCount ?? s?.completionNotLastCount ?? (Array.isArray(v.completionNotLast) ? v.completionNotLast.length : null) }, orderAnomalies: takeHead(v.orderAnomalies, 8).map(slimTraceOrderAnomaly), valuesRedacted: true }; };",
|
|
"const slimLoadingOwner = (item) => { const v = objectOrNull(item) || {}; return { ownerKey: clip(v.ownerKey, 120), ownerKind: clip(v.ownerKind, 32), ownerLabel: clip(v.ownerLabel, 120), ownerTraceId: clip(v.ownerTraceId, 64), ownerMessageId: clip(v.ownerMessageId, 64), ownerSessionId: clip(v.ownerSessionId, 64), 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), ownerTraceId: clip(owner?.ownerTraceId, 64), ownerMessageId: clip(owner?.ownerMessageId, 64), ownerSessionId: clip(owner?.ownerSessionId, 64), count: owner?.count ?? null })) : [] }; };",
|
|
"const slimLoadingMetrics = (value) => { const v = objectOrNull(value); if (!v) return null; return { summary: objectOrNull(v.summary), longestSegments: takeHead(v.longestSegments ?? v.segments, 8).map(slimLoadingSegment), owners: takeHead(v.owners, 8).map(slimLoadingOwner), timeline: takeTail(v.timeline, 12).map((item) => { const row = objectOrNull(item) || {}; return { seq: row.seq ?? null, ts: row.ts ?? null, promptIndex: row.promptIndex ?? null, loadingCount: row.loadingCount ?? null, ownerCount: row.ownerCount ?? null, owners: Array.isArray(row.owners) ? row.owners.slice(0, 6).map((owner) => ({ ownerKind: clip(owner?.ownerKind, 32), ownerLabel: clip(owner?.ownerLabel, 120), ownerTraceId: clip(owner?.ownerTraceId, 64), count: owner?.count ?? null })) : [] }; }), valuesRedacted: true }; };",
|
|
"const srcMetrics = objectOrNull(source?.sampleMetrics);",
|
|
"const fullRecentWindow = objectOrNull(fullSource?.windows?.recent);",
|
|
"const fullRecentMetrics = objectOrNull(fullRecentWindow?.sampleMetrics);",
|
|
"const fullArchiveMetrics = objectOrNull(fullSource?.sampleMetrics);",
|
|
"const metricHasTurnDetail = (value) => { const v = objectOrNull(value); const s = objectOrNull(v?.summary); return !!(v && ((Array.isArray(v.roundItems) && v.roundItems.length > 0) || (Array.isArray(v.rounds) && v.rounds.length > 0) || (Array.isArray(v.turnColumns) && v.turnColumns.length > 0) || (Array.isArray(v.turnTimingRows) && v.turnTimingRows.length > 0) || (Array.isArray(v.turnTimingTotalElapsedForwardJumps) && v.turnTimingTotalElapsedForwardJumps.length > 0) || (Array.isArray(v.turnTimingRecentUpdateSawtoothJumps) && v.turnTimingRecentUpdateSawtoothJumps.length > 0) || Number(v.turnTimingRows ?? s?.turnTimingRows ?? 0) > 0)); };",
|
|
"const selectedMetrics = metricHasTurnDetail(srcMetrics) ? srcMetrics : metricHasTurnDetail(fullRecentMetrics) ? fullRecentMetrics : metricHasTurnDetail(fullArchiveMetrics) ? fullArchiveMetrics : srcMetrics || fullRecentMetrics || fullArchiveMetrics;",
|
|
"const selectedSummary = objectOrNull(selectedMetrics?.summary);",
|
|
"const metrics = selectedMetrics ? {",
|
|
" sampleCount: selectedMetrics.sampleCount ?? selectedSummary?.sampleCount ?? null,",
|
|
" loadingSampleCount: selectedMetrics.loadingSampleCount ?? selectedSummary?.loadingSampleCount ?? null,",
|
|
" loadingMaxCount: selectedMetrics.loadingMaxCount ?? selectedSummary?.loadingMaxCount ?? null,",
|
|
" loadingMaxOwnerCount: selectedMetrics.loadingMaxOwnerCount ?? selectedSummary?.loadingMaxOwnerCount ?? null,",
|
|
" loadingOwnerCount: selectedMetrics.loadingOwnerCount ?? selectedSummary?.loadingOwnerCount ?? null,",
|
|
" loadingConcurrentSampleCount: selectedMetrics.loadingConcurrentSampleCount ?? selectedSummary?.loadingConcurrentSampleCount ?? null,",
|
|
" loadingLongestContinuousSeconds: selectedMetrics.loadingLongestContinuousSeconds ?? selectedSummary?.loadingLongestContinuousSeconds ?? null,",
|
|
" loadingCurrentContinuousSeconds: selectedMetrics.loadingCurrentContinuousSeconds ?? selectedSummary?.loadingCurrentContinuousSeconds ?? null,",
|
|
" loadingOverFiveSecondSegmentCount: selectedMetrics.loadingOverFiveSecondSegmentCount ?? selectedSummary?.loadingOverFiveSecondSegmentCount ?? null,",
|
|
" loading: slimLoadingMetrics(selectedMetrics.loading),",
|
|
" traceOrder: slimTraceOrderMetrics(selectedMetrics.traceOrder ?? fullRecentMetrics?.traceOrder ?? fullArchiveMetrics?.traceOrder),",
|
|
" roundItems: takeTail(firstNonEmptyArray(selectedMetrics.roundItems, selectedMetrics.rounds, fullRecentMetrics?.rounds, fullArchiveMetrics?.rounds), 8).map(slimRound),",
|
|
" rounds: takeTail(firstNonEmptyArray(selectedMetrics.roundItems, selectedMetrics.rounds, fullRecentMetrics?.rounds, fullArchiveMetrics?.rounds), 8).map(slimRound),",
|
|
" turnColumns: takeTail(firstNonEmptyArray(selectedMetrics.turnColumns, fullRecentMetrics?.turnColumns, fullArchiveMetrics?.turnColumns), 8).map(slimTurnColumn),",
|
|
" turnTimingRows: selectedMetrics.turnTimingRows ?? selectedSummary?.turnTimingRows ?? fullRecentMetrics?.summary?.turnTimingRows ?? fullRecentMetrics?.turnTimingRows?.length ?? fullArchiveMetrics?.summary?.turnTimingRows ?? fullArchiveMetrics?.turnTimingRows?.length ?? null,",
|
|
" turnTimingNonMonotonicCount: selectedMetrics.turnTimingNonMonotonicCount ?? selectedSummary?.turnTimingNonMonotonicCount ?? fullRecentMetrics?.summary?.turnTimingNonMonotonicCount ?? fullRecentMetrics?.turnTimingNonMonotonic?.length ?? fullArchiveMetrics?.summary?.turnTimingNonMonotonicCount ?? fullArchiveMetrics?.turnTimingNonMonotonic?.length ?? null,",
|
|
" turnTimingTotalElapsedDecreaseCount: selectedMetrics.turnTimingTotalElapsedDecreaseCount ?? selectedSummary?.turnTimingTotalElapsedDecreaseCount ?? fullRecentMetrics?.summary?.turnTimingTotalElapsedDecreaseCount ?? fullArchiveMetrics?.summary?.turnTimingTotalElapsedDecreaseCount ?? null,",
|
|
" turnTimingTotalElapsedZeroResetCount: selectedMetrics.turnTimingTotalElapsedZeroResetCount ?? selectedSummary?.turnTimingTotalElapsedZeroResetCount ?? fullRecentMetrics?.summary?.turnTimingTotalElapsedZeroResetCount ?? fullRecentMetrics?.turnTimingElapsedZeroResets?.length ?? fullArchiveMetrics?.summary?.turnTimingTotalElapsedZeroResetCount ?? fullArchiveMetrics?.turnTimingElapsedZeroResets?.length ?? null,",
|
|
" turnTimingTotalElapsedForwardJumpCount: selectedMetrics.turnTimingTotalElapsedForwardJumpCount ?? selectedSummary?.turnTimingTotalElapsedForwardJumpCount ?? fullRecentMetrics?.summary?.turnTimingTotalElapsedForwardJumpCount ?? fullRecentMetrics?.turnTimingTotalElapsedForwardJumps?.length ?? fullArchiveMetrics?.summary?.turnTimingTotalElapsedForwardJumpCount ?? fullArchiveMetrics?.turnTimingTotalElapsedForwardJumps?.length ?? null,",
|
|
" turnTimingTotalElapsedForwardJumpMaxSeconds: selectedMetrics.turnTimingTotalElapsedForwardJumpMaxSeconds ?? selectedSummary?.turnTimingTotalElapsedForwardJumpMaxSeconds ?? fullRecentMetrics?.summary?.turnTimingTotalElapsedForwardJumpMaxSeconds ?? fullArchiveMetrics?.summary?.turnTimingTotalElapsedForwardJumpMaxSeconds ?? null,",
|
|
" turnTimingTerminalElapsedGrowthCount: selectedMetrics.turnTimingTerminalElapsedGrowthCount ?? selectedSummary?.turnTimingTerminalElapsedGrowthCount ?? fullRecentMetrics?.summary?.turnTimingTerminalElapsedGrowthCount ?? fullRecentMetrics?.turnTimingTerminalElapsedGrowth?.length ?? fullArchiveMetrics?.summary?.turnTimingTerminalElapsedGrowthCount ?? fullArchiveMetrics?.turnTimingTerminalElapsedGrowth?.length ?? null,",
|
|
" turnTimingRecentUpdateJumpCount: selectedMetrics.turnTimingRecentUpdateJumpCount ?? selectedSummary?.turnTimingRecentUpdateJumpCount ?? selectedSummary?.turnTimingRecentUpdateSawtoothJumpCount ?? fullRecentMetrics?.summary?.turnTimingRecentUpdateJumpCount ?? fullRecentMetrics?.summary?.turnTimingRecentUpdateSawtoothJumpCount ?? fullArchiveMetrics?.summary?.turnTimingRecentUpdateJumpCount ?? fullArchiveMetrics?.summary?.turnTimingRecentUpdateSawtoothJumpCount ?? null,",
|
|
" turnTimingRecentUpdateSawtoothJumpCount: selectedMetrics.turnTimingRecentUpdateSawtoothJumpCount ?? selectedSummary?.turnTimingRecentUpdateSawtoothJumpCount ?? fullRecentMetrics?.summary?.turnTimingRecentUpdateSawtoothJumpCount ?? fullArchiveMetrics?.summary?.turnTimingRecentUpdateSawtoothJumpCount ?? null,",
|
|
" turnTimingRecentUpdateMaxIncreaseSeconds: selectedMetrics.turnTimingRecentUpdateMaxIncreaseSeconds ?? selectedSummary?.turnTimingRecentUpdateMaxIncreaseSeconds ?? fullRecentMetrics?.summary?.turnTimingRecentUpdateMaxIncreaseSeconds ?? fullArchiveMetrics?.summary?.turnTimingRecentUpdateMaxIncreaseSeconds ?? null,",
|
|
" promptSegments: selectedMetrics.promptSegments ?? selectedSummary?.promptSegments ?? null",
|
|
"} : null;",
|
|
"const srcRuntimeAlerts = objectOrNull(source?.runtimeAlerts);",
|
|
"const runtimeAlerts = srcRuntimeAlerts ? { httpErrorCount: srcRuntimeAlerts.httpErrorCount ?? null, requestFailedCount: srcRuntimeAlerts.requestFailedCount ?? null, domDiagnosticSampleCount: srcRuntimeAlerts.domDiagnosticSampleCount ?? null, consoleAlertCount: srcRuntimeAlerts.consoleAlertCount ?? null } : null;",
|
|
"const srcPagePerformance = objectOrNull(source?.pagePerformance);",
|
|
"const srcPagePerformanceSummary = objectOrNull(srcPagePerformance?.summary);",
|
|
"const pagePerformance = srcPagePerformance ? { sameOriginApiPaths: srcPagePerformance.sameOriginApiPaths ?? srcPagePerformanceSummary?.sameOriginApiPathCount ?? null, longLivedStreamPathCount: srcPagePerformance.longLivedStreamPathCount ?? srcPagePerformanceSummary?.longLivedStreamPathCount ?? null, longLivedStreamOpenOverFiveSecondSampleCount: srcPagePerformance.longLivedStreamOpenOverFiveSecondSampleCount ?? srcPagePerformanceSummary?.longLivedStreamOpenOverFiveSecondSampleCount ?? null, slowPathCount: srcPagePerformance.slowPathCount ?? srcPagePerformanceSummary?.slowPathCount ?? null, slowSampleCount: srcPagePerformance.slowSampleCount ?? srcPagePerformanceSummary?.slowSampleCount ?? null, worstP95Ms: srcPagePerformance.worstP95Ms ?? srcPagePerformanceSummary?.worstP95Ms ?? null } : null;",
|
|
"const fullPagePerformance = objectOrNull(fullRecentWindow?.pagePerformance) || objectOrNull(fullSource?.pagePerformance);",
|
|
"const fullArchivePagePerformance = objectOrNull(fullSource?.pagePerformance);",
|
|
"const isLongLivedApi = (item) => item?.isLongLivedStream === true || item?.routeKind === 'same-origin-api-stream' || item?.budgetMetric === 'streamOpenMs';",
|
|
"const sourceSlowApi = Array.isArray(source?.pagePerformanceSlowApi) ? source.pagePerformanceSlowApi.filter((item) => !isLongLivedApi(item) && Number(item?.overBudgetCount ?? item?.overFiveSecondCount ?? 0) > 0) : (Array.isArray(fullPagePerformance?.sameOriginApiByPath) ? fullPagePerformance.sameOriginApiByPath.filter((item) => !isLongLivedApi(item) && Number(item?.overBudgetCount ?? item?.overFiveSecondCount ?? 0) > 0) : []);",
|
|
"const archiveSlowApi = firstNonEmptyArray(source?.archivePagePerformanceSlowApi, Array.isArray(fullArchivePagePerformance?.sameOriginApiByPath) ? fullArchivePagePerformance.sameOriginApiByPath : []).filter((item) => !isLongLivedApi(item) && Number(item?.overBudgetCount ?? item?.overFiveSecondCount ?? 0) > 0);",
|
|
"const sourceSseStreams = Array.isArray(source?.pagePerformanceSseStreams) ? source.pagePerformanceSseStreams : (Array.isArray(fullPagePerformance?.sameOriginApiByPath) ? fullPagePerformance.sameOriginApiByPath.filter((item) => isLongLivedApi(item)) : []);",
|
|
"const combinedFindingSource = firstNonEmptyArray(source?.findings, fullSource?.findings, fullRecentWindow?.findings, source?.archiveSummary?.redFindings, fullSource?.archiveSummary?.redFindings);",
|
|
"const combinedFindings = sortFindings(combinedFindingSource);",
|
|
"const toolFindings = sortFindings([...firstArray(source?.toolFindings), ...firstArray(fullSource?.toolFindings), ...combinedFindingSource.filter((item) => String(item?.id ?? item?.kind ?? item?.code ?? '').startsWith('tool-'))]);",
|
|
"const archiveRedFindings = sortFindings(firstNonEmptyArray(source?.archiveSummary?.redFindings, fullSource?.archiveSummary?.redFindings, fullSource?.findings).filter((item) => String(item?.severity ?? item?.level ?? '').toLowerCase() === 'red'));",
|
|
"const allFindingsForCommands = [...firstArray(source?.findings), ...firstArray(fullSource?.findings), ...firstArray(fullRecentWindow?.findings), ...firstArray(source?.archiveSummary?.redFindings), ...firstArray(fullSource?.archiveSummary?.redFindings)];",
|
|
"const findingSamplesById = (id) => { for (const item of allFindingsForCommands) { if (String(item?.id ?? item?.kind ?? item?.code ?? '') !== id) continue; if (Array.isArray(item?.samples) && item.samples.length > 0) return item.samples; } return []; };",
|
|
"const commandFailuresFromFindings = [...allFindingsForCommands, ...archiveRedFindings].flatMap((item) => Array.isArray(item?.commands) ? item.commands : []);",
|
|
"const srcPromptNetwork = objectOrNull(source?.promptNetwork);",
|
|
"const promptNetwork = srcPromptNetwork ? { promptSegments: srcPromptNetwork.promptSegments ?? null } : null;",
|
|
"const projectManagement = slimProjectManagement(source?.projectManagement || fullSource?.projectManagement);",
|
|
"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,",
|
|
" jsonlScope: objectOrNull(source.jsonlScope),",
|
|
" analysisWindow: objectOrNull(source.analysisWindow),",
|
|
" archiveSummary: objectOrNull(source.archiveSummary),",
|
|
" sampleMetrics: metrics,",
|
|
" runtimeAlerts,",
|
|
" pagePerformance,",
|
|
" projectManagement,",
|
|
" promptNetwork,",
|
|
" pagePerformanceSlowApi: takeHead(sourceSlowApi, 4).map(slimSlowApi),",
|
|
" archivePagePerformanceSlowApi: takeHead(archiveSlowApi, 8).map(slimSlowApi),",
|
|
" pagePerformanceSseStreams: takeHead(sourceSseStreams, 4).map((item) => ({ path: item?.path ?? item?.route ?? null, route: item?.route ?? null, sampleCount: item?.sampleCount ?? null, streamOpenSampleCount: item?.streamOpenSampleCount ?? null, streamOpenP95Ms: item?.streamOpenP95Ms ?? null, streamOpenMaxMs: item?.streamOpenMaxMs ?? null, streamOpenBudgetMs: item?.streamOpenBudgetMs ?? null, streamOpenOverBudgetCount: item?.streamOpenOverBudgetCount ?? null, streamOpenOverFiveSecondCount: item?.streamOpenOverFiveSecondCount ?? null, streamLifetimeOverFiveSecondCount: item?.streamLifetimeOverFiveSecondCount ?? null })),",
|
|
" toolFindings: takeHead(toolFindings, 8).map(slimFinding),",
|
|
" commandState: objectOrNull(source.commandState) || objectOrNull(fullSource?.commandState) || null,",
|
|
" findings: takeHead(combinedFindings, 8).map(slimFinding),",
|
|
" archiveRedFindings: takeHead(archiveRedFindings, 8).map(slimFinding),",
|
|
" httpErrorGroups: takeHead(firstArray(source.httpErrorGroups, fullRecentWindow?.runtimeAlerts?.networkHttpErrorsByPath, fullSource?.httpErrorGroups, fullSource?.runtimeAlerts?.networkHttpErrorsByPath), 4).map(slimNetworkGroup),",
|
|
" requestFailedGroups: takeHead(firstArray(source.requestFailedGroups, fullRecentWindow?.runtimeAlerts?.networkRequestFailedByPath, fullSource?.requestFailedGroups, fullSource?.runtimeAlerts?.networkRequestFailedByPath), 5).map(slimNetworkGroup),",
|
|
" domDiagnosticGroups: takeHead(firstArray(source.domDiagnosticGroups, fullRecentWindow?.runtimeAlerts?.domDiagnosticsByFingerprint, fullSource?.domDiagnosticGroups, fullSource?.runtimeAlerts?.domDiagnosticsByFingerprint), 3).map(slimDomGroup),",
|
|
" 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(firstNonEmptyArray(source.runnerErrors, fullSource?.runnerErrors, runnerErrorsFromJsonl), 8).map(slimRunnerError),",
|
|
" commandFailures: takeTail(firstNonEmptyArray(source.commandFailures, fullSource?.commandFailures, commandFailuresFromFindings), 8).map(slimCommandFailure),",
|
|
" turnTimingRecentUpdateJumps: takeHead(firstNonEmptyArray(source.turnTimingRecentUpdateJumps, selectedMetrics?.turnTimingRecentUpdateJumps, selectedMetrics?.turnTimingRecentUpdateSawtoothJumps, srcMetrics?.turnTimingRecentUpdateJumps, srcMetrics?.turnTimingRecentUpdateSawtoothJumps, fullRecentWindow?.sampleMetrics?.turnTimingRecentUpdateSawtoothJumps, fullRecentWindow?.turnTimingRecentUpdateJumps, fullSource?.sampleMetrics?.turnTimingRecentUpdateSawtoothJumps, fullSource?.turnTimingRecentUpdateJumps, findingSamplesById('turn-timing-recent-update-sawtooth-jump')), 5).map(slimJump),",
|
|
" turnTimingElapsedZeroResets: takeHead(firstNonEmptyArray(source.turnTimingElapsedZeroResets, selectedMetrics?.turnTimingElapsedZeroResets, srcMetrics?.turnTimingElapsedZeroResets, fullRecentWindow?.sampleMetrics?.turnTimingElapsedZeroResets, fullRecentWindow?.turnTimingElapsedZeroResets, fullSource?.sampleMetrics?.turnTimingElapsedZeroResets, fullSource?.turnTimingElapsedZeroResets, findingSamplesById('turn-timing-total-elapsed-zero-reset')), 5).map(slimJump),",
|
|
" turnTimingTotalElapsedForwardJumps: takeHead(firstNonEmptyArray(source.turnTimingTotalElapsedForwardJumps, selectedMetrics?.turnTimingTotalElapsedForwardJumps, srcMetrics?.turnTimingTotalElapsedForwardJumps, fullRecentWindow?.sampleMetrics?.turnTimingTotalElapsedForwardJumps, fullRecentWindow?.turnTimingTotalElapsedForwardJumps, fullSource?.sampleMetrics?.turnTimingTotalElapsedForwardJumps, fullSource?.turnTimingTotalElapsedForwardJumps, findingSamplesById('turn-timing-total-elapsed-forward-jump')), 5).map(slimJump),",
|
|
" reportJsonPath: source.reportJsonPath || reportJsonPath,",
|
|
" reportJsonSha256: source.reportJsonSha256 || sha256(reportJsonPath),",
|
|
" reportMdPath: source.reportMdPath || reportMdPath,",
|
|
" reportMdSha256: source.reportMdSha256 || sha256(reportMdPath),",
|
|
" analyzer: {",
|
|
" exitCode: Number.isFinite(analyzerExit) ? analyzerExit : null,",
|
|
" recoveredFrom: stdoutJson && stdoutJson.ok !== false ? 'stdout' : reportJson ? 'report-file' : stdoutJson ? 'stdout-error' : 'missing-output',",
|
|
" stdoutBytes: statSize(stdoutPath),",
|
|
" stderrBytes: statSize(stderrPath),",
|
|
" stderrTail: tail(readText(stderrPath))",
|
|
" }",
|
|
"} : {",
|
|
" ok: false,",
|
|
" error: 'web-probe-analyzer-output-missing',",
|
|
" reportJsonPath,",
|
|
" reportMdPath,",
|
|
" analyzer: {",
|
|
" exitCode: Number.isFinite(analyzerExit) ? analyzerExit : null,",
|
|
" recoveredFrom: 'missing-output',",
|
|
" stdoutBytes: statSize(stdoutPath),",
|
|
" stderrBytes: statSize(stderrPath),",
|
|
" stderrTail: tail(readText(stderrPath))",
|
|
" }",
|
|
"};",
|
|
"const compactStdoutLimitBytes = 8000;",
|
|
"const compactOutput = (value) => JSON.stringify(value);",
|
|
"let output = compactOutput(compact);",
|
|
"if (Buffer.byteLength(output, 'utf8') > compactStdoutLimitBytes) {",
|
|
" const minimalRounds = takeTail(firstNonEmptyArray(compact.sampleMetrics?.rounds, compact.sampleMetrics?.roundItems, source?.sampleMetrics?.rounds, fullRecentMetrics?.rounds, fullArchiveMetrics?.rounds), 8).map(slimRound);",
|
|
" const minimalTurnColumns = takeTail(firstNonEmptyArray(compact.sampleMetrics?.turnColumns, source?.sampleMetrics?.turnColumns, fullRecentMetrics?.turnColumns, fullArchiveMetrics?.turnColumns), 8).map(slimTurnColumn);",
|
|
" const minimal = {",
|
|
" ok: compact.ok,",
|
|
" counts: compact.counts ?? null,",
|
|
" jsonlScope: compact.jsonlScope ?? null,",
|
|
" analysisWindow: compact.analysisWindow ?? null,",
|
|
" archiveSummary: compact.archiveSummary ?? null,",
|
|
" sampleMetrics: compact.sampleMetrics ? {",
|
|
" roundItems: minimalRounds,",
|
|
" rounds: minimalRounds,",
|
|
" sampleCount: compact.sampleMetrics.sampleCount ?? null,",
|
|
" loadingSampleCount: compact.sampleMetrics.loadingSampleCount ?? null,",
|
|
" loadingMaxCount: compact.sampleMetrics.loadingMaxCount ?? null,",
|
|
" loadingMaxOwnerCount: compact.sampleMetrics.loadingMaxOwnerCount ?? null,",
|
|
" loadingOwnerCount: compact.sampleMetrics.loadingOwnerCount ?? null,",
|
|
" loadingConcurrentSampleCount: compact.sampleMetrics.loadingConcurrentSampleCount ?? null,",
|
|
" loadingLongestContinuousSeconds: compact.sampleMetrics.loadingLongestContinuousSeconds ?? null,",
|
|
" loadingCurrentContinuousSeconds: compact.sampleMetrics.loadingCurrentContinuousSeconds ?? null,",
|
|
" loadingOverFiveSecondSegmentCount: compact.sampleMetrics.loadingOverFiveSecondSegmentCount ?? null,",
|
|
" sessionRailSampleCount: compact.sampleMetrics.sessionRailSampleCount ?? compact.sampleMetrics.sessionRailTitles?.summary?.sampleCount ?? null,",
|
|
" sessionRailVisibleSampleCount: compact.sampleMetrics.sessionRailVisibleSampleCount ?? compact.sampleMetrics.sessionRailTitles?.summary?.visibleSampleCount ?? null,",
|
|
" sessionRailFallbackMajoritySampleCount: compact.sampleMetrics.sessionRailFallbackMajoritySampleCount ?? compact.sampleMetrics.sessionRailTitles?.summary?.majorityFallbackSampleCount ?? null,",
|
|
" sessionRailFallbackMaxRatio: compact.sampleMetrics.sessionRailFallbackMaxRatio ?? compact.sampleMetrics.sessionRailTitles?.summary?.maxFallbackRatio ?? null,",
|
|
" sessionRailFallbackMaxVisibleCount: compact.sampleMetrics.sessionRailFallbackMaxVisibleCount ?? compact.sampleMetrics.sessionRailTitles?.summary?.maxVisibleCount ?? null,",
|
|
" sessionRailFallbackMaxCount: compact.sampleMetrics.sessionRailFallbackMaxCount ?? compact.sampleMetrics.sessionRailTitles?.summary?.maxFallbackTitleCount ?? null,",
|
|
" loading: compact.sampleMetrics.loading ?? null,",
|
|
" sessionRailTitles: compact.sampleMetrics.sessionRailTitles ? { summary: compact.sampleMetrics.sessionRailTitles.summary ?? null, samples: Array.isArray(compact.sampleMetrics.sessionRailTitles.samples) ? compact.sampleMetrics.sessionRailTitles.samples.slice(0, 4) : [], examples: Array.isArray(compact.sampleMetrics.sessionRailTitles.examples) ? compact.sampleMetrics.sessionRailTitles.examples.slice(0, 4) : [] } : null,",
|
|
" turnTimingRows: compact.sampleMetrics.turnTimingRows ?? null,",
|
|
" turnTimingNonMonotonicCount: compact.sampleMetrics.turnTimingNonMonotonicCount ?? null,",
|
|
" turnTimingTotalElapsedDecreaseCount: compact.sampleMetrics.turnTimingTotalElapsedDecreaseCount ?? null,",
|
|
" turnTimingTotalElapsedForwardJumpCount: compact.sampleMetrics.turnTimingTotalElapsedForwardJumpCount ?? null,",
|
|
" turnTimingTotalElapsedForwardJumpMaxSeconds: compact.sampleMetrics.turnTimingTotalElapsedForwardJumpMaxSeconds ?? null,",
|
|
" turnTimingTerminalElapsedGrowthCount: compact.sampleMetrics.turnTimingTerminalElapsedGrowthCount ?? null,",
|
|
" turnTimingRecentUpdateJumpCount: compact.sampleMetrics.turnTimingRecentUpdateJumpCount ?? null,",
|
|
" turnTimingRecentUpdateSawtoothJumpCount: compact.sampleMetrics.turnTimingRecentUpdateSawtoothJumpCount ?? null,",
|
|
" turnTimingRecentUpdateMaxIncreaseSeconds: compact.sampleMetrics.turnTimingRecentUpdateMaxIncreaseSeconds ?? null,",
|
|
" turnColumns: minimalTurnColumns",
|
|
" } : null,",
|
|
" runtimeAlerts: compact.runtimeAlerts ?? null,",
|
|
" pagePerformance: compact.pagePerformance ?? null,",
|
|
" projectManagement: compact.projectManagement ?? null,",
|
|
" promptNetwork: compact.promptNetwork ?? null,",
|
|
" toolFindings: Array.isArray(compact.toolFindings) ? compact.toolFindings.slice(0, 8) : [],",
|
|
" commandState: compact.commandState ?? null,",
|
|
" findings: Array.isArray(compact.findings) ? compact.findings.slice(0, 4) : [],",
|
|
" archiveRedFindings: Array.isArray(compact.archiveRedFindings) ? compact.archiveRedFindings.slice(0, 4) : [],",
|
|
" turnTimingRecentUpdateJumps: Array.isArray(compact.turnTimingRecentUpdateJumps) ? compact.turnTimingRecentUpdateJumps.slice(0, 5) : [],",
|
|
" turnTimingElapsedZeroResets: Array.isArray(compact.turnTimingElapsedZeroResets) ? compact.turnTimingElapsedZeroResets.slice(0, 5) : [],",
|
|
" turnTimingTotalElapsedForwardJumps: Array.isArray(compact.turnTimingTotalElapsedForwardJumps) ? compact.turnTimingTotalElapsedForwardJumps.slice(0, 5) : [],",
|
|
" pagePerformanceSlowApi: Array.isArray(compact.pagePerformanceSlowApi) ? compact.pagePerformanceSlowApi.slice(0, 2).map((item) => ({ ...item, slowSamples: Array.isArray(item.slowSamples) ? item.slowSamples.slice(0, 1) : [] })) : [],",
|
|
" archivePagePerformanceSlowApi: Array.isArray(compact.archivePagePerformanceSlowApi) ? compact.archivePagePerformanceSlowApi.slice(0, 4).map((item) => ({ ...item, slowSamples: Array.isArray(item.slowSamples) ? item.slowSamples.slice(0, 1) : [] })) : [],",
|
|
" pagePerformanceSseStreams: Array.isArray(compact.pagePerformanceSseStreams) ? compact.pagePerformanceSseStreams.slice(0, 2).map((item) => ({ path: item.path ?? item.route ?? null, route: item.route ?? item.path ?? null, sampleCount: item.sampleCount ?? null, streamOpenP95Ms: item.streamOpenP95Ms ?? null, streamOpenMaxMs: item.streamOpenMaxMs ?? null, streamOpenBudgetMs: item.streamOpenBudgetMs ?? null, streamOpenOverBudgetCount: item.streamOpenOverBudgetCount ?? null, streamOpenOverFiveSecondCount: item.streamOpenOverFiveSecondCount ?? null, streamLifetimeOverFiveSecondCount: item.streamLifetimeOverFiveSecondCount ?? null })) : [],",
|
|
" httpErrorGroups: Array.isArray(compact.httpErrorGroups) ? compact.httpErrorGroups.slice(0, 3) : [],",
|
|
" requestFailedGroups: Array.isArray(compact.requestFailedGroups) ? compact.requestFailedGroups.slice(0, 3) : [],",
|
|
" domDiagnosticGroups: Array.isArray(compact.domDiagnosticGroups) ? compact.domDiagnosticGroups.slice(0, 2) : [],",
|
|
" domDiagnosticSamples: Array.isArray(compact.domDiagnosticSamples) ? compact.domDiagnosticSamples.slice(0, 2) : [],",
|
|
" consoleAlertGroups: Array.isArray(compact.consoleAlertGroups) ? compact.consoleAlertGroups.slice(0, 3) : [],",
|
|
" consoleAlertSamples: Array.isArray(compact.consoleAlertSamples) ? compact.consoleAlertSamples.slice(0, 2) : [],",
|
|
" runnerErrors: Array.isArray(compact.runnerErrors) ? compact.runnerErrors.slice(-4) : [],",
|
|
" commandFailures: Array.isArray(compact.commandFailures) ? compact.commandFailures.slice(-4) : [],",
|
|
" turnTimingRecentUpdateJumps: Array.isArray(compact.turnTimingRecentUpdateJumps) ? compact.turnTimingRecentUpdateJumps.slice(0, 4) : [],",
|
|
" turnTimingElapsedZeroResets: Array.isArray(compact.turnTimingElapsedZeroResets) ? compact.turnTimingElapsedZeroResets.slice(0, 4) : [],",
|
|
" turnTimingTotalElapsedForwardJumps: Array.isArray(compact.turnTimingTotalElapsedForwardJumps) ? compact.turnTimingTotalElapsedForwardJumps.slice(0, 4) : [],",
|
|
" reportJsonPath: compact.reportJsonPath ?? reportJsonPath,",
|
|
" reportJsonSha256: compact.reportJsonSha256 ?? sha256(reportJsonPath),",
|
|
" reportMdPath: compact.reportMdPath ?? reportMdPath,",
|
|
" reportMdSha256: compact.reportMdSha256 ?? sha256(reportMdPath),",
|
|
" analyzer: { ...(compact.analyzer ?? {}), compactStdoutLimited: true, compactStdoutLimitBytes, originalCompactBytes: Buffer.byteLength(output, 'utf8') },",
|
|
" valuesRedacted: true",
|
|
" };",
|
|
" output = compactOutput(minimal);",
|
|
" if (Buffer.byteLength(output, 'utf8') > compactStdoutLimitBytes) {",
|
|
" const tiny = {",
|
|
" ok: compact.ok,",
|
|
" counts: compact.counts ?? null,",
|
|
" jsonlScope: compact.jsonlScope ?? null,",
|
|
" analysisWindow: compact.analysisWindow ?? null,",
|
|
" archiveSummary: compact.archiveSummary ?? null,",
|
|
" sampleMetrics: compact.sampleMetrics ? {",
|
|
" sampleCount: compact.sampleMetrics.sampleCount ?? null,",
|
|
" loadingSampleCount: compact.sampleMetrics.loadingSampleCount ?? null,",
|
|
" loadingMaxCount: compact.sampleMetrics.loadingMaxCount ?? null,",
|
|
" loadingMaxOwnerCount: compact.sampleMetrics.loadingMaxOwnerCount ?? null,",
|
|
" loadingOwnerCount: compact.sampleMetrics.loadingOwnerCount ?? null,",
|
|
" loadingLongestContinuousSeconds: compact.sampleMetrics.loadingLongestContinuousSeconds ?? null,",
|
|
" loadingCurrentContinuousSeconds: compact.sampleMetrics.loadingCurrentContinuousSeconds ?? null,",
|
|
" loadingOverFiveSecondSegmentCount: compact.sampleMetrics.loadingOverFiveSecondSegmentCount ?? null,",
|
|
" loading: compact.sampleMetrics.loading ? { summary: compact.sampleMetrics.loading.summary ?? null, longestSegments: Array.isArray(compact.sampleMetrics.loading.longestSegments) ? compact.sampleMetrics.loading.longestSegments.slice(0, 4) : [], owners: Array.isArray(compact.sampleMetrics.loading.owners) ? compact.sampleMetrics.loading.owners.slice(0, 4) : [] } : null,",
|
|
" sessionRailSampleCount: compact.sampleMetrics.sessionRailSampleCount ?? compact.sampleMetrics.sessionRailTitles?.summary?.sampleCount ?? null,",
|
|
" sessionRailVisibleSampleCount: compact.sampleMetrics.sessionRailVisibleSampleCount ?? compact.sampleMetrics.sessionRailTitles?.summary?.visibleSampleCount ?? null,",
|
|
" sessionRailFallbackMajoritySampleCount: compact.sampleMetrics.sessionRailFallbackMajoritySampleCount ?? compact.sampleMetrics.sessionRailTitles?.summary?.majorityFallbackSampleCount ?? null,",
|
|
" sessionRailFallbackMaxRatio: compact.sampleMetrics.sessionRailFallbackMaxRatio ?? compact.sampleMetrics.sessionRailTitles?.summary?.maxFallbackRatio ?? null,",
|
|
" sessionRailFallbackMaxVisibleCount: compact.sampleMetrics.sessionRailFallbackMaxVisibleCount ?? compact.sampleMetrics.sessionRailTitles?.summary?.maxVisibleCount ?? null,",
|
|
" sessionRailFallbackMaxCount: compact.sampleMetrics.sessionRailFallbackMaxCount ?? compact.sampleMetrics.sessionRailTitles?.summary?.maxFallbackTitleCount ?? null,",
|
|
" sessionRailTitles: compact.sampleMetrics.sessionRailTitles ? { summary: compact.sampleMetrics.sessionRailTitles.summary ?? null, samples: Array.isArray(compact.sampleMetrics.sessionRailTitles.samples) ? compact.sampleMetrics.sessionRailTitles.samples.slice(0, 4) : [], examples: Array.isArray(compact.sampleMetrics.sessionRailTitles.examples) ? compact.sampleMetrics.sessionRailTitles.examples.slice(0, 4) : [] } : null,",
|
|
" turnTimingRows: compact.sampleMetrics.turnTimingRows ?? null,",
|
|
" turnTimingNonMonotonicCount: compact.sampleMetrics.turnTimingNonMonotonicCount ?? null,",
|
|
" turnTimingTotalElapsedDecreaseCount: compact.sampleMetrics.turnTimingTotalElapsedDecreaseCount ?? null,",
|
|
" turnTimingTotalElapsedForwardJumpCount: compact.sampleMetrics.turnTimingTotalElapsedForwardJumpCount ?? null,",
|
|
" turnTimingTerminalElapsedGrowthCount: compact.sampleMetrics.turnTimingTerminalElapsedGrowthCount ?? null,",
|
|
" turnTimingRecentUpdateJumpCount: compact.sampleMetrics.turnTimingRecentUpdateJumpCount ?? compact.sampleMetrics.turnTimingRecentUpdateSawtoothJumpCount ?? null",
|
|
" } : null,",
|
|
" runtimeAlerts: compact.runtimeAlerts ?? null,",
|
|
" pagePerformance: compact.pagePerformance ?? null,",
|
|
" projectManagement: compact.projectManagement ? { summary: compact.projectManagement.summary ?? compact.projectManagement, samples: Array.isArray(compact.projectManagement.samples) ? compact.projectManagement.samples.slice(-4) : [], commands: Array.isArray(compact.projectManagement.commands) ? compact.projectManagement.commands.slice(-4) : [], launchCommands: Array.isArray(compact.projectManagement.launchCommands) ? compact.projectManagement.launchCommands.slice(-4) : [], projectApiByPath: Array.isArray(compact.projectManagement.projectApiByPath) ? compact.projectManagement.projectApiByPath.slice(0, 4) : [], slowProjectApiPerformance: Array.isArray(compact.projectManagement.slowProjectApiPerformance) ? compact.projectManagement.slowProjectApiPerformance.slice(0, 4) : [], valuesRedacted: true } : null,",
|
|
" toolFindings: Array.isArray(compact.toolFindings) ? compact.toolFindings.slice(0, 8) : [],",
|
|
" commandState: compact.commandState ?? null,",
|
|
" findings: Array.isArray(compact.findings) ? compact.findings.slice(0, 4) : [],",
|
|
" archiveRedFindings: Array.isArray(compact.archiveRedFindings) ? compact.archiveRedFindings.slice(0, 4) : [],",
|
|
" pagePerformanceSlowApi: Array.isArray(compact.pagePerformanceSlowApi) ? compact.pagePerformanceSlowApi.slice(0, 2).map((item) => ({ path: item.path ?? item.route ?? null, route: item.route ?? item.path ?? null, sampleCount: item.sampleCount ?? null, p95Ms: item.p95Ms ?? item.p95 ?? null, maxMs: item.maxMs ?? item.max ?? null, budgetMs: item.budgetMs ?? null, overBudgetCount: item.overBudgetCount ?? null, overFiveSecondCount: item.overFiveSecondCount ?? null, slowSamples: Array.isArray(item.slowSamples) ? item.slowSamples.slice(0, 1) : [] })) : [],",
|
|
" archivePagePerformanceSlowApi: Array.isArray(compact.archivePagePerformanceSlowApi) ? compact.archivePagePerformanceSlowApi.slice(0, 4).map((item) => ({ path: item.path ?? item.route ?? null, route: item.route ?? item.path ?? null, sampleCount: item.sampleCount ?? null, p95Ms: item.p95Ms ?? item.p95 ?? null, maxMs: item.maxMs ?? item.max ?? null, budgetMs: item.budgetMs ?? null, overBudgetCount: item.overBudgetCount ?? null, overFiveSecondCount: item.overFiveSecondCount ?? null, slowSamples: Array.isArray(item.slowSamples) ? item.slowSamples.slice(0, 1) : [] })) : [],",
|
|
" pagePerformanceSseStreams: Array.isArray(compact.pagePerformanceSseStreams) ? compact.pagePerformanceSseStreams.slice(0, 2).map((item) => ({ path: item.path ?? item.route ?? null, route: item.route ?? item.path ?? null, sampleCount: item.sampleCount ?? null, streamOpenP95Ms: item.streamOpenP95Ms ?? null, streamOpenMaxMs: item.streamOpenMaxMs ?? null, streamOpenOverFiveSecondCount: item.streamOpenOverFiveSecondCount ?? null, streamLifetimeOverFiveSecondCount: item.streamLifetimeOverFiveSecondCount ?? null })) : [],",
|
|
" httpErrorGroups: Array.isArray(compact.httpErrorGroups) ? compact.httpErrorGroups.slice(0, 2) : [],",
|
|
" requestFailedGroups: Array.isArray(compact.requestFailedGroups) ? compact.requestFailedGroups.slice(0, 2) : [],",
|
|
" consoleAlertGroups: Array.isArray(compact.consoleAlertGroups) ? compact.consoleAlertGroups.slice(0, 2) : [],",
|
|
" commandFailures: Array.isArray(compact.commandFailures) ? compact.commandFailures.slice(-3) : [],",
|
|
" turnTimingRecentUpdateJumps: Array.isArray(compact.turnTimingRecentUpdateJumps) ? compact.turnTimingRecentUpdateJumps.slice(0, 3) : [],",
|
|
" turnTimingTotalElapsedForwardJumps: Array.isArray(compact.turnTimingTotalElapsedForwardJumps) ? compact.turnTimingTotalElapsedForwardJumps.slice(0, 3) : [],",
|
|
" reportJsonPath: compact.reportJsonPath ?? reportJsonPath,",
|
|
" reportJsonSha256: compact.reportJsonSha256 ?? sha256(reportJsonPath),",
|
|
" reportMdPath: compact.reportMdPath ?? reportMdPath,",
|
|
" reportMdSha256: compact.reportMdSha256 ?? sha256(reportMdPath),",
|
|
" analyzer: { ...(compact.analyzer ?? {}), compactStdoutLimited: true, compactStdoutLimitBytes, originalCompactBytes: Buffer.byteLength(compactOutput(compact), 'utf8'), originalMinimalBytes: Buffer.byteLength(output, 'utf8') },",
|
|
" valuesRedacted: true",
|
|
" };",
|
|
" output = compactOutput(tiny);",
|
|
" if (Buffer.byteLength(output, 'utf8') > compactStdoutLimitBytes) {",
|
|
" const ultratiny = {",
|
|
" ok: compact.ok,",
|
|
" counts: compact.counts ?? null,",
|
|
" analysisWindow: compact.analysisWindow ?? null,",
|
|
" archiveSummary: compact.archiveSummary ?? null,",
|
|
" sampleMetrics: compact.sampleMetrics ? {",
|
|
" sampleCount: compact.sampleMetrics.sampleCount ?? null,",
|
|
" loadingSampleCount: compact.sampleMetrics.loadingSampleCount ?? null,",
|
|
" loadingMaxCount: compact.sampleMetrics.loadingMaxCount ?? null,",
|
|
" loadingOverFiveSecondSegmentCount: compact.sampleMetrics.loadingOverFiveSecondSegmentCount ?? null,",
|
|
" loading: compact.sampleMetrics.loading ? { summary: compact.sampleMetrics.loading.summary ?? null, longestSegments: Array.isArray(compact.sampleMetrics.loading.longestSegments) ? compact.sampleMetrics.loading.longestSegments.slice(0, 4) : [], owners: Array.isArray(compact.sampleMetrics.loading.owners) ? compact.sampleMetrics.loading.owners.slice(0, 4) : [] } : null,",
|
|
" sessionRailFallbackMajoritySampleCount: compact.sampleMetrics.sessionRailFallbackMajoritySampleCount ?? compact.sampleMetrics.sessionRailTitles?.summary?.majorityFallbackSampleCount ?? null,",
|
|
" turnTimingRows: compact.sampleMetrics.turnTimingRows ?? null,",
|
|
" turnTimingTotalElapsedForwardJumpCount: compact.sampleMetrics.turnTimingTotalElapsedForwardJumpCount ?? null,",
|
|
" turnTimingRecentUpdateJumpCount: compact.sampleMetrics.turnTimingRecentUpdateJumpCount ?? compact.sampleMetrics.turnTimingRecentUpdateSawtoothJumpCount ?? null",
|
|
" } : null,",
|
|
" runtimeAlerts: compact.runtimeAlerts ?? null,",
|
|
" pagePerformance: compact.pagePerformance ?? null,",
|
|
" projectManagement: compact.projectManagement ? { summary: compact.projectManagement.summary ?? compact.projectManagement, samples: Array.isArray(compact.projectManagement.samples) ? compact.projectManagement.samples.slice(-3) : [], commands: Array.isArray(compact.projectManagement.commands) ? compact.projectManagement.commands.slice(-3) : [], launchCommands: Array.isArray(compact.projectManagement.launchCommands) ? compact.projectManagement.launchCommands.slice(-3) : [], projectApiByPath: Array.isArray(compact.projectManagement.projectApiByPath) ? compact.projectManagement.projectApiByPath.slice(0, 3) : [], slowProjectApiPerformance: Array.isArray(compact.projectManagement.slowProjectApiPerformance) ? compact.projectManagement.slowProjectApiPerformance.slice(0, 2) : [], valuesRedacted: true } : null,",
|
|
" toolFindings: Array.isArray(compact.toolFindings) ? compact.toolFindings.slice(0, 8) : [],",
|
|
" commandState: compact.commandState ?? null,",
|
|
" findings: Array.isArray(compact.findings) ? compact.findings.slice(0, 4).map((item) => ({ kind: item.kind ?? item.code ?? null, severity: item.severity ?? item.level ?? null, count: item.count ?? item.sampleCount ?? null, summary: clip(item.summary ?? item.message, 120) })) : [],",
|
|
" archiveRedFindings: Array.isArray(compact.archiveRedFindings) ? compact.archiveRedFindings.slice(0, 4).map((item) => ({ kind: item.kind ?? item.code ?? null, severity: item.severity ?? item.level ?? null, count: item.count ?? item.sampleCount ?? null, summary: clip(item.summary ?? item.message, 120) })) : [],",
|
|
" httpErrorGroups: Array.isArray(compact.httpErrorGroups) ? compact.httpErrorGroups.slice(0, 2).map((item) => ({ count: item.count ?? null, method: item.method ?? null, status: item.status ?? null, path: item.path ?? null, lastAt: item.lastAt ?? null })) : [],",
|
|
" requestFailedGroups: Array.isArray(compact.requestFailedGroups) ? compact.requestFailedGroups.slice(0, 2).map((item) => ({ count: item.count ?? null, method: item.method ?? null, path: item.path ?? null, failureKinds: item.failureKinds ?? null })) : [],",
|
|
" commandFailures: Array.isArray(compact.commandFailures) ? compact.commandFailures.slice(-3).map((item) => ({ ts: item.ts ?? null, type: item.type ?? null, commandId: item.commandId ?? null, durationMs: item.durationMs ?? null, sampleSeq: item.sampleSeq ?? null, beforePath: item.beforePath ?? null, afterPath: item.afterPath ?? null, message: clip(item.message ?? item.failureKind ?? item.name, 120) })) : [],",
|
|
" turnTimingRecentUpdateJumps: Array.isArray(compact.turnTimingRecentUpdateJumps) ? compact.turnTimingRecentUpdateJumps.slice(0, 3) : [],",
|
|
" turnTimingElapsedZeroResets: Array.isArray(compact.turnTimingElapsedZeroResets) ? compact.turnTimingElapsedZeroResets.slice(0, 3) : [],",
|
|
" turnTimingTotalElapsedForwardJumps: Array.isArray(compact.turnTimingTotalElapsedForwardJumps) ? compact.turnTimingTotalElapsedForwardJumps.slice(0, 3) : [],",
|
|
" pagePerformanceSlowApi: Array.isArray(compact.pagePerformanceSlowApi) ? compact.pagePerformanceSlowApi.slice(0, 2).map((item) => ({ path: item.path ?? item.route ?? null, sampleCount: item.sampleCount ?? null, p95Ms: item.p95Ms ?? null, maxMs: item.maxMs ?? null, budgetMs: item.budgetMs ?? null, overBudgetCount: item.overBudgetCount ?? null, overFiveSecondCount: item.overFiveSecondCount ?? null })) : [],",
|
|
" archivePagePerformanceSlowApi: Array.isArray(compact.archivePagePerformanceSlowApi) ? compact.archivePagePerformanceSlowApi.slice(0, 4).map((item) => ({ path: item.path ?? item.route ?? null, sampleCount: item.sampleCount ?? null, p95Ms: item.p95Ms ?? null, maxMs: item.maxMs ?? null, budgetMs: item.budgetMs ?? null, overBudgetCount: item.overBudgetCount ?? null, overFiveSecondCount: item.overFiveSecondCount ?? null })) : [],",
|
|
" pagePerformanceSseStreams: Array.isArray(compact.pagePerformanceSseStreams) ? compact.pagePerformanceSseStreams.slice(0, 2).map((item) => ({ path: item.path ?? item.route ?? null, sampleCount: item.sampleCount ?? null, streamOpenP95Ms: item.streamOpenP95Ms ?? null, streamOpenMaxMs: item.streamOpenMaxMs ?? null, streamOpenBudgetMs: item.streamOpenBudgetMs ?? null, streamOpenOverBudgetCount: item.streamOpenOverBudgetCount ?? null, streamOpenOverFiveSecondCount: item.streamOpenOverFiveSecondCount ?? null, streamLifetimeOverFiveSecondCount: item.streamLifetimeOverFiveSecondCount ?? null })) : [],",
|
|
" reportJsonPath: compact.reportJsonPath ?? reportJsonPath,",
|
|
" reportJsonSha256: compact.reportJsonSha256 ?? sha256(reportJsonPath),",
|
|
" reportMdPath: compact.reportMdPath ?? reportMdPath,",
|
|
" reportMdSha256: compact.reportMdSha256 ?? sha256(reportMdPath),",
|
|
" analyzer: { ...(compact.analyzer ?? {}), compactStdoutLimited: true, ultratiny: true, compactStdoutLimitBytes, originalCompactBytes: Buffer.byteLength(compactOutput(compact), 'utf8'), originalTinyBytes: Buffer.byteLength(output, 'utf8') },",
|
|
" valuesRedacted: true",
|
|
" };",
|
|
" output = compactOutput(ultratiny);",
|
|
" }",
|
|
" if (Buffer.byteLength(output, 'utf8') > compactStdoutLimitBytes) {",
|
|
" output = compactOutput({ ok: compact.ok, counts: compact.counts ?? null, jsonlScope: compact.jsonlScope ?? null, analysisWindow: compact.analysisWindow ?? null, archiveSummary: compact.archiveSummary ? { redFindingCount: compact.archiveSummary.redFindingCount ?? null, findingCount: compact.archiveSummary.findingCount ?? null, sampleCount: compact.archiveSummary.sampleMetrics?.sampleCount ?? null, slowPathCount: compact.archiveSummary.pagePerformance?.slowPathCount ?? null } : null, projectManagement: compact.projectManagement ? { summary: compact.projectManagement.summary ?? compact.projectManagement, samples: Array.isArray(compact.projectManagement.samples) ? compact.projectManagement.samples.slice(-2) : [], commands: Array.isArray(compact.projectManagement.commands) ? compact.projectManagement.commands.slice(-2) : [], launchCommands: Array.isArray(compact.projectManagement.launchCommands) ? compact.projectManagement.launchCommands.slice(-2) : [], projectApiByPath: Array.isArray(compact.projectManagement.projectApiByPath) ? compact.projectManagement.projectApiByPath.slice(0, 2) : [], valuesRedacted: true } : null, toolFindings: Array.isArray(compact.toolFindings) ? compact.toolFindings.slice(0, 8) : [], commandState: compact.commandState ? { pendingCount: compact.commandState.pendingCount ?? null, processingCount: compact.commandState.processingCount ?? null, abandonedCount: compact.commandState.abandonedCount ?? null, failedCount: compact.commandState.failedCount ?? null } : null, findings: Array.isArray(compact.findings) ? compact.findings.slice(0, 4).map((item) => ({ kind: item.kind ?? item.code ?? null, severity: item.severity ?? item.level ?? null, count: item.count ?? item.sampleCount ?? null, summary: clip(item.summary ?? item.message, 120) })) : [], archiveRedFindings: Array.isArray(compact.archiveRedFindings) ? compact.archiveRedFindings.slice(0, 4).map((item) => ({ kind: item.kind ?? item.code ?? null, severity: item.severity ?? item.level ?? null, count: item.count ?? item.sampleCount ?? null, summary: clip(item.summary ?? item.message, 120) })) : [], commandFailures: Array.isArray(compact.commandFailures) ? compact.commandFailures.slice(-3).map((item) => ({ ts: item.ts ?? null, type: item.type ?? null, commandId: item.commandId ?? null, durationMs: item.durationMs ?? null, sampleSeq: item.sampleSeq ?? null, beforePath: item.beforePath ?? null, afterPath: item.afterPath ?? null, message: clip(item.message ?? item.failureKind ?? item.name, 120) })) : [], archivePagePerformanceSlowApi: Array.isArray(compact.archivePagePerformanceSlowApi) ? compact.archivePagePerformanceSlowApi.slice(0, 4).map((item) => ({ path: item.path ?? item.route ?? null, sampleCount: item.sampleCount ?? null, p95Ms: item.p95Ms ?? null, maxMs: item.maxMs ?? null, overFiveSecondCount: item.overFiveSecondCount ?? null })) : [], reportJsonPath: compact.reportJsonPath ?? reportJsonPath, reportJsonSha256: compact.reportJsonSha256 ?? sha256(reportJsonPath), reportMdPath: compact.reportMdPath ?? reportMdPath, reportMdSha256: compact.reportMdSha256 ?? sha256(reportMdPath), analyzer: { ...(compact.analyzer ?? {}), compactStdoutLimited: true, ultratiny: true, hardFallback: true, compactStdoutLimitBytes }, valuesRedacted: true });",
|
|
" }",
|
|
" }",
|
|
"}",
|
|
"console.log(output);",
|
|
"UNIDESK_WEB_OBSERVE_ANALYZE_COMPACT",
|
|
"exit \"$analyzer_exit\"",
|
|
].join("\n");
|
|
const result = runTransWorkspaceStdinScript(options.node, spec.workspace, script, options.commandTimeoutSeconds);
|
|
const primaryAnalysis = recoverWebObserveAnalyzeTurnDetails(options, spec, parseJsonObject(result.stdout));
|
|
const artifactAnalysis = (result.timedOut || result.exitCode !== 0 || primaryAnalysis === null)
|
|
? recoverWebObserveAnalyzeFromArtifacts(options, spec, result)
|
|
: null;
|
|
const analysis = recoverWebObserveAnalyzeTurnDetails(options, spec, artifactAnalysis ?? primaryAnalysis);
|
|
const analysisOk = analysis?.ok === true;
|
|
const analysisFailure = analysisOk
|
|
? null
|
|
: {
|
|
reason: analysis === null ? "analyzer-output-not-json" : "analyzer-reported-not-ok",
|
|
exitCode: result.exitCode,
|
|
timedOut: result.timedOut,
|
|
parsedJson: analysis !== null,
|
|
recoveredFromArtifacts: artifactAnalysis !== null,
|
|
stdoutBytes: result.stdout.length,
|
|
stderrBytes: result.stderr.length,
|
|
stdoutTail: result.stdout.trim().slice(-1200),
|
|
stderrTail: result.stderr.trim().slice(-1200),
|
|
workspace: spec.workspace,
|
|
observerId: webObserveIdFromOptions(options),
|
|
valuesRedacted: true,
|
|
};
|
|
const analysisArtifactDir = options.stateDir ? join(options.stateDir, "analysis") : "";
|
|
const failureAnalysis = analysis ?? (analysisFailure ? {
|
|
ok: false,
|
|
command: "web-probe-observe analyze",
|
|
stateDir: options.stateDir,
|
|
reportJsonPath: analysisArtifactDir ? join(analysisArtifactDir, "report.json") : null,
|
|
reportMdPath: analysisArtifactDir ? join(analysisArtifactDir, "report.md") : null,
|
|
analyzer: {
|
|
exitCode: result.exitCode,
|
|
timedOut: result.timedOut,
|
|
stdoutBytes: result.stdout.length,
|
|
stderrBytes: result.stderr.length,
|
|
stdoutPath: analysisArtifactDir ? join(analysisArtifactDir, "analyzer-stdout.json") : null,
|
|
stderrPath: analysisArtifactDir ? join(analysisArtifactDir, "analyzer-stderr.log") : null,
|
|
stdoutTail: result.stdout.trim().slice(-1200),
|
|
stderrTail: result.stderr.trim().slice(-1200),
|
|
recoveredFrom: "analyzer-timeout-failure-contract",
|
|
valuesRedacted: true,
|
|
},
|
|
findings: [{
|
|
kind: result.timedOut ? "web-probe-analyzer-timeout" : "web-probe-analyzer-failed",
|
|
severity: "red",
|
|
count: 1,
|
|
summary: result.timedOut
|
|
? "observe analyze timed out before producing a compact report; inspect analyzer stdout/stderr artifacts under the observer analysis directory"
|
|
: "observe analyze failed before producing a compact report; inspect analyzer stdout/stderr artifacts under the observer analysis directory",
|
|
}],
|
|
next: {
|
|
collectAnalyzerStdout: options.stateDir ? `bun scripts/cli.ts web-probe observe collect --node ${options.node} --lane ${options.lane} --state-dir ${options.stateDir} --file analysis/analyzer-stdout.json` : null,
|
|
collectAnalyzerStderr: options.stateDir ? `bun scripts/cli.ts web-probe observe collect --node ${options.node} --lane ${options.lane} --state-dir ${options.stateDir} --file analysis/analyzer-stderr.log` : null,
|
|
valuesRedacted: true,
|
|
},
|
|
valuesRedacted: true,
|
|
} : null);
|
|
return withWebObserveAnalyzeRendered({
|
|
ok: analysisOk,
|
|
status: analysisOk ? "analyzed" : "blocked",
|
|
command: webObserveCommandLabel("analyze", options),
|
|
id: webObserveIdFromOptions(options),
|
|
node: options.node,
|
|
lane: options.lane,
|
|
workspace: spec.workspace,
|
|
analysis: failureAnalysis,
|
|
failure: analysisFailure,
|
|
alertThresholds,
|
|
wrapper: buildWebObserveWrapperForObserveOptions("analyze", options, spec.workspace),
|
|
result: analysis === null ? compactCommandResultWithStdoutTail(result) : compactCommandResult(result),
|
|
valuesRedacted: true,
|
|
});
|
|
}
|
|
|
|
export function recoverWebObserveAnalyzeFromArtifacts(options: NodeWebProbeObserveOptions, spec: HwlabRuntimeLaneSpec, result: { exitCode: number; timedOut: boolean }): Record<string, unknown> | null {
|
|
const recoverScript = [
|
|
"set -eu",
|
|
nodeWebObserveResolveStateDirShell(options),
|
|
"analysis_dir=\"$state_dir/analysis\"",
|
|
"analysis_stdout=\"$analysis_dir/analyzer-stdout.json\"",
|
|
"analysis_stderr=\"$analysis_dir/analyzer-stderr.log\"",
|
|
"report_json=\"$analysis_dir/report.json\"",
|
|
"report_md=\"$analysis_dir/report.md\"",
|
|
`transport_exit=${shellQuote(String(result.exitCode))}`,
|
|
`transport_timed_out=${shellQuote(result.timedOut ? "true" : "false")}`,
|
|
"node - \"$state_dir\" \"$analysis_stdout\" \"$analysis_stderr\" \"$report_json\" \"$report_md\" \"$transport_exit\" \"$transport_timed_out\" <<'UNIDESK_WEB_OBSERVE_RECOVER_ANALYZE_ARTIFACT'",
|
|
"const fs = require('fs');",
|
|
"const crypto = require('crypto');",
|
|
"const [stateDir, stdoutPath, stderrPath, reportJsonPath, reportMdPath, transportExitRaw, transportTimedOutRaw] = process.argv.slice(2);",
|
|
"const readText = (path) => { try { return fs.readFileSync(path, 'utf8'); } catch { return ''; } };",
|
|
"const readJson = (path) => { const text = readText(path); if (!text.trim()) return null; try { return JSON.parse(text); } catch { return null; } };",
|
|
"const statSize = (path) => { try { return fs.statSync(path).size; } catch { return 0; } };",
|
|
"const sha256 = (path) => { const text = readText(path); return text ? 'sha256:' + crypto.createHash('sha256').update(text).digest('hex') : null; };",
|
|
"const objectOrNull = (value) => value && typeof value === 'object' && !Array.isArray(value) ? value : null;",
|
|
"const arr = (value) => Array.isArray(value) ? value : [];",
|
|
"const clip = (value, limit = 160) => value === null || value === undefined ? null : String(value).slice(0, limit);",
|
|
"const numberish = (...values) => { for (const value of values) { const n = Number(value); if (Number.isFinite(n)) return value; } return null; };",
|
|
"const slimFinding = (item) => { const v = objectOrNull(item) || {}; return { kind: clip(v.kind ?? v.id ?? v.code, 48), code: clip(v.code ?? v.id ?? v.kind, 48), severity: clip(v.severity ?? v.level, 24), level: clip(v.level ?? v.severity, 24), count: v.count ?? v.sampleCount ?? null, sampleCount: v.sampleCount ?? v.count ?? null, summary: clip(v.summary ?? v.message, 180), message: clip(v.message ?? v.summary, 180) }; };",
|
|
"const slimRound = (item) => { const v = objectOrNull(item) || {}; return { promptIndex: v.promptIndex ?? null, promptTextHash: clip(v.promptTextHash, 80), sampleCount: v.sampleCount ?? null, firstSeq: v.firstSeq ?? null, lastSeq: v.lastSeq ?? null, lastTotalElapsedSeconds: v.lastTotalElapsedSeconds ?? null, lastRecentUpdateSeconds: v.lastRecentUpdateSeconds ?? null, loadingSamples: v.loadingSamples ?? null, maxLoadingCount: v.maxLoadingCount ?? null, loadingOwnerCount: v.loadingOwnerCount ?? null, diagnosticSamples: v.diagnosticSamples ?? null, terminalSamples: v.terminalSamples ?? null, finalTextSamples: v.finalTextSamples ?? null, turnTimingTotalElapsedZeroResetCount: v.turnTimingTotalElapsedZeroResetCount ?? null, turnTimingTotalElapsedForwardJumpCount: v.turnTimingTotalElapsedForwardJumpCount ?? null, turnTimingTotalElapsedForwardJumpMaxSeconds: v.turnTimingTotalElapsedForwardJumpMaxSeconds ?? null, turnTimingRecentUpdateJumpCount: v.turnTimingRecentUpdateJumpCount ?? null, turnTimingRecentUpdateMaxIncreaseSeconds: v.turnTimingRecentUpdateMaxIncreaseSeconds ?? null }; };",
|
|
"const slimTurnColumn = (item) => { const v = objectOrNull(item) || {}; return { label: clip(v.label, 24), source: clip(v.source, 48), pageRole: clip(v.pageRole, 24), pageId: clip(v.pageId, 32), pageEpoch: v.pageEpoch ?? null, promptIndex: v.promptIndex ?? null, lastPromptIndex: v.lastPromptIndex ?? null, firstSeq: v.firstSeq ?? null, lastSeq: v.lastSeq ?? null, traceId: clip(v.traceId, 64), messageId: clip(v.messageId, 64) }; };",
|
|
"const slimGroup = (item) => { const v = objectOrNull(item) || {}; return { count: v.count ?? null, method: clip(v.method, 12), status: v.status ?? null, path: clip(v.path ?? v.urlPath ?? v.route, 96), firstAt: v.firstAt ?? null, lastAt: v.lastAt ?? null, text: clip(v.text ?? v.preview, 180), failureKinds: arr(v.failureKinds).slice(0, 4).map((x) => clip(x, 48)), traceIds: arr(v.traceIds).slice(0, 3).map((x) => clip(x, 64)) }; };",
|
|
"const slimSlowSample = (item) => { const v = objectOrNull(item) || {}; return { ts: v.ts ?? null, seq: v.seq ?? null, path: clip(v.path ?? v.rawPath, 96), initiatorType: clip(v.initiatorType, 24), durationMs: v.durationMs ?? null, requestToResponseStartMs: v.requestToResponseStartMs ?? v.streamOpenMs ?? null, responseTransferMs: v.responseTransferMs ?? null, timingStatus: clip(v.timingStatus, 16), nextHopProtocol: clip(v.nextHopProtocol, 24), serverTimingNames: arr(v.serverTimingNames).slice(0, 4).map((x) => clip(x, 32)), otelTraceId: clip(v.otelTraceId, 32) }; };",
|
|
"const slimSlowApi = (item) => { const v = objectOrNull(item) || {}; return { path: clip(v.path ?? v.route, 96), route: clip(v.route ?? v.path, 96), sampleCount: v.sampleCount ?? null, p95Ms: v.p95Ms ?? v.p95 ?? null, maxMs: v.maxMs ?? v.max ?? null, budgetMs: v.budgetMs ?? null, overBudgetCount: v.overBudgetCount ?? null, overFiveSecondCount: v.overFiveSecondCount ?? null, slowSamples: arr(v.slowSamples).slice(0, 3).map(slimSlowSample) }; };",
|
|
"const compactLoading = (value) => { const v = objectOrNull(value); if (!v) return null; const s = objectOrNull(v.summary) || {}; return { summary: s, longestSegments: arr(v.longestSegments ?? v.segments).slice(0, 8), owners: arr(v.owners).slice(0, 8), timeline: arr(v.timeline).slice(-12) }; };",
|
|
"const compactSessionRailTitles = (value) => { const v = objectOrNull(value); if (!v) return null; return { summary: objectOrNull(v.summary) || {}, samples: arr(v.samples).slice(0, 8), examples: arr(v.examples).slice(0, 8) }; };",
|
|
"const compactTraceOrder = (value) => { const v = objectOrNull(value); if (!v) return null; const s = objectOrNull(v.summary) || {}; return { summary: { sampleCount: v.sampleCount ?? s.sampleCount ?? null, traceRowCount: v.traceRowCount ?? s.traceRowCount ?? null, orderAnomalyCount: v.orderAnomalyCount ?? s.orderAnomalyCount ?? arr(v.orderAnomalies).length, completionNotLastCount: v.completionNotLastCount ?? s.completionNotLastCount ?? arr(v.completionNotLast).length }, orderAnomalies: arr(v.orderAnomalies).slice(0, 8) }; };",
|
|
"const compactMetrics = (value) => { const v = objectOrNull(value) || {}; const s = objectOrNull(v.summary) || {}; return { sampleCount: numberish(v.sampleCount, s.sampleCount), rounds: arr(v.rounds ?? v.roundItems).slice(-8).map(slimRound), roundItems: arr(v.roundItems ?? v.rounds).slice(-8).map(slimRound), turnColumns: arr(v.turnColumns).slice(-12).map(slimTurnColumn), turnTimingRows: numberish(v.turnTimingRows, s.turnTimingRows), turnTimingNonMonotonicCount: numberish(v.turnTimingNonMonotonicCount, s.turnTimingNonMonotonicCount), turnTimingTotalElapsedDecreaseCount: numberish(v.turnTimingTotalElapsedDecreaseCount, s.turnTimingTotalElapsedDecreaseCount), turnTimingTotalElapsedZeroResetCount: numberish(v.turnTimingTotalElapsedZeroResetCount, s.turnTimingTotalElapsedZeroResetCount), turnTimingTotalElapsedForwardJumpCount: numberish(v.turnTimingTotalElapsedForwardJumpCount, s.turnTimingTotalElapsedForwardJumpCount), turnTimingTerminalElapsedGrowthCount: numberish(v.turnTimingTerminalElapsedGrowthCount, s.turnTimingTerminalElapsedGrowthCount), turnTimingRecentUpdateJumpCount: numberish(v.turnTimingRecentUpdateJumpCount, v.turnTimingRecentUpdateSawtoothJumpCount, s.turnTimingRecentUpdateJumpCount, s.turnTimingRecentUpdateSawtoothJumpCount), turnTimingRecentUpdateMaxIncreaseSeconds: numberish(v.turnTimingRecentUpdateMaxIncreaseSeconds, s.turnTimingRecentUpdateMaxIncreaseSeconds), loading: compactLoading(v.loading), traceOrder: compactTraceOrder(v.traceOrder), sessionRailTitles: compactSessionRailTitles(v.sessionRailTitles), promptSegments: numberish(v.promptSegments, s.promptSegments), loadingSampleCount: numberish(v.loadingSampleCount, s.loadingSampleCount), loadingMaxCount: numberish(v.loadingMaxCount, s.loadingMaxCount), loadingMaxOwnerCount: numberish(v.loadingMaxOwnerCount, s.loadingMaxOwnerCount), loadingOwnerCount: numberish(v.loadingOwnerCount, s.loadingOwnerCount), loadingLongestContinuousSeconds: numberish(v.loadingLongestContinuousSeconds, s.loadingLongestContinuousSeconds), loadingCurrentContinuousSeconds: numberish(v.loadingCurrentContinuousSeconds, s.loadingCurrentContinuousSeconds), loadingOverFiveSecondSegmentCount: numberish(v.loadingOverFiveSecondSegmentCount, s.loadingOverFiveSecondSegmentCount) }; };",
|
|
"const stdoutJson = readJson(stdoutPath);",
|
|
"const reportJson = readJson(reportJsonPath);",
|
|
"const source = objectOrNull(stdoutJson) || objectOrNull(reportJson);",
|
|
"if (!source) { console.log(JSON.stringify({ ok: false, command: 'web-probe-observe analyze', stateDir, error: 'web-probe-analyzer-artifacts-missing', analyzer: { recoveredFrom: 'analysis-artifact-after-transport-timeout', stdoutPath, stderrPath, reportJsonPath, reportMdPath, stdoutBytes: statSize(stdoutPath), stderrBytes: statSize(stderrPath), reportJsonBytes: statSize(reportJsonPath), reportMdBytes: statSize(reportMdPath), transportExitCode: Number(transportExitRaw), transportTimedOut: transportTimedOutRaw === 'true', valuesRedacted: true }, valuesRedacted: true })); process.exit(0); }",
|
|
"const recent = objectOrNull(source.windows?.recent) || {};",
|
|
"const srcMetrics = objectOrNull(source.sampleMetrics) || objectOrNull(recent.sampleMetrics) || {};",
|
|
"const pagePerformance = objectOrNull(source.pagePerformance) || objectOrNull(recent.pagePerformance) || {};",
|
|
"const runtimeAlerts = objectOrNull(source.runtimeAlerts) || objectOrNull(recent.runtimeAlerts) || {};",
|
|
"const promptNetwork = objectOrNull(source.promptNetwork) || objectOrNull(recent.promptNetwork) || {};",
|
|
"const archiveSummary = objectOrNull(source.archiveSummary) || {};",
|
|
"const archiveSampleMetrics = objectOrNull(archiveSummary.sampleMetrics) || objectOrNull(source.sampleMetrics?.summary) || objectOrNull(srcMetrics.summary) || {};",
|
|
"const slowApis = arr(source.pagePerformanceSlowApi).length > 0 ? arr(source.pagePerformanceSlowApi) : arr(pagePerformance.sameOriginApiByPath).filter((item) => Number(item?.overBudgetCount ?? item?.overFiveSecondCount ?? 0) > 0);",
|
|
"const compact = { ok: source.ok === true, command: source.command ?? 'web-probe-observe analyze', stateDir: source.stateDir ?? stateDir, jsonlScope: source.jsonlScope ?? null, alertThresholds: source.alertThresholds ?? null, counts: source.counts ?? null, archiveSummary: { ...archiveSummary, sampleMetrics: archiveSampleMetrics, pagePerformance: objectOrNull(archiveSummary.pagePerformance) || objectOrNull(pagePerformance.summary) || {}, runtimeAlerts: objectOrNull(archiveSummary.runtimeAlerts) || objectOrNull(runtimeAlerts.summary) || {}, redFindings: arr(archiveSummary.redFindings).slice(0, 12).map(slimFinding) }, analysisWindow: source.analysisWindow ?? objectOrNull(recent.summary), sampleMetrics: compactMetrics(srcMetrics), pageProvenance: objectOrNull(source.pageProvenance?.summary) || source.pageProvenance ?? null, pagePerformance: objectOrNull(pagePerformance.summary) || pagePerformance, projectManagement: objectOrNull(source.projectManagement) || null, promptNetwork: objectOrNull(promptNetwork.summary) || promptNetwork, runtimeAlerts: objectOrNull(runtimeAlerts.summary) || runtimeAlerts, runnerErrors: arr(source.runnerErrors).slice(-8), commandFailures: arr(source.commandFailures).slice(-8), commandState: objectOrNull(source.commandState) || null, toolFindings: arr(source.toolFindings).slice(0, 8).map(slimFinding), httpErrorGroups: arr(source.httpErrorGroups ?? runtimeAlerts.networkHttpErrorsByPath).slice(0, 8).map(slimGroup), requestFailedGroups: arr(source.requestFailedGroups ?? runtimeAlerts.networkRequestFailedByPath).slice(0, 8).map(slimGroup), domDiagnosticGroups: arr(source.domDiagnosticGroups ?? runtimeAlerts.domDiagnosticsByText).slice(0, 5).map(slimGroup), domDiagnosticSamples: arr(source.domDiagnosticSamples ?? runtimeAlerts.domDiagnostics).slice(0, 8).map(slimGroup), consoleAlertGroups: arr(source.consoleAlertGroups ?? runtimeAlerts.consoleAlertsByPath).slice(0, 8).map(slimGroup), consoleAlertSamples: arr(source.consoleAlertSamples ?? runtimeAlerts.consoleAlerts).slice(0, 8).map(slimGroup), turnTimingRecentUpdateJumps: arr(source.turnTimingRecentUpdateJumps ?? srcMetrics.turnTimingRecentUpdateSawtoothJumps).slice(0, 8), turnTimingElapsedZeroResets: arr(source.turnTimingElapsedZeroResets ?? srcMetrics.turnTimingElapsedZeroResets).slice(0, 8), turnTimingTotalElapsedForwardJumps: arr(source.turnTimingTotalElapsedForwardJumps ?? srcMetrics.turnTimingTotalElapsedForwardJumps).slice(0, 8), pagePerformanceSlowApi: slowApis.slice(0, 8).map(slimSlowApi), archivePagePerformanceSlowApi: arr(source.archivePagePerformanceSlowApi).slice(0, 8).map(slimSlowApi), pagePerformancePartialApi: arr(source.pagePerformancePartialApi).slice(0, 8), pagePerformanceSseStreams: arr(source.pagePerformanceSseStreams).slice(0, 8), findings: arr(source.findings).slice(0, 12).map(slimFinding), archiveRedFindings: arr(source.archiveRedFindings ?? archiveSummary.redFindings).slice(0, 12).map(slimFinding), reportJsonPath: source.reportJsonPath ?? reportJsonPath, reportJsonSha256: source.reportJsonSha256 ?? sha256(reportJsonPath), reportMdPath: source.reportMdPath ?? reportMdPath, reportMdSha256: source.reportMdSha256 ?? sha256(reportMdPath), analyzer: { ...(objectOrNull(source.analyzer) || {}), recoveredFrom: 'analysis-artifact-after-transport-timeout', stdoutPath, stderrPath, stdoutBytes: statSize(stdoutPath), stderrBytes: statSize(stderrPath), reportJsonBytes: statSize(reportJsonPath), reportMdBytes: statSize(reportMdPath), transportExitCode: Number(transportExitRaw), transportTimedOut: transportTimedOutRaw === 'true', valuesRedacted: true }, valuesRedacted: true };",
|
|
"console.log(JSON.stringify(compact));",
|
|
"UNIDESK_WEB_OBSERVE_RECOVER_ANALYZE_ARTIFACT",
|
|
].join("\n");
|
|
const recovered = parseJsonObject(runTransWorkspaceStdinScript(options.node, spec.workspace, recoverScript, Math.min(options.commandTimeoutSeconds, 30)).stdout);
|
|
return recovered;
|
|
}
|