2460 lines
198 KiB
TypeScript
2460 lines
198 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 { resolveCliChildJsonObject } from "../cli-child-json-recovery";
|
|
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 HwlabRuntimeWebProbeAuthLoginSpec, type HwlabRuntimeWebProbeBrowserFreezePolicySpec, 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 { parseNodeWebProbeObserveCollectView, type NodeWebProbeObserveCollectView } from "../hwlab-node-web-observe-collect";
|
|
import { 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 WebProbeSentinelDashboardAction, type WebProbeSentinelOptions, type WebProbeSentinelReportView, type WebProbeSentinelSourceAuthority, type WebProbeSentinelSourceOverrideOptions } 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 { runWebProbeRemoteArtifactJob } from "../web-probe-remote-artifact";
|
|
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 { runNodeWebProbeObserveCommand, runNodeWebProbeObserveGc, runNodeWebProbeObserveStart, runNodeWebProbeObserveStatus } from "./web-probe-observe-actions";
|
|
import { runNodeWebProbeObserveCollect } from "./web-probe-observe-collect";
|
|
import { parseWebProbeOriginName, resolveNodeWebProbeCliOrigin, resolveNodeWebProbeOrigin, webProbeOriginSummary } from "./web-probe-origin";
|
|
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, nodeWebObserveForceStopNodeScript, nodeWebObserveStatusNodeScript, nodeWebObserveWaitCommandShell, runNodeWebProbeScript, safeWebObserveSegment, safeWebObserveTargetSegment } from "./web-observe-scripts";
|
|
import { displayRepoPath, readBootstrapAdminPasswordMaterial, sleepSync } from "./web-probe";
|
|
|
|
export { webObserveShort, webObserveText } from "./web-probe-observe-actions";
|
|
|
|
export function parseNodeWebProbeSentinelOptions(args: string[]): NodeWebProbeSentinelOptions {
|
|
const [sentinelActionRaw] = args;
|
|
if (
|
|
sentinelActionRaw !== "plan"
|
|
&& sentinelActionRaw !== "status"
|
|
&& sentinelActionRaw !== "image"
|
|
&& sentinelActionRaw !== "control-plane"
|
|
&& sentinelActionRaw !== "publish-current"
|
|
&& sentinelActionRaw !== "validate"
|
|
&& sentinelActionRaw !== "maintenance"
|
|
&& sentinelActionRaw !== "dashboard"
|
|
&& sentinelActionRaw !== "error"
|
|
&& sentinelActionRaw !== "report"
|
|
&& sentinelActionRaw !== "inspect-url"
|
|
&& sentinelActionRaw !== "inspect-id"
|
|
) {
|
|
throw new Error("web-probe sentinel usage: sentinel plan|status|image|control-plane|publish-current|validate|maintenance|dashboard|error|report|inspect-url|inspect-id --node NODE --lane vNN [--dry-run|--confirm]");
|
|
}
|
|
assertKnownOptions(args, new Set([
|
|
"--node",
|
|
"--lane",
|
|
"--timeout-seconds",
|
|
"--release-id",
|
|
"--reason",
|
|
"--view",
|
|
"--run",
|
|
"--run-id",
|
|
"--error-id",
|
|
"--trace-id",
|
|
"--sample-seq",
|
|
"--sentinel",
|
|
"--sentinel-id",
|
|
"--viewport",
|
|
"--local-dir",
|
|
"--name",
|
|
"--timeout-ms",
|
|
"--wait-timeout-ms",
|
|
"--command-timeout-seconds",
|
|
"--source-commit",
|
|
"--source-stage-ref",
|
|
"--source-mirror-commit",
|
|
"--source-authority",
|
|
]), new Set(["--dry-run", "--confirm", "--wait", "--rerun", "--manual-recovery", "--recovery", "--quick-verify", "--raw", "--full", "--latest", "--full-page", "--no-full-page", "--diagnose-monitor-web"]));
|
|
const nodeOption = optionValue(args, "--node") ?? null;
|
|
const laneOption = optionValue(args, "--lane") ?? null;
|
|
const inspectMode = sentinelActionRaw === "inspect-url" || sentinelActionRaw === "inspect-id";
|
|
let node = nodeOption ?? "";
|
|
let lane = laneOption ?? "";
|
|
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}`);
|
|
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);
|
|
const sourceOverride = parseWebProbeSentinelSourceOverride(args);
|
|
let sentinel: WebProbeSentinelOptions;
|
|
if (inspectMode) {
|
|
const input = args[1];
|
|
if (input === undefined || input.startsWith("--")) throw new Error(`web-probe sentinel ${sentinelActionRaw} requires a URL or id argument`);
|
|
const resolved = resolveWebProbeSentinelInspectTarget(input, sentinelActionRaw, nodeOption, laneOption, sentinelId);
|
|
node = resolved.node;
|
|
lane = resolved.lane;
|
|
sentinel = {
|
|
kind: "inspect",
|
|
action: sentinelActionRaw,
|
|
node,
|
|
lane,
|
|
sentinelId: resolved.sentinelId,
|
|
input,
|
|
view: parseWebProbeSentinelReportView(optionValue(args, "--view") ?? "summary"),
|
|
raw: args.includes("--raw"),
|
|
full: args.includes("--full"),
|
|
diagnoseMonitorWeb: args.includes("--diagnose-monitor-web"),
|
|
resolutionWarnings: resolved.warnings,
|
|
timeoutSeconds,
|
|
};
|
|
} else {
|
|
node = requiredOption(args, "--node");
|
|
lane = requiredOption(args, "--lane");
|
|
assertNodeId(node);
|
|
assertLane(lane);
|
|
if (!isHwlabRuntimeLane(lane)) throw new Error(`web-probe only supports HWLAB runtime lanes, got ${lane}`);
|
|
}
|
|
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, sourceOverride };
|
|
} 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, rerun: args.includes("--rerun"), sourceOverride };
|
|
} else if (sentinelActionRaw === "publish-current") {
|
|
const manualRecovery = args.includes("--manual-recovery") || args.includes("--recovery");
|
|
const reason = optionValue(args, "--reason") ?? null;
|
|
if (manualRecovery && (reason === null || reason.trim().length < 8)) throw new Error("web-probe sentinel publish-current manual recovery requires --reason with at least 8 characters");
|
|
sentinel = { kind: "publish", action: "publish-current", node, lane, sentinelId, dryRun: dryRun || !confirm, confirm, wait: args.includes("--wait"), timeoutSeconds, rerun: args.includes("--rerun"), manualRecovery, reason, sourceOverride };
|
|
} 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 if (sentinelActionRaw === "dashboard") {
|
|
const dashboardAction = parseWebProbeSentinelDashboardAction(args[1]);
|
|
const timeoutMs = positiveIntegerOption(args, "--timeout-ms", 30000, 120000);
|
|
const waitTimeoutMs = positiveIntegerOption(args, "--wait-timeout-ms", Math.max(60000, timeoutMs + 15000), 600000);
|
|
const commandTimeoutSeconds = positiveIntegerOption(args, "--command-timeout-seconds", Math.min(900, Math.max(120, Math.ceil(waitTimeoutMs / 1000) + 30)), 900);
|
|
const runId = optionValue(args, "--run") ?? optionValue(args, "--run-id") ?? null;
|
|
const errorId = optionValue(args, "--error-id") ?? null;
|
|
if (errorId !== null && !/^wbe_[a-f0-9]{20}$/u.test(errorId)) throw new Error(`web-probe sentinel dashboard --error-id must look like wbe_<20 hex>, got ${errorId}`);
|
|
sentinel = {
|
|
kind: "dashboard",
|
|
action: dashboardAction,
|
|
node,
|
|
lane,
|
|
sentinelId,
|
|
viewport: parseWebProbeSentinelDashboardViewport(optionValue(args, "--viewport") ?? "1440x900"),
|
|
localDir: optionValue(args, "--local-dir") ?? "/tmp",
|
|
name: optionValue(args, "--name") ?? null,
|
|
runId,
|
|
errorId,
|
|
timeoutMs,
|
|
waitTimeoutMs,
|
|
timeoutSeconds: commandTimeoutSeconds,
|
|
commandTimeoutSeconds,
|
|
fullPage: args.includes("--full-page") && !args.includes("--no-full-page"),
|
|
raw: args.includes("--raw"),
|
|
};
|
|
} else if (sentinelActionRaw === "error") {
|
|
const errorAction = args[1];
|
|
if (errorAction !== "get") throw new Error("web-probe sentinel error usage: error get --node NODE --lane vNN --sentinel ID --error-id wbe_...");
|
|
const errorId = requiredOption(args, "--error-id");
|
|
if (!/^wbe_[a-f0-9]{20}$/u.test(errorId)) throw new Error(`web-probe sentinel error get --error-id must look like wbe_<20 hex>, got ${errorId}`);
|
|
sentinel = { kind: "error", action: "get", node, lane, sentinelId, errorId, raw: args.includes("--raw"), full: args.includes("--full"), timeoutSeconds };
|
|
} else if (sentinelActionRaw === "report") {
|
|
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"),
|
|
full: args.includes("--full"),
|
|
timeoutSeconds,
|
|
};
|
|
}
|
|
return {
|
|
action: "sentinel",
|
|
sentinel,
|
|
node,
|
|
lane,
|
|
};
|
|
}
|
|
|
|
function resolveWebProbeSentinelInspectTarget(
|
|
input: string,
|
|
action: "inspect-url" | "inspect-id",
|
|
nodeOption: string | null,
|
|
laneOption: string | null,
|
|
sentinelOption: string | null,
|
|
): { readonly node: string; readonly lane: string; readonly sentinelId: string | null; readonly warnings: readonly string[] } {
|
|
if ((nodeOption === null) !== (laneOption === null)) throw new Error(`web-probe sentinel ${action} requires both --node and --lane when overriding target`);
|
|
if (nodeOption !== null && laneOption !== null) {
|
|
assertNodeId(nodeOption);
|
|
assertLane(laneOption);
|
|
if (!isHwlabRuntimeLane(laneOption)) throw new Error(`web-probe only supports HWLAB runtime lanes, got ${laneOption}`);
|
|
const spec = hwlabRuntimeLaneSpecForNode(laneOption, nodeOption);
|
|
const fallbackSentinel = spec.observability.webProbe?.monitorRoot?.sentinelId ?? null;
|
|
return { node: nodeOption, lane: laneOption, sentinelId: sentinelOption ?? fallbackSentinel, warnings: [] };
|
|
}
|
|
const candidates = webProbeMonitorRootCandidates();
|
|
const url = parseMonitorInspectUrl(input);
|
|
const urlSentinel = url === null ? null : sentinelIdFromMonitorUrl(url);
|
|
const desiredSentinel = sentinelOption ?? urlSentinel;
|
|
const matched = candidates.filter((candidate) => {
|
|
if (desiredSentinel !== null && candidate.sentinelId !== desiredSentinel) return false;
|
|
if (url === null) return true;
|
|
return monitorUrlMatchesRoot(url, candidate.publicBaseUrl);
|
|
});
|
|
if (matched.length === 0) {
|
|
const hint = candidates.slice(0, 8).map((candidate) => `${candidate.node}/${candidate.lane}/${candidate.sentinelId}`).join(", ");
|
|
throw new Error(`web-probe sentinel ${action} could not map input to a YAML monitorRoot; pass --node and --lane. candidates: ${hint}`);
|
|
}
|
|
const selected = matched.length === 1 ? matched[0] : preferredMonitorRootCandidate(matched);
|
|
if (selected === null) {
|
|
const hint = matched.slice(0, 8).map((candidate) => `--node ${candidate.node} --lane ${candidate.lane} --sentinel ${candidate.sentinelId}`).join(" | ");
|
|
throw new Error(`web-probe sentinel ${action} target is ambiguous; pass one target explicitly. candidates: ${hint}`);
|
|
}
|
|
const warnings = matched.length > 1
|
|
? [`multiple YAML monitorRoot entries matched; selected ${selected.node}/${selected.lane}/${selected.sentinelId} by source-of-truth preference`]
|
|
: [];
|
|
return { node: selected.node, lane: selected.lane, sentinelId: selected.sentinelId, warnings };
|
|
}
|
|
|
|
function webProbeMonitorRootCandidates(): readonly { readonly node: string; readonly lane: string; readonly sentinelId: string; readonly publicBaseUrl: string }[] {
|
|
const candidates: { node: string; lane: string; sentinelId: string; publicBaseUrl: string }[] = [];
|
|
for (const lane of hwlabRuntimeLaneIds()) {
|
|
for (const node of hwlabRuntimeNodeIds()) {
|
|
let spec: HwlabRuntimeLaneSpec;
|
|
try {
|
|
spec = hwlabRuntimeLaneSpecForNode(lane, node);
|
|
} catch {
|
|
continue;
|
|
}
|
|
const root = spec.observability.webProbe?.monitorRoot;
|
|
if (root?.enabled !== true) continue;
|
|
candidates.push({ node: spec.nodeId, lane: spec.lane, sentinelId: root.sentinelId, publicBaseUrl: root.publicBaseUrl });
|
|
}
|
|
}
|
|
return candidates;
|
|
}
|
|
|
|
function parseMonitorInspectUrl(input: string): URL | null {
|
|
try {
|
|
return new URL(input);
|
|
} catch {
|
|
return null;
|
|
}
|
|
}
|
|
|
|
function sentinelIdFromMonitorUrl(url: URL): string | null {
|
|
const query = url.searchParams.get("sentinel") ?? url.searchParams.get("sentinelId");
|
|
if (query !== null && /^[a-z0-9][a-z0-9-]{1,80}$/u.test(query)) return query;
|
|
const match = /\/sentinels\/([a-z0-9][a-z0-9-]{1,80})(?:\/|$)/u.exec(url.pathname);
|
|
return match?.[1] ?? null;
|
|
}
|
|
|
|
function monitorUrlMatchesRoot(url: URL, publicBaseUrl: string): boolean {
|
|
let base: URL;
|
|
try {
|
|
base = new URL(publicBaseUrl);
|
|
} catch {
|
|
return false;
|
|
}
|
|
if (url.origin !== base.origin) return false;
|
|
const basePath = base.pathname.replace(/\/+$/u, "");
|
|
if (basePath.length === 0) return true;
|
|
return url.pathname === basePath || url.pathname.startsWith(`${basePath}/`);
|
|
}
|
|
|
|
function preferredMonitorRootCandidate(candidates: readonly { readonly node: string; readonly lane: string; readonly sentinelId: string }[]): { readonly node: string; readonly lane: string; readonly sentinelId: string } | null {
|
|
return candidates.find((candidate) => candidate.node === "JD01" && candidate.lane === "v03" && candidate.sentinelId === "jd01-web-probe-sentinel")
|
|
?? candidates.find((candidate) => candidate.node === "JD01" && candidate.lane === "v03")
|
|
?? null;
|
|
}
|
|
|
|
function parseWebProbeSentinelSourceOverride(args: string[]): WebProbeSentinelSourceOverrideOptions {
|
|
const sourceCommit = optionValue(args, "--source-commit") ?? null;
|
|
const sourceStageRef = optionValue(args, "--source-stage-ref") ?? null;
|
|
const sourceMirrorCommit = optionValue(args, "--source-mirror-commit") ?? null;
|
|
const sourceAuthorityRaw = optionValue(args, "--source-authority") ?? null;
|
|
if (sourceCommit !== null && !/^[0-9a-f]{40}$/iu.test(sourceCommit)) throw new Error(`--source-commit must be a full git sha, got ${sourceCommit}`);
|
|
if (sourceMirrorCommit !== null && !/^[0-9a-f]{40}$/iu.test(sourceMirrorCommit)) throw new Error(`--source-mirror-commit must be a full git sha, got ${sourceMirrorCommit}`);
|
|
if (sourceStageRef !== null && !sourceStageRef.startsWith("refs/")) throw new Error(`--source-stage-ref must be a git ref, got ${sourceStageRef}`);
|
|
let sourceAuthority: WebProbeSentinelSourceAuthority | null = null;
|
|
if (sourceAuthorityRaw !== null) {
|
|
if (sourceAuthorityRaw !== "git-mirror-snapshot" && sourceAuthorityRaw !== "gitea-snapshot") throw new Error("--source-authority must be git-mirror-snapshot or gitea-snapshot");
|
|
sourceAuthority = sourceAuthorityRaw;
|
|
}
|
|
return { sourceCommit, sourceStageRef, sourceMirrorCommit, sourceAuthority };
|
|
}
|
|
|
|
function parseWebProbeSentinelDashboardAction(value: string | undefined): WebProbeSentinelDashboardAction {
|
|
if (value === "verify" || value === "screenshot" || value === "trigger") return value;
|
|
throw new Error("web-probe sentinel dashboard usage: dashboard verify|screenshot|trigger --node NODE --lane vNN --sentinel <id>");
|
|
}
|
|
|
|
function parseWebProbeSentinelDashboardViewport(value: string): string {
|
|
if (!/^[1-9][0-9]{1,4}x[1-9][0-9]{1,4}$/u.test(value)) throw new Error(`web-probe sentinel dashboard --viewport must look like 1440x900, got ${value}`);
|
|
const [widthRaw, heightRaw] = value.split("x");
|
|
const width = Number(widthRaw);
|
|
const height = Number(heightRaw);
|
|
if (width < 240 || width > 7680 || height < 240 || height > 4320) throw new Error(`web-probe sentinel dashboard --viewport out of range: ${value}`);
|
|
return value;
|
|
}
|
|
|
|
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"
|
|
&& observeActionRaw !== "gc"
|
|
) {
|
|
throw new Error("web-probe observe usage: observe start --node NODE --lane vNN [...]; observe status|command|stop|collect|analyze <id> [...]; observe gc --node NODE --lane vNN [--confirm]");
|
|
}
|
|
assertKnownOptions(args.slice(1), new Set([
|
|
"--node",
|
|
"--lane",
|
|
"--origin",
|
|
"--url",
|
|
"--target-path",
|
|
"--viewport",
|
|
"--browser-proxy-mode",
|
|
"--sample-interval-ms",
|
|
"--screenshot-interval-ms",
|
|
"--observer-refresh-interval-ms",
|
|
"--max-samples",
|
|
"--max-run-seconds",
|
|
"--keep-hours",
|
|
"--limit",
|
|
"--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",
|
|
"--second-text",
|
|
"--path",
|
|
"--label",
|
|
"--session-id",
|
|
"--provider",
|
|
"--profile",
|
|
"--account-id",
|
|
"--account",
|
|
"--from-account-id",
|
|
"--to-account-id",
|
|
"--after-round",
|
|
"--severity",
|
|
"--alternate-session-strategy",
|
|
"--expected-sentinel-range",
|
|
"--expected-action-wait-ms",
|
|
"--duration-ms",
|
|
"--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", "--wait-project-management-ready", "--blocking", "--non-blocking", "--dry-run", "--confirm"]));
|
|
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" && observeActionRaw !== "gc" && stateDir === null && jobId === null) {
|
|
throw new Error("web-probe observe status|command|stop|collect|analyze requires --state-dir or --job-id");
|
|
}
|
|
const requestedOrigin = optionValue(args, "--origin");
|
|
const customUrl = optionValue(args, "--url");
|
|
const browserProxyModeRaw = optionValue(args, "--browser-proxy-mode");
|
|
if (observeActionRaw !== "start" && (requestedOrigin !== undefined || customUrl !== undefined || browserProxyModeRaw !== undefined)) {
|
|
throw new Error("web-probe observe --origin/--url/--browser-proxy-mode is only valid for start; later actions inherit the observer origin");
|
|
}
|
|
const requestedBrowserProxyMode = browserProxyModeRaw === undefined ? undefined : parseWebProbeBrowserProxyMode(browserProxyModeRaw);
|
|
const selectedOrigin = observeActionRaw === "start"
|
|
? resolveNodeWebProbeCliOrigin(spec, requestedOrigin, customUrl, requestedBrowserProxyMode)
|
|
: {
|
|
originName: indexed?.originName ?? "custom" as const,
|
|
originMode: indexed?.originMode ?? "custom-url" as const,
|
|
originConfigPath: indexed?.originConfigPath ?? null,
|
|
url: indexed?.url || spec.publicWebUrl,
|
|
browserProxyMode: requestedBrowserProxyMode ?? indexed?.browserProxyMode ?? spec.webProbe?.browserProxyMode ?? "auto" as const,
|
|
browserProxyModeSource: requestedBrowserProxyMode === undefined
|
|
? indexed?.browserProxyModeSource ?? (spec.webProbe?.browserProxyMode === undefined ? "default" as const : "yaml-web-probe" as const)
|
|
: "cli" as const,
|
|
};
|
|
const confirm = args.includes("--confirm");
|
|
const dryRun = args.includes("--dry-run") || !confirm;
|
|
if (confirm && args.includes("--dry-run")) throw new Error("web-probe observe gc accepts only one of --confirm or --dry-run");
|
|
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 commandSecondText = optionValue(args, "--second-text") ?? null;
|
|
const commandSourceId = optionValue(args, "--source-id") ?? null;
|
|
const commandProfile = optionValue(args, "--profile") ?? null;
|
|
const commandProvider = optionValue(args, "--provider") ?? 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 commandExpectedActionWaitMsRaw = optionValue(args, "--expected-action-wait-ms") ?? null;
|
|
const commandExpectedActionWaitMs = commandExpectedActionWaitMsRaw === null ? null : Number(commandExpectedActionWaitMsRaw);
|
|
if (commandExpectedActionWaitMs !== null && (!Number.isInteger(commandExpectedActionWaitMs) || commandExpectedActionWaitMs < 1000 || commandExpectedActionWaitMs > 600000)) {
|
|
throw new Error("unsafe web-probe observe --expected-action-wait-ms: expected integer 1000-600000");
|
|
}
|
|
const commandDurationMsRaw = optionValue(args, "--duration-ms") ?? null;
|
|
const commandDurationMs = commandDurationMsRaw === null ? null : Number(commandDurationMsRaw);
|
|
if (commandDurationMs !== null && (!Number.isInteger(commandDurationMs) || commandDurationMs < 100 || commandDurationMs > 600000)) {
|
|
throw new Error("unsafe web-probe observe --duration-ms: expected integer 100-600000");
|
|
}
|
|
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],
|
|
["--profile", commandProfile],
|
|
["--second-text", commandSecondText],
|
|
["--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`);
|
|
}
|
|
if (observeActionRaw === "command" && commandType === "validateRealtimeFanout") {
|
|
if (!commandProfile?.trim()) throw new Error("validateRealtimeFanout requires --profile");
|
|
if (spec.webProbe?.realtimeFanoutProfiles?.[commandProfile.trim()] === undefined) {
|
|
throw new Error(`validateRealtimeFanout profile is not declared by the owning YAML: ${commandProfile.trim()}`);
|
|
}
|
|
if (!commandProvider?.trim()) throw new Error("validateRealtimeFanout requires --provider");
|
|
if (!commandText?.trim()) throw new Error("validateRealtimeFanout requires --text or --text-stdin for the first turn");
|
|
if (!commandSecondText?.trim()) throw new Error("validateRealtimeFanout requires --second-text for the follow-up turn");
|
|
}
|
|
if (observeActionRaw === "command" && commandType === "validateWorkbenchKafkaDebugReplay") {
|
|
if (spec.webProbe?.workbenchKafkaDebugReplay?.enabled !== true) {
|
|
throw new Error("validateWorkbenchKafkaDebugReplay is not enabled by the owning YAML");
|
|
}
|
|
}
|
|
if (observeActionRaw === "command" && commandType === "validateWorkbenchTraceReadability") {
|
|
if (spec.webProbe?.workbenchTraceReadability?.enabled !== true) {
|
|
throw new Error("owning YAML 未启用 validateWorkbenchTraceReadability");
|
|
}
|
|
}
|
|
return {
|
|
action: "observe",
|
|
observeAction: observeActionRaw,
|
|
id: observeId ?? jobId,
|
|
node,
|
|
lane,
|
|
url: selectedOrigin.url,
|
|
originName: selectedOrigin.originName,
|
|
originMode: selectedOrigin.originMode,
|
|
originConfigPath: selectedOrigin.originConfigPath,
|
|
targetPath: optionValue(args, "--target-path") ?? "/workbench",
|
|
viewport: optionValue(args, "--viewport") ?? "1440x900",
|
|
browserProxyMode: selectedOrigin.browserProxyMode,
|
|
browserProxyModeSource: selectedOrigin.browserProxyModeSource,
|
|
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),
|
|
maxRunSeconds: positiveIntegerOption(args, "--max-run-seconds", 0, 86_400),
|
|
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),
|
|
gcKeepHours: positiveIntegerOption(args, "--keep-hours", webObserveGcDefaultKeepHours(), 8760),
|
|
gcLimit: positiveIntegerOption(args, "--limit", 200, 5000),
|
|
confirm,
|
|
dryRun,
|
|
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,
|
|
commandSecondText,
|
|
commandPath: optionValue(args, "--path") ?? null,
|
|
commandLabel: optionValue(args, "--label") ?? null,
|
|
commandSessionId: optionValue(args, "--session-id") ?? null,
|
|
commandProvider,
|
|
commandProfile,
|
|
commandAfterRound,
|
|
commandSeverity,
|
|
commandAlternateSessionStrategy,
|
|
commandExpectedSentinelRange,
|
|
commandExpectedActionWaitMs,
|
|
commandDurationMs,
|
|
commandRequireComposerReady: args.includes("--require-composer-ready"),
|
|
commandWaitProjectManagementReady: args.includes("--wait-project-management-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 === "validateRealtimeFanout"
|
|
|| value === "validateWorkbenchKafkaDebugReplay"
|
|
|| value === "validateWorkbenchTraceReadability"
|
|
|| 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 === "closeMdtodoSourceConfig"
|
|
|| 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 === "performanceCapture"
|
|
|| 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, validateRealtimeFanout, validateWorkbenchKafkaDebugReplay, validateWorkbenchTraceReadability, steer, cancel, selectProvider, clickSession, refreshCurrentSession, switchAwayAndBack, assertSessionInvariant, selectProjectSource, selectMdtodoSource, selectMdtodoFile, selectMdtodoTask, expandMdtodoTask, openMdtodoReportPreview, toggleMdtodoReportFullscreen, openMdtodoSourceConfig, closeMdtodoSourceConfig, configureMdtodoHwpodSource, probeMdtodoSource, reindexMdtodoSource, editMdtodoTaskInline, editMdtodoTaskTitle, editMdtodoTaskBody, toggleMdtodoTaskStatus, addMdtodoRootTask, addMdtodoSubTask, continueMdtodoTask, deleteMdtodoTask, launchWorkbenchFromTask, launchWorkbenchFromMdtodo, performanceCapture, 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 = parseWebProbeOriginName(spec.webProbe?.defaultOrigin ?? "public");
|
|
return resolveNodeWebProbeOrigin(spec, origin).url;
|
|
}
|
|
|
|
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}`);
|
|
}
|
|
}
|
|
|
|
function webObserveGcDefaultKeepHours(): number {
|
|
const configPath = "config/unidesk-cli.yaml";
|
|
const parsed = record(Bun.YAML.parse(readFileSync(rootPath(configPath), "utf8")) as unknown);
|
|
const gc = record(parsed.gc);
|
|
const scratch = record(gc.stateStaleScratch);
|
|
const keepHours = scratch.keepHours;
|
|
if (!Number.isInteger(keepHours) || keepHours < 0) {
|
|
throw new Error(`${configPath}#gc.stateStaleScratch.keepHours must be a non-negative integer`);
|
|
}
|
|
return keepHours;
|
|
}
|
|
|
|
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 === "script" && typeof options.commandLabel === "string"
|
|
? options.commandLabel
|
|
: 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,
|
|
origin: webProbeOriginSummary(options),
|
|
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, options.browserProxyMode);
|
|
const script = [
|
|
"set -eu",
|
|
`${[...webProbeProxy.envAssignments, `HWLAB_WEB_USER=${shellQuote(material.username ?? 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,
|
|
origin: webProbeOriginSummary(options),
|
|
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 job = runWebProbeRemoteArtifactJob({
|
|
route,
|
|
localDir: options.localDir,
|
|
waitTimeoutMs: options.waitTimeoutMs,
|
|
commandTimeoutMs: options.commandTimeoutSeconds * 1000,
|
|
inactivityTimeoutMs: 30000,
|
|
keepRemote: options.keepRemote,
|
|
runIdPrefix: `web-probe-screenshot-${options.node.toLowerCase()}-${options.lane}`,
|
|
}, script);
|
|
const result = job.result;
|
|
const transport = record(job.transport);
|
|
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,
|
|
origin: webProbeOriginSummary(options),
|
|
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_WEB_PROBE_ARTIFACT_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_WEB_PROBE_ARTIFACT_REMOTE_DIR/web-probe-screenshot.mjs\" <<'WEB_PROBE_SCREENSHOT_JS'",
|
|
webProbeScreenshotRemoteModule(),
|
|
"WEB_PROBE_SCREENSHOT_JS",
|
|
"bun \"$UNIDESK_WEB_PROBE_ARTIFACT_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, options.browserProxyMode);
|
|
const startScript = [
|
|
"set -eu",
|
|
`${[...webProbeProxy.envAssignments, `HWLAB_WEB_USER=${shellQuote(material.username ?? 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,
|
|
origin: webProbeOriginSummary(options),
|
|
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,
|
|
origin: webProbeOriginSummary(options),
|
|
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: material.username === null ? secretSpec.bootstrapAdminUsername : "<source-ref>",
|
|
usernameSourceRef: material.usernameSourceRef,
|
|
usernameSourceKey: material.usernameSourceKey,
|
|
usernameSourceLine: material.usernameSourceLine,
|
|
usernameFingerprint: material.usernameFingerprint,
|
|
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 nodeWebProbeBrowserFreezePolicy(spec: HwlabRuntimeLaneSpec): HwlabRuntimeWebProbeBrowserFreezePolicySpec {
|
|
const policy = spec.webProbe?.browserFreezePolicy;
|
|
if (policy === undefined) {
|
|
throw new Error(`${hwlabRuntimeLaneConfigPath()} node=${spec.nodeId} lane=${spec.lane} requires webProbe.browserFreezePolicy for web-probe observe`);
|
|
}
|
|
return policy;
|
|
}
|
|
|
|
export function nodeWebProbeProjectManagementConfig(spec: HwlabRuntimeLaneSpec): HwlabRuntimeWebProbeProjectManagementSpec | null {
|
|
return spec.webProbe?.projectManagement ?? null;
|
|
}
|
|
|
|
export function nodeWebProbeAuthLoginConfig(spec: HwlabRuntimeLaneSpec): HwlabRuntimeWebProbeAuthLoginSpec | null {
|
|
return spec.webProbe?.authLogin ?? 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);
|
|
if (options.observeAction === "gc") return runNodeWebProbeObserveGc(options, spec);
|
|
return runNodeWebProbeObserveAnalyze(options, spec);
|
|
}
|
|
|
|
export function runNodeWebProbeObserveAnalyze(options: NodeWebProbeObserveOptions, spec: HwlabRuntimeLaneSpec): Record<string, unknown> | RenderedCliResult {
|
|
const analyzerB64 = Buffer.from(nodeWebObserveAnalyzerSource(), "utf8").toString("base64");
|
|
const alertThresholds = nodeWebProbeAlertThresholds(spec);
|
|
const browserFreezePolicy = nodeWebProbeBrowserFreezePolicy(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_BROWSER_FREEZE_POLICY_JSON=${shellQuote(JSON.stringify(browserFreezePolicy))}`,
|
|
`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_BROWSER_FREEZE_POLICY_JSON=\"$UNIDESK_WEB_OBSERVE_BROWSER_FREEZE_POLICY_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 evidenceSummary = (value) => { const v = objectOrNull(value); if (!v) return null; const out = {}; for (const key of ['http404Count','responseErrorCount','requestFailedCount','statuses','afterProjectedSeqs','sinceSeqs','traceIds','maxFallbackRatio','maxFallbackTitleCount','overThresholdSampleCount','majorityFallbackSampleCount']) if (v[key] !== undefined && v[key] !== null) out[key] = Array.isArray(v[key]) ? v[key].slice(0,6) : v[key]; const text = Object.keys(out).length > 0 ? JSON.stringify(out) : null; return text ? clip(text, 220) : null; };",
|
|
"const slimFinding = (item) => { const v = objectOrNull(item) || {}; return { kind: clip(v.kind ?? v.id ?? v.code, 160), code: clip(v.code ?? v.id ?? v.kind, 160), 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, timingSourceOfTruth: clip(v.timingSourceOfTruth ?? v.expectedElapsedSource ?? v.evidenceKind, 80), timingStatus: clip(v.timingStatus, 48), timingAlert: v.timingAlert === true, summary: clip(v.summary ?? v.message, 180), message: clip(v.message ?? v.summary, 180), rootCause: clip(v.rootCause, 120), rootCauseStatus: clip(v.rootCauseStatus, 80), rootCauseConfidence: clip(v.rootCauseConfidence, 40), nextAction: clip(v.nextAction, 220), evidenceSummary: evidenceSummary(v.evidence) ?? clip(v.evidenceSummary, 220) }; };",
|
|
"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 slimRequestBucket = (item) => { const v = objectOrNull(item) || {}; return { t: v.startAt ?? null, e: v.endAt ?? null, c: v.count ?? null, rpm: v.requestPerMinute ?? null }; };",
|
|
"const slimRequestCurve = (item, isApi = false) => { const v = objectOrNull(item) || {}; return { pageKey: clip(v.pageKey, 96), pageRole: clip(v.pageRole, 24), pageId: clip(v.pageId, 32), pageEpoch: v.pageEpoch ?? null, apiKey: clip(v.apiKey, 120), method: clip(v.method, 12), path: clip(v.path, isApi ? 120 : 96), count: v.count ?? null, bucketCount: v.bucketCount ?? null, peakRequestPerMinute: v.peakRequestPerMinute ?? null, peakBucket: slimRequestBucket(v.peakBucket), buckets: takeTail(v.buckets, 12).map(slimRequestBucket), valuesRedacted: true }; };",
|
|
"const slimRequestPeak = (item) => { const v = objectOrNull(item) || {}; return { scope: clip(v.scope, 16), thresholdPerMinute: v.thresholdPerMinute ?? null, bucketMs: v.bucketMs ?? null, startAt: v.startAt ?? null, endAt: v.endAt ?? null, count: v.count ?? null, requestPerMinute: v.requestPerMinute ?? null, pageKey: clip(v.pageKey, 96), path: clip(v.path, 96), apiKey: clip(v.apiKey, 120), valuesRedacted: true }; };",
|
|
"const slimRequestRateCurve = (value) => { const v = objectOrNull(value); if (!v) return null; return { summary: objectOrNull(v.summary) || v, buckets: takeTail(v.buckets, 12).map(slimRequestBucket), pageCurves: takeHead(v.pageCurves, 8).map((item) => slimRequestCurve(item, false)), apiPathCurves: takeHead(v.apiPathCurves, 12).map((item) => slimRequestCurve(item, true)), peaks: takeHead(v.peaks, 12).map(slimRequestPeak), valuesRedacted: true }; };",
|
|
"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, sessionCreatePresent: readiness.sessionCreatePresent === true, sessionCreateVisible: readiness.sessionCreateVisible === true, sessionRailPresent: readiness.sessionRailPresent === true, sessionRailCollapsed: readiness.sessionRailCollapsed ?? null, sessionCollapseTogglePresent: readiness.sessionCollapseTogglePresent === true, sessionCollapseToggleVisible: readiness.sessionCollapseToggleVisible === true, sessionCollapseToggleExpanded: readiness.sessionCollapseToggleExpanded ?? null, 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 details = objectOrNull(error.details) || {}; const attempts = Array.isArray(error.attempts) ? error.attempts : Array.isArray(details.attempts) ? details.attempts : []; const lastAttempt = attempts.length > 0 ? objectOrNull(attempts[attempts.length - 1]) || {} : {}; const rawReadiness = objectOrNull(lastAttempt.readiness) || objectOrNull(lastAttempt.readinessBeforeClick) || objectOrNull(details.readinessBeforeClick) || objectOrNull(details.readinessAfterWait) || 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 runtimeAlertSummary = objectOrNull(srcRuntimeAlerts?.summary);",
|
|
"const runtimeAlerts = srcRuntimeAlerts ? { httpErrorCount: srcRuntimeAlerts.httpErrorCount ?? runtimeAlertSummary?.httpErrorCount ?? null, requestFailedCount: srcRuntimeAlerts.requestFailedCount ?? runtimeAlertSummary?.requestFailedCount ?? null, domDiagnosticSampleCount: srcRuntimeAlerts.domDiagnosticSampleCount ?? runtimeAlertSummary?.domDiagnosticSampleCount ?? null, consoleAlertCount: srcRuntimeAlerts.consoleAlertCount ?? runtimeAlertSummary?.consoleAlertCount ?? null, webPerformancePayloadRequestCount: srcRuntimeAlerts.webPerformancePayloadRequestCount ?? runtimeAlertSummary?.webPerformancePayloadRequestCount ?? null, webPerformancePayloadParsedCount: srcRuntimeAlerts.webPerformancePayloadParsedCount ?? runtimeAlertSummary?.webPerformancePayloadParsedCount ?? null, webPerformancePayloadParseIssueCount: srcRuntimeAlerts.webPerformancePayloadParseIssueCount ?? runtimeAlertSummary?.webPerformancePayloadParseIssueCount ?? null, webPerformanceRuntimeDiagnosticCount: srcRuntimeAlerts.webPerformanceRuntimeDiagnosticCount ?? runtimeAlertSummary?.webPerformanceRuntimeDiagnosticCount ?? null, webPerformanceRuntimeDiagnosticGroupCount: srcRuntimeAlerts.webPerformanceRuntimeDiagnosticGroupCount ?? runtimeAlertSummary?.webPerformanceRuntimeDiagnosticGroupCount ?? null } : null;",
|
|
"const slimWebPerformanceGroup = (item) => { const v = objectOrNull(item) || {}; return { diagnosticCode: clip(v.diagnosticCode, 80), reason: clip(v.reason, 60), module: clip(v.module, 80), eventType: clip(v.eventType, 40), count: v.count ?? null, eventCount: v.eventCount ?? null, deliveredCount: v.deliveredCount ?? null, chunkCount: v.chunkCount ?? null, droppedCount: v.droppedCount ?? null, maxFlushDurationMs: v.maxFlushDurationMs ?? null, maxItemsPerChunk: v.maxItemsPerChunk ?? null, maxChunkMs: v.maxChunkMs ?? null, replacedByKeyCount: v.replacedByKeyCount ?? null, firstAt: v.firstAt ?? null, lastAt: v.lastAt ?? null, valuesRedacted: true }; };",
|
|
"const webPerformancePayloadStates = objectOrNull(srcRuntimeAlerts?.webPerformancePayloadStates) || objectOrNull(fullRecentWindow?.runtimeAlerts?.webPerformancePayloadStates) || objectOrNull(fullSource?.runtimeAlerts?.webPerformancePayloadStates);",
|
|
"const webPerformanceRuntimeDiagnostics = { summary: runtimeAlerts ? { payloadRequestCount: runtimeAlerts.webPerformancePayloadRequestCount ?? 0, payloadParsedCount: runtimeAlerts.webPerformancePayloadParsedCount ?? 0, payloadParseIssueCount: runtimeAlerts.webPerformancePayloadParseIssueCount ?? 0, runtimeDiagnosticCount: runtimeAlerts.webPerformanceRuntimeDiagnosticCount ?? 0, runtimeDiagnosticGroupCount: runtimeAlerts.webPerformanceRuntimeDiagnosticGroupCount ?? 0, capturedEventCount: webPerformancePayloadStates?.capturedEventCount ?? null, storedEventCount: webPerformancePayloadStates?.storedEventCount ?? null, valuesRedacted: true } : null, states: takeHead(firstNonEmptyArray(webPerformancePayloadStates?.states), 6), groups: takeHead(firstNonEmptyArray(srcRuntimeAlerts?.webPerformanceRuntimeDiagnosticsByCode, fullRecentWindow?.runtimeAlerts?.webPerformanceRuntimeDiagnosticsByCode, fullSource?.runtimeAlerts?.webPerformanceRuntimeDiagnosticsByCode), 6).map(slimWebPerformanceGroup), valuesRedacted: true };",
|
|
"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 sourceRequestRate = objectOrNull(source?.requestRate);",
|
|
"const requestRateCurve = slimRequestRateCurve(source?.requestRateCurve) || slimRequestRateCurve(sourceRequestRate) || slimRequestRateCurve(fullRecentWindow?.requestRate) || slimRequestRateCurve(fullSource?.requestRate);",
|
|
"const requestRateSummary = objectOrNull(requestRateCurve?.summary) || objectOrNull(sourceRequestRate?.summary) || sourceRequestRate || null;",
|
|
"const requestRatePeaks = takeHead(firstNonEmptyArray(source?.requestRatePeaks, requestRateCurve?.peaks, sourceRequestRate?.peaks, fullRecentWindow?.requestRate?.peaks, fullSource?.requestRate?.peaks), 12).map(slimRequestPeak);",
|
|
"const frontendPerformance = objectOrNull(source?.frontendPerformance) || objectOrNull(fullRecentWindow?.frontendPerformance) || objectOrNull(fullSource?.frontendPerformance) || null;",
|
|
"const frontendPerformanceSummary = objectOrNull(frontendPerformance?.summary) || frontendPerformance;",
|
|
"const frontendPerformanceHotspots = { scripts: takeHead(firstNonEmptyArray(source?.frontendPerformanceHotspots?.scripts, frontendPerformance?.scriptHotspots, fullRecentWindow?.frontendPerformance?.scriptHotspots, fullSource?.frontendPerformance?.scriptHotspots), 8), profileFunctions: takeHead(firstNonEmptyArray(source?.frontendPerformanceHotspots?.profileFunctions, frontendPerformance?.profileHotspots, fullRecentWindow?.frontendPerformance?.profileHotspots, fullSource?.frontendPerformance?.profileHotspots), 8), profileStacks: takeHead(firstNonEmptyArray(source?.frontendPerformanceHotspots?.profileStacks, frontendPerformance?.profileStacks, fullRecentWindow?.frontendPerformance?.profileStacks, fullSource?.frontendPerformance?.profileStacks), 5), valuesRedacted: true };",
|
|
"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,",
|
|
" webPerformanceRuntimeDiagnostics,",
|
|
" pagePerformance,",
|
|
" projectManagement,",
|
|
" promptNetwork,",
|
|
" requestRate: requestRateSummary,",
|
|
" requestRateCurve,",
|
|
" requestRatePeaks,",
|
|
" frontendPerformance: frontendPerformanceSummary,",
|
|
" frontendPerformanceHotspots,",
|
|
" 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,",
|
|
" webPerformanceRuntimeDiagnostics: compact.webPerformanceRuntimeDiagnostics ?? null,",
|
|
" pagePerformance: compact.pagePerformance ?? null,",
|
|
" requestRate: compact.requestRate ?? null,",
|
|
" requestRateCurve: compact.requestRateCurve ? { summary: compact.requestRateCurve.summary ?? null, buckets: Array.isArray(compact.requestRateCurve.buckets) ? compact.requestRateCurve.buckets.slice(-6) : [], pageCurves: Array.isArray(compact.requestRateCurve.pageCurves) ? compact.requestRateCurve.pageCurves.slice(0, 4).map((item) => ({ pageKey: item.pageKey ?? null, path: item.path ?? null, count: item.count ?? null, peakRequestPerMinute: item.peakRequestPerMinute ?? null, peakBucket: item.peakBucket ?? null, buckets: Array.isArray(item.buckets) ? item.buckets.slice(-6) : [] })) : [], apiPathCurves: Array.isArray(compact.requestRateCurve.apiPathCurves) ? compact.requestRateCurve.apiPathCurves.slice(0, 6).map((item) => ({ apiKey: item.apiKey ?? null, path: item.path ?? null, count: item.count ?? null, peakRequestPerMinute: item.peakRequestPerMinute ?? null, peakBucket: item.peakBucket ?? null, buckets: Array.isArray(item.buckets) ? item.buckets.slice(-6) : [] })) : [], valuesRedacted: true } : null,",
|
|
" requestRatePeaks: Array.isArray(compact.requestRatePeaks) ? compact.requestRatePeaks.slice(0, 6) : [],",
|
|
" frontendPerformance: compact.frontendPerformance ?? null,",
|
|
" frontendPerformanceHotspots: compact.frontendPerformanceHotspots ? { scripts: Array.isArray(compact.frontendPerformanceHotspots.scripts) ? compact.frontendPerformanceHotspots.scripts.slice(0, 6) : [], profileFunctions: Array.isArray(compact.frontendPerformanceHotspots.profileFunctions) ? compact.frontendPerformanceHotspots.profileFunctions.slice(0, 6) : [], profileStacks: Array.isArray(compact.frontendPerformanceHotspots.profileStacks) ? compact.frontendPerformanceHotspots.profileStacks.slice(0, 4) : [], valuesRedacted: true } : 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,",
|
|
" webPerformanceRuntimeDiagnostics: compact.webPerformanceRuntimeDiagnostics ?? null,",
|
|
" pagePerformance: compact.pagePerformance ?? null,",
|
|
" requestRate: compact.requestRate ?? null,",
|
|
" requestRateCurve: compact.requestRateCurve ? { summary: compact.requestRateCurve.summary ?? null, buckets: Array.isArray(compact.requestRateCurve.buckets) ? compact.requestRateCurve.buckets.slice(-6) : [], pageCurves: Array.isArray(compact.requestRateCurve.pageCurves) ? compact.requestRateCurve.pageCurves.slice(0, 3).map((item) => ({ pageKey: item.pageKey ?? null, path: item.path ?? null, count: item.count ?? null, peakRequestPerMinute: item.peakRequestPerMinute ?? null, peakBucket: item.peakBucket ?? null, buckets: Array.isArray(item.buckets) ? item.buckets.slice(-6) : [] })) : [], apiPathCurves: Array.isArray(compact.requestRateCurve.apiPathCurves) ? compact.requestRateCurve.apiPathCurves.slice(0, 4).map((item) => ({ apiKey: item.apiKey ?? null, path: item.path ?? null, count: item.count ?? null, peakRequestPerMinute: item.peakRequestPerMinute ?? null, peakBucket: item.peakBucket ?? null, buckets: Array.isArray(item.buckets) ? item.buckets.slice(-6) : [] })) : [], valuesRedacted: true } : null,",
|
|
" requestRatePeaks: Array.isArray(compact.requestRatePeaks) ? compact.requestRatePeaks.slice(0, 4) : [],",
|
|
" 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,",
|
|
" webPerformanceRuntimeDiagnostics: compact.webPerformanceRuntimeDiagnostics ?? null,",
|
|
" pagePerformance: compact.pagePerformance ?? null,",
|
|
" requestRate: compact.requestRate ?? null,",
|
|
" requestRatePeaks: Array.isArray(compact.requestRatePeaks) ? compact.requestRatePeaks.slice(0, 3) : [],",
|
|
" 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, timingSourceOfTruth: item.timingSourceOfTruth ?? null, timingStatus: item.timingStatus ?? null, timingAlert: item.timingAlert === true, 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, timingSourceOfTruth: item.timingSourceOfTruth ?? null, timingStatus: item.timingStatus ?? null, timingAlert: item.timingAlert === true, 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, requestRate: compact.requestRate ?? null, requestRateCurve: compact.requestRateCurve ? { summary: compact.requestRateCurve.summary ?? null, buckets: Array.isArray(compact.requestRateCurve.buckets) ? compact.requestRateCurve.buckets.slice(-4) : [], pageCurves: Array.isArray(compact.requestRateCurve.pageCurves) ? compact.requestRateCurve.pageCurves.slice(0, 2).map((item) => ({ path: item.path ?? item.pageKey ?? null, count: item.count ?? null, peakRequestPerMinute: item.peakRequestPerMinute ?? null, peakBucket: item.peakBucket ?? null, buckets: Array.isArray(item.buckets) ? item.buckets.slice(-4) : [] })) : [], apiPathCurves: Array.isArray(compact.requestRateCurve.apiPathCurves) ? compact.requestRateCurve.apiPathCurves.slice(0, 3).map((item) => ({ apiKey: item.apiKey ?? item.path ?? null, count: item.count ?? null, peakRequestPerMinute: item.peakRequestPerMinute ?? null, peakBucket: item.peakBucket ?? null, buckets: Array.isArray(item.buckets) ? item.buckets.slice(-4) : [] })) : [], valuesRedacted: true } : null, requestRatePeaks: Array.isArray(compact.requestRatePeaks) ? compact.requestRatePeaks.slice(0, 3) : [], webPerformanceRuntimeDiagnostics: compact.webPerformanceRuntimeDiagnostics ?? 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, timingSourceOfTruth: item.timingSourceOfTruth ?? null, timingStatus: item.timingStatus ?? null, timingAlert: item.timingAlert === true, 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, timingSourceOfTruth: item.timingSourceOfTruth ?? null, timingStatus: item.timingStatus ?? null, timingAlert: item.timingAlert === true, 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 });",
|
|
" }",
|
|
" }",
|
|
"}",
|
|
"if (Buffer.byteLength(output, 'utf8') > 7500) {",
|
|
" const frontend = objectOrNull(compact.frontendPerformance) || {};",
|
|
" const frontendHotspots = objectOrNull(compact.frontendPerformanceHotspots) || {};",
|
|
" output = compactOutput({",
|
|
" ok: compact.ok,",
|
|
" counts: compact.counts ? { samples: compact.counts.samples ?? null, control: compact.counts.control ?? null, network: compact.counts.network ?? null, console: compact.counts.console ?? null, artifacts: compact.counts.artifacts ?? null } : null,",
|
|
" jsonlScope: compact.jsonlScope ? { mode: compact.jsonlScope.mode ?? null, focus: objectOrNull(compact.jsonlScope.focus)?.mode ?? null, archivePrefix: compact.jsonlScope.archivePrefix ?? null } : null,",
|
|
" analysisWindow: compact.analysisWindow ? { name: compact.analysisWindow.name ?? null, fromAt: compact.analysisWindow.fromAt ?? null, toAt: compact.analysisWindow.toAt ?? null, samples: compact.analysisWindow.samples ?? null, network: compact.analysisWindow.network ?? null, console: compact.analysisWindow.console ?? null } : 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,",
|
|
" sampleMetrics: compact.sampleMetrics ? { sampleCount: compact.sampleMetrics.sampleCount ?? null, roundCount: Array.isArray(compact.sampleMetrics.rounds) ? compact.sampleMetrics.rounds.length : null, turnColumnCount: Array.isArray(compact.sampleMetrics.turnColumns) ? compact.sampleMetrics.turnColumns.length : null, turnTimingRows: compact.sampleMetrics.turnTimingRows ?? null, turnTimingTotalElapsedForwardJumpCount: compact.sampleMetrics.turnTimingTotalElapsedForwardJumpCount ?? null, turnTimingRecentUpdateJumpCount: compact.sampleMetrics.turnTimingRecentUpdateJumpCount ?? compact.sampleMetrics.turnTimingRecentUpdateSawtoothJumpCount ?? null, loadingSampleCount: compact.sampleMetrics.loadingSampleCount ?? null, loadingMaxCount: compact.sampleMetrics.loadingMaxCount ?? null, loadingOverFiveSecondSegmentCount: compact.sampleMetrics.loadingOverFiveSecondSegmentCount ?? null } : null,",
|
|
" requestRate: compact.requestRate ? { requestCount: compact.requestRate.requestCount ?? null, totalPeakPerMinute: compact.requestRate.totalPeakPerMinute ?? null, pagePeakPerMinute: compact.requestRate.pagePeakPerMinute ?? null, apiPathPeakPerMinute: compact.requestRate.apiPathPeakPerMinute ?? null, overThresholdPeakCount: compact.requestRate.overThresholdPeakCount ?? null } : null,",
|
|
" frontendPerformance: frontend ? { eventCount: frontend.eventCount ?? null, longTaskCount: frontend.longTaskCount ?? null, longAnimationFrameCount: frontend.longAnimationFrameCount ?? null, eventLoopGapCount: frontend.eventLoopGapCount ?? null, captureCount: frontend.captureCount ?? null, maxLongTaskMs: frontend.maxLongTaskMs ?? null, maxLongAnimationFrameMs: frontend.maxLongAnimationFrameMs ?? null, maxEventLoopGapMs: frontend.maxEventLoopGapMs ?? null, profileHotspotCount: frontend.profileHotspotCount ?? null, scriptHotspotCount: frontend.scriptHotspotCount ?? null, profileFunctions: takeHead(frontendHotspots.profileFunctions, 3).map((item) => ({ functionName: clip(item?.functionName ?? item?.name, 64), url: clip(item?.url ?? item?.sourceURL, 96), selfTimeMs: item?.selfTimeMs ?? null, totalTimeMs: item?.totalTimeMs ?? null, hitCount: item?.hitCount ?? null })), profileStacks: takeHead(frontendHotspots.profileStacks, 2).map((item) => ({ functionName: clip(item?.functionName ?? item?.name, 64), selfTimeMs: item?.selfTimeMs ?? null, frameCount: item?.frameCount ?? (Array.isArray(item?.frames) ? item.frames.length : null) })), scripts: takeHead(frontendHotspots.scripts, 3).map((item) => ({ functionName: clip(item?.sourceFunctionName ?? item?.functionName ?? item?.invoker, 64), url: clip(item?.sourceURL ?? item?.url, 96), totalDurationMs: item?.totalDurationMs ?? null, count: item?.count ?? null })) } : null,",
|
|
" webPerformanceRuntimeDiagnostics: compact.webPerformanceRuntimeDiagnostics ?? null,",
|
|
" 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, 100) })) : [],",
|
|
" archiveRedFindings: Array.isArray(compact.archiveRedFindings) ? compact.archiveRedFindings.slice(0, 3).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, 100) })) : [],",
|
|
" 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, overBudgetCount: item.overBudgetCount ?? null })) : [],",
|
|
" reportJsonPath: compact.reportJsonPath ?? reportJsonPath,",
|
|
" reportJsonSha256: compact.reportJsonSha256 ?? sha256(reportJsonPath),",
|
|
" reportMdPath: compact.reportMdPath ?? reportMdPath,",
|
|
" reportMdSha256: compact.reportMdSha256 ?? sha256(reportMdPath),",
|
|
" analyzer: { exitCode: compact.analyzer?.exitCode ?? null, recoveredFrom: compact.analyzer?.recoveredFrom ?? null, stdoutBytes: compact.analyzer?.stdoutBytes ?? null, stderrBytes: compact.analyzer?.stderrBytes ?? null, compactStdoutLimited: true, hardFallback: true, byteClamp: 7500 },",
|
|
" 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 analysisArtifactDir = options.stateDir ? join(options.stateDir, "analysis") : "";
|
|
const reportJsonPath = analysisArtifactDir ? join(analysisArtifactDir, "report.json") : null;
|
|
const collectReportCommand = webObserveAnalyzeCollectCommand(options, "analysis/report.json");
|
|
const stdoutResolution = resolveCliChildJsonObject({
|
|
stdout: result.stdout,
|
|
stderr: result.stderr,
|
|
requestedStdoutType: "web-probe observe analyze compact JSON",
|
|
acceptParsed: isWebObserveAnalyzeJsonContract,
|
|
forceArtifactFallbackReason: result.timedOut ? "remote-command-timeout" : result.exitCode !== 0 ? "remote-command-failed" : null,
|
|
artifactFallback: {
|
|
path: reportJsonPath,
|
|
nextCommand: collectReportCommand,
|
|
read: () => {
|
|
const recovered = recoverWebObserveAnalyzeFromArtifacts(options, spec, result);
|
|
if (recovered !== null) {
|
|
return {
|
|
ok: true,
|
|
value: recovered,
|
|
path: stringOrNullValue(recordValue(recovered).reportJsonPath) ?? reportJsonPath,
|
|
reason: "analysis-artifact-contract",
|
|
};
|
|
}
|
|
return { ok: false, reason: "analysis-artifact-missing-or-invalid", path: reportJsonPath };
|
|
},
|
|
},
|
|
});
|
|
const artifactAnalysis = stdoutResolution.source === "artifact" ? stdoutResolution.parsed : null;
|
|
const analysis = recoverWebObserveAnalyzeTurnDetails(options, spec, stdoutResolution.parsed);
|
|
const analysisOk = analysis?.ok === true;
|
|
const stdoutDiagnostics = stdoutResolution.diagnostics;
|
|
const failureReason = analysis === null
|
|
? stringOrNullValue(recordValue(stdoutDiagnostics).fallbackReason) ?? "analyzer-output-not-json"
|
|
: "analyzer-reported-not-ok";
|
|
const analysisFailure = analysisOk
|
|
? null
|
|
: {
|
|
reason: failureReason,
|
|
exitCode: result.exitCode,
|
|
timedOut: result.timedOut,
|
|
parsedJson: analysis !== null,
|
|
recoveredFromArtifacts: artifactAnalysis !== null,
|
|
stdoutRecovery: stdoutDiagnostics,
|
|
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 failureAnalysis = analysis ?? (analysisFailure ? {
|
|
ok: false,
|
|
command: "web-probe-observe analyze",
|
|
stateDir: options.stateDir,
|
|
reportJsonPath,
|
|
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",
|
|
stdoutRecovery: stdoutDiagnostics,
|
|
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: {
|
|
collectReportJson: collectReportCommand,
|
|
collectAnalyzerStdout: webObserveAnalyzeCollectCommand(options, "analysis/analyzer-stdout.json"),
|
|
collectAnalyzerStderr: webObserveAnalyzeCollectCommand(options, "analysis/analyzer-stderr.log"),
|
|
valuesRedacted: true,
|
|
},
|
|
valuesRedacted: true,
|
|
} : null);
|
|
const compactResult = analysis === null ? compactCommandResultWithStdoutTail(result) : compactCommandResult(result);
|
|
compactResult.stdoutRecovery = stdoutDiagnostics;
|
|
const payload = {
|
|
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,
|
|
browserFreezePolicy,
|
|
wrapper: buildWebObserveWrapperForObserveOptions("analyze", options, spec.workspace),
|
|
result: compactResult,
|
|
full: options.full,
|
|
valuesRedacted: true,
|
|
};
|
|
return options.raw ? compactWebObserveAnalyzePayloadForRaw(payload, options.compactRaw) : withWebObserveAnalyzeRendered(payload);
|
|
}
|
|
|
|
function webObserveAnalyzeCollectCommand(options: NodeWebProbeObserveOptions, file: string): string {
|
|
const selector = options.stateDir ? `--state-dir ${shellQuote(options.stateDir)}` : webObserveIdFromOptions(options);
|
|
return `bun scripts/cli.ts web-probe observe collect ${selector} --node ${options.node} --lane ${options.lane} --view files --file ${shellQuote(file)}`;
|
|
}
|
|
|
|
function isWebObserveAnalyzeJsonContract(value: Record<string, unknown>): boolean {
|
|
const hasReportPath = typeof value.reportJsonPath === "string" || typeof value.reportMdPath === "string";
|
|
const hasAnalyzer = Object.keys(recordValue(value.analyzer)).length > 0;
|
|
const hasFindings = arrayRecordsValue(value.findings).length > 0 || arrayRecordsValue(value.archiveRedFindings).length > 0;
|
|
const hasAnalyzeData = hasReportPath
|
|
|| hasAnalyzer
|
|
|| hasFindings
|
|
|| Object.keys(recordValue(value.counts)).length > 0
|
|
|| Object.keys(recordValue(value.sampleMetrics)).length > 0
|
|
|| Object.keys(recordValue(value.requestRate)).length > 0
|
|
|| Object.keys(recordValue(value.requestRateCurve)).length > 0
|
|
|| Object.keys(recordValue(value.frontendPerformance)).length > 0
|
|
|| Object.keys(recordValue(value.webPerformanceRuntimeDiagnostics)).length > 0
|
|
|| Object.keys(recordValue(value.runtimeAlerts)).length > 0
|
|
|| Object.keys(recordValue(value.archiveSummary)).length > 0
|
|
|| Object.keys(recordValue(value.analysisWindow)).length > 0;
|
|
const hasFailureData = typeof value.error === "string" || hasAnalyzer || hasFindings || hasReportPath;
|
|
if (value.ok === false) return hasFailureData;
|
|
return hasAnalyzeData;
|
|
}
|
|
|
|
function compactWebObserveAnalyzePayloadForRaw(payload: Record<string, unknown>, compactRaw: boolean): Record<string, unknown> {
|
|
if (!compactRaw) return payload;
|
|
const analysis = payload.analysis && typeof payload.analysis === "object" && !Array.isArray(payload.analysis)
|
|
? payload.analysis as Record<string, unknown>
|
|
: {};
|
|
return {
|
|
ok: payload.ok,
|
|
status: payload.status,
|
|
command: payload.command,
|
|
id: payload.id,
|
|
node: payload.node,
|
|
lane: payload.lane,
|
|
workspace: payload.workspace,
|
|
analysis: compactWebObserveAnalyzeAnalysisForRaw(analysis),
|
|
failure: compactWebObserveAnalyzeFailureForRaw(payload.failure),
|
|
result: payload.result,
|
|
valuesRedacted: true,
|
|
};
|
|
}
|
|
|
|
function compactWebObserveAnalyzeAnalysisForRaw(analysis: Record<string, unknown>): Record<string, unknown> {
|
|
const counts = recordValue(analysis.counts);
|
|
const archiveSummary = recordValue(analysis.archiveSummary);
|
|
const requestRate = recordValue(analysis.requestRate);
|
|
const requestRateCurve = recordValue(analysis.requestRateCurve);
|
|
const findings = arrayRecordsValue(analysis.findings).slice(0, 4).map(compactWebObserveAnalyzeFindingForRaw);
|
|
const archiveRedFindings = arrayRecordsValue(analysis.archiveRedFindings ?? archiveSummary.redFindings).slice(0, 3).map(compactWebObserveAnalyzeFindingForRaw);
|
|
const frontendPerformance = recordValue(analysis.frontendPerformance);
|
|
const frontendHotspots = recordValue(analysis.frontendPerformanceHotspots);
|
|
return {
|
|
ok: analysis.ok === true ? true : analysis.ok === false ? false : null,
|
|
command: stringOrNullValue(analysis.command),
|
|
stateDir: stringOrNullValue(analysis.stateDir),
|
|
counts,
|
|
analysisWindow: recordValue(analysis.analysisWindow),
|
|
archiveSummary: {
|
|
redFindingCount: numberOrNullValue(archiveSummary.redFindingCount),
|
|
findingCount: numberOrNullValue(archiveSummary.findingCount),
|
|
sampleCount: numberOrNullValue(recordValue(archiveSummary.sampleMetrics).sampleCount),
|
|
pagePerformance: recordValue(archiveSummary.pagePerformance),
|
|
runtimeAlerts: recordValue(archiveSummary.runtimeAlerts),
|
|
valuesRedacted: true,
|
|
},
|
|
runtimeAlerts: compactWebObserveAnalyzeAlertSummaryForRaw(analysis.runtimeAlerts),
|
|
requestRate: Object.keys(requestRate).length > 0 ? requestRate : recordValue(requestRateCurve.summary),
|
|
requestRateCurve: compactWebObserveAnalyzeRequestRateCurveForRaw(requestRateCurve),
|
|
requestRatePeaks: arrayRecordsValue(analysis.requestRatePeaks ?? requestRateCurve.peaks ?? requestRate.peaks).slice(0, 4),
|
|
frontendPerformance: compactWebObserveAnalyzeFrontendPerformanceForRaw(frontendPerformance, frontendHotspots),
|
|
pagePerformanceSlowApi: arrayRecordsValue(analysis.pagePerformanceSlowApi ?? analysis.archivePagePerformanceSlowApi).slice(0, 4).map((item) => ({
|
|
path: stringOrNullValue(item.path ?? item.route),
|
|
route: stringOrNullValue(item.route ?? item.path),
|
|
sampleCount: numberOrNullValue(item.sampleCount),
|
|
p95Ms: numberOrNullValue(item.p95Ms ?? item.p95),
|
|
maxMs: numberOrNullValue(item.maxMs ?? item.max),
|
|
budgetMs: numberOrNullValue(item.budgetMs),
|
|
overBudgetCount: numberOrNullValue(item.overBudgetCount),
|
|
overFiveSecondCount: numberOrNullValue(item.overFiveSecondCount),
|
|
valuesRedacted: true,
|
|
})),
|
|
findings,
|
|
archiveRedFindings,
|
|
reportJsonPath: stringOrNullValue(analysis.reportJsonPath),
|
|
reportJsonSha256: stringOrNullValue(analysis.reportJsonSha256),
|
|
reportMdPath: stringOrNullValue(analysis.reportMdPath),
|
|
reportMdSha256: stringOrNullValue(analysis.reportMdSha256),
|
|
analyzer: compactWebObserveAnalyzeAnalyzerForRaw(analysis.analyzer),
|
|
valuesRedacted: true,
|
|
};
|
|
}
|
|
|
|
function compactWebObserveAnalyzeRequestRateCurveForRaw(value: Record<string, unknown>): Record<string, unknown> | null {
|
|
const summary = recordValue(value.summary);
|
|
const hasCurve = Object.keys(summary).length > 0
|
|
|| arrayRecordsValue(value.buckets).length > 0
|
|
|| arrayRecordsValue(value.pageCurves).length > 0
|
|
|| arrayRecordsValue(value.apiPathCurves).length > 0
|
|
|| arrayRecordsValue(value.peaks).length > 0;
|
|
if (!hasCurve) return null;
|
|
return {
|
|
summary,
|
|
buckets: arrayRecordsValue(value.buckets).slice(-4).map(compactWebObserveAnalyzeRequestRateBucketForRaw),
|
|
pageCurves: arrayRecordsValue(value.pageCurves).slice(0, 3).map(compactWebObserveAnalyzeRequestRateSeriesForRaw),
|
|
apiPathCurves: arrayRecordsValue(value.apiPathCurves).slice(0, 4).map(compactWebObserveAnalyzeRequestRateSeriesForRaw),
|
|
peaks: arrayRecordsValue(value.peaks).slice(0, 4),
|
|
valuesRedacted: true,
|
|
};
|
|
}
|
|
|
|
function compactWebObserveAnalyzeFrontendPerformanceForRaw(performance: Record<string, unknown>, hotspots: Record<string, unknown>): Record<string, unknown> | null {
|
|
const summary = recordValue(performance.summary);
|
|
const hasPerformance = Object.keys(summary).length > 0
|
|
|| arrayRecordsValue(performance.profileHotspots).length > 0
|
|
|| arrayRecordsValue(performance.profileStacks).length > 0
|
|
|| arrayRecordsValue(performance.scriptHotspots).length > 0
|
|
|| arrayRecordsValue(hotspots.profileFunctions).length > 0
|
|
|| arrayRecordsValue(hotspots.profileStacks).length > 0
|
|
|| arrayRecordsValue(hotspots.scripts).length > 0;
|
|
if (!hasPerformance) return null;
|
|
const performanceSummary = Object.keys(summary).length > 0 ? summary : performance;
|
|
return {
|
|
summary: {
|
|
eventCount: numberOrNullValue(performanceSummary.eventCount),
|
|
longTaskCount: numberOrNullValue(performanceSummary.longTaskCount),
|
|
longAnimationFrameCount: numberOrNullValue(performanceSummary.longAnimationFrameCount),
|
|
eventLoopGapCount: numberOrNullValue(performanceSummary.eventLoopGapCount),
|
|
captureCount: numberOrNullValue(performanceSummary.captureCount),
|
|
maxLongTaskMs: numberOrNullValue(performanceSummary.maxLongTaskMs),
|
|
maxLongAnimationFrameMs: numberOrNullValue(performanceSummary.maxLongAnimationFrameMs),
|
|
maxEventLoopGapMs: numberOrNullValue(performanceSummary.maxEventLoopGapMs),
|
|
profileHotspotCount: numberOrNullValue(performanceSummary.profileHotspotCount),
|
|
scriptHotspotCount: numberOrNullValue(performanceSummary.scriptHotspotCount),
|
|
valuesRedacted: true,
|
|
},
|
|
profileFunctions: arrayRecordsValue(hotspots.profileFunctions ?? performance.profileHotspots).slice(0, 4).map(compactWebObserveAnalyzeFrontendHotspotForRaw),
|
|
profileStacks: arrayRecordsValue(hotspots.profileStacks ?? performance.profileStacks).slice(0, 3).map(compactWebObserveAnalyzeFrontendStackForRaw),
|
|
scriptHotspots: arrayRecordsValue(hotspots.scripts ?? performance.scriptHotspots).slice(0, 4).map(compactWebObserveAnalyzeFrontendHotspotForRaw),
|
|
valuesRedacted: true,
|
|
};
|
|
}
|
|
|
|
function compactWebObserveAnalyzeFrontendHotspotForRaw(value: Record<string, unknown>): Record<string, unknown> {
|
|
return {
|
|
functionName: stringOrNullValue(value.functionName ?? value.name ?? value.sourceFunctionName ?? value.invoker)?.slice(0, 80) ?? null,
|
|
url: stringOrNullValue(value.url ?? value.sourceURL ?? value.scriptUrl)?.slice(0, 120) ?? null,
|
|
lineNumber: numberOrNullValue(value.lineNumber ?? value.line),
|
|
columnNumber: numberOrNullValue(value.columnNumber ?? value.column),
|
|
selfTimeMs: numberOrNullValue(value.selfTimeMs ?? value.selfMs),
|
|
totalTimeMs: numberOrNullValue(value.totalTimeMs ?? value.totalMs),
|
|
totalDurationMs: numberOrNullValue(value.totalDurationMs ?? value.durationMs),
|
|
hitCount: numberOrNullValue(value.hitCount ?? value.sampleCount ?? value.count),
|
|
captureCount: numberOrNullValue(value.captureCount),
|
|
valuesRedacted: true,
|
|
};
|
|
}
|
|
|
|
function compactWebObserveAnalyzeFrontendStackForRaw(value: Record<string, unknown>): Record<string, unknown> {
|
|
return {
|
|
...compactWebObserveAnalyzeFrontendHotspotForRaw(value),
|
|
frameCount: numberOrNullValue(value.frameCount),
|
|
frames: arrayRecordsValue(value.frames).slice(0, 2).map((frame) => ({
|
|
functionName: stringOrNullValue(frame.functionName ?? frame.name)?.slice(0, 64) ?? null,
|
|
url: stringOrNullValue(frame.url ?? frame.sourceURL ?? frame.scriptUrl)?.slice(0, 96) ?? null,
|
|
lineNumber: numberOrNullValue(frame.lineNumber ?? frame.line),
|
|
columnNumber: numberOrNullValue(frame.columnNumber ?? frame.column),
|
|
valuesRedacted: true,
|
|
})),
|
|
};
|
|
}
|
|
|
|
function compactWebObserveAnalyzeRequestRateBucketForRaw(value: Record<string, unknown>): Record<string, unknown> {
|
|
return {
|
|
t: stringOrNullValue(value.startAt ?? value.t),
|
|
e: stringOrNullValue(value.endAt ?? value.e),
|
|
c: numberOrNullValue(value.count ?? value.c),
|
|
rpm: numberOrNullValue(value.requestPerMinute ?? value.rpm),
|
|
};
|
|
}
|
|
|
|
function compactWebObserveAnalyzeRequestRateSeriesForRaw(value: Record<string, unknown>): Record<string, unknown> {
|
|
const out: Record<string, unknown> = {
|
|
count: numberOrNullValue(value.count),
|
|
bucketCount: numberOrNullValue(value.bucketCount),
|
|
peakRequestPerMinute: numberOrNullValue(value.peakRequestPerMinute),
|
|
peakBucket: compactWebObserveAnalyzeRequestRateBucketForRaw(recordValue(value.peakBucket)),
|
|
buckets: arrayRecordsValue(value.buckets).slice(-6).map(compactWebObserveAnalyzeRequestRateBucketForRaw),
|
|
valuesRedacted: true,
|
|
};
|
|
const pageKey = stringOrNullValue(value.pageKey);
|
|
const apiKey = stringOrNullValue(value.apiKey);
|
|
const path = stringOrNullValue(value.path);
|
|
const method = stringOrNullValue(value.method);
|
|
const pageRole = stringOrNullValue(value.pageRole);
|
|
const pageId = stringOrNullValue(value.pageId);
|
|
const pageEpoch = numberOrNullValue(value.pageEpoch);
|
|
if (pageKey) out.pageKey = pageKey;
|
|
if (pageRole) out.pageRole = pageRole;
|
|
if (pageId) out.pageId = pageId;
|
|
if (pageEpoch !== null) out.pageEpoch = pageEpoch;
|
|
if (apiKey) out.apiKey = apiKey;
|
|
if (method) out.method = method;
|
|
if (path) out.path = path;
|
|
return out;
|
|
}
|
|
|
|
function compactWebObserveAnalyzeFailureForRaw(value: unknown): Record<string, unknown> | null {
|
|
const failure = recordValue(value);
|
|
if (Object.keys(failure).length === 0) return null;
|
|
return {
|
|
reason: stringOrNullValue(failure.reason),
|
|
exitCode: numberOrNullValue(failure.exitCode),
|
|
timedOut: failure.timedOut === true,
|
|
parsedJson: failure.parsedJson === true,
|
|
recoveredFromArtifacts: failure.recoveredFromArtifacts === true,
|
|
stdoutBytes: numberOrNullValue(failure.stdoutBytes),
|
|
stderrBytes: numberOrNullValue(failure.stderrBytes),
|
|
stdoutTail: stringOrNullValue(failure.stdoutTail)?.slice(-1200) ?? null,
|
|
stderrTail: stringOrNullValue(failure.stderrTail)?.slice(-1200) ?? null,
|
|
valuesRedacted: true,
|
|
};
|
|
}
|
|
|
|
function compactWebObserveAnalyzeAnalyzerForRaw(value: unknown): Record<string, unknown> {
|
|
const analyzer = recordValue(value);
|
|
return {
|
|
exitCode: numberOrNullValue(analyzer.exitCode),
|
|
recoveredFrom: stringOrNullValue(analyzer.recoveredFrom),
|
|
stdoutBytes: numberOrNullValue(analyzer.stdoutBytes),
|
|
stderrBytes: numberOrNullValue(analyzer.stderrBytes),
|
|
reportJsonBytes: numberOrNullValue(analyzer.reportJsonBytes),
|
|
reportMdBytes: numberOrNullValue(analyzer.reportMdBytes),
|
|
transportExitCode: numberOrNullValue(analyzer.transportExitCode),
|
|
transportTimedOut: analyzer.transportTimedOut === true,
|
|
compactStdoutLimited: analyzer.compactStdoutLimited === true,
|
|
valuesRedacted: true,
|
|
};
|
|
}
|
|
|
|
function compactWebObserveAnalyzeAlertSummaryForRaw(value: unknown): Record<string, unknown> {
|
|
const source = recordValue(value);
|
|
const summary = recordValue(source.summary);
|
|
const out: Record<string, unknown> = {};
|
|
for (const key of [
|
|
"httpErrorCount",
|
|
"requestFailedCount",
|
|
"significantRequestFailedCount",
|
|
"workbenchSessionListReadCount",
|
|
"workbenchTraceEventsReadCount",
|
|
"webPerformanceBeaconFailureCount",
|
|
"workbenchEventSourceFailureCount",
|
|
"consoleAlertCount",
|
|
"significantConsoleAlertCount",
|
|
]) {
|
|
const value = summary[key] ?? source[key];
|
|
if (value !== undefined && value !== null) out[key] = value;
|
|
}
|
|
out.valuesRedacted = true;
|
|
return out;
|
|
}
|
|
|
|
function compactWebObserveAnalyzeFindingForRaw(value: Record<string, unknown>): Record<string, unknown> {
|
|
return {
|
|
kind: stringOrNullValue(value.kind ?? value.id ?? value.code),
|
|
severity: stringOrNullValue(value.severity ?? value.level),
|
|
count: numberOrNullValue(value.count ?? value.sampleCount),
|
|
timingSourceOfTruth: stringOrNullValue(value.timingSourceOfTruth ?? value.expectedElapsedSource ?? value.evidenceKind),
|
|
timingStatus: stringOrNullValue(value.timingStatus),
|
|
timingAlert: value.timingAlert === true,
|
|
summary: stringOrNullValue(value.summary ?? value.message)?.slice(0, 140) ?? null,
|
|
rootCause: stringOrNullValue(value.rootCause)?.slice(0, 90) ?? null,
|
|
rootCauseStatus: stringOrNullValue(value.rootCauseStatus)?.slice(0, 90) ?? null,
|
|
rootCauseConfidence: stringOrNullValue(value.rootCauseConfidence)?.slice(0, 40) ?? null,
|
|
valuesRedacted: true,
|
|
};
|
|
}
|
|
|
|
function arrayRecordsValue(value: unknown): Record<string, unknown>[] {
|
|
return Array.isArray(value) ? value.map(recordValue).filter((item) => Object.keys(item).length > 0) : [];
|
|
}
|
|
|
|
function recordValue(value: unknown): Record<string, unknown> {
|
|
return value !== null && typeof value === "object" && !Array.isArray(value) ? value as Record<string, unknown> : {};
|
|
}
|
|
|
|
function stringOrNullValue(value: unknown): string | null {
|
|
return typeof value === "string" && value.length > 0 ? value : null;
|
|
}
|
|
|
|
function numberOrNullValue(value: unknown): number | null {
|
|
const number = Number(value);
|
|
return Number.isFinite(number) ? number : null;
|
|
}
|
|
|
|
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 evidenceSummary = (value) => { const v = objectOrNull(value); if (!v) return null; const out = {}; for (const key of ['http404Count','responseErrorCount','requestFailedCount','statuses','afterProjectedSeqs','sinceSeqs','traceIds','maxFallbackRatio','maxFallbackTitleCount','overThresholdSampleCount','majorityFallbackSampleCount']) if (v[key] !== undefined && v[key] !== null) out[key] = Array.isArray(v[key]) ? v[key].slice(0,6) : v[key]; const text = Object.keys(out).length > 0 ? JSON.stringify(out) : null; return text ? clip(text, 220) : null; };",
|
|
"const slimFinding = (item) => { const v = objectOrNull(item) || {}; return { kind: clip(v.kind ?? v.id ?? v.code, 160), code: clip(v.code ?? v.id ?? v.kind, 160), 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, timingSourceOfTruth: clip(v.timingSourceOfTruth ?? v.expectedElapsedSource ?? v.evidenceKind, 80), timingStatus: clip(v.timingStatus, 48), timingAlert: v.timingAlert === true, summary: clip(v.summary ?? v.message, 180), message: clip(v.message ?? v.summary, 180), rootCause: clip(v.rootCause, 120), rootCauseStatus: clip(v.rootCauseStatus, 80), rootCauseConfidence: clip(v.rootCauseConfidence, 40), nextAction: clip(v.nextAction, 220), evidenceSummary: evidenceSummary(v.evidence) ?? clip(v.evidenceSummary, 220) }; };",
|
|
"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 slimRequestBucket = (item) => { const v = objectOrNull(item) || {}; return { t: v.startAt ?? null, e: v.endAt ?? null, c: v.count ?? null, rpm: v.requestPerMinute ?? null }; };",
|
|
"const slimRequestCurve = (item, isApi = false) => { const v = objectOrNull(item) || {}; return { pageKey: clip(v.pageKey, 96), pageRole: clip(v.pageRole, 24), pageId: clip(v.pageId, 32), pageEpoch: v.pageEpoch ?? null, apiKey: clip(v.apiKey, 120), method: clip(v.method, 12), path: clip(v.path, isApi ? 120 : 96), count: v.count ?? null, bucketCount: v.bucketCount ?? null, peakRequestPerMinute: v.peakRequestPerMinute ?? null, peakBucket: slimRequestBucket(v.peakBucket), buckets: arr(v.buckets).slice(-12).map(slimRequestBucket), valuesRedacted: true }; };",
|
|
"const slimRequestPeak = (item) => { const v = objectOrNull(item) || {}; return { scope: clip(v.scope, 16), thresholdPerMinute: v.thresholdPerMinute ?? null, bucketMs: v.bucketMs ?? null, startAt: v.startAt ?? null, endAt: v.endAt ?? null, count: v.count ?? null, requestPerMinute: v.requestPerMinute ?? null, pageKey: clip(v.pageKey, 96), path: clip(v.path, 96), apiKey: clip(v.apiKey, 120), valuesRedacted: true }; };",
|
|
"const slimRequestRateCurve = (value) => { const v = objectOrNull(value); if (!v) return null; return { summary: objectOrNull(v.summary) || v, buckets: arr(v.buckets).slice(-12).map(slimRequestBucket), pageCurves: arr(v.pageCurves).slice(0, 8).map((item) => slimRequestCurve(item, false)), apiPathCurves: arr(v.apiPathCurves).slice(0, 12).map((item) => slimRequestCurve(item, true)), peaks: arr(v.peaks).slice(0, 12).map(slimRequestPeak), valuesRedacted: true }; };",
|
|
"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 browserProcess = objectOrNull(source.browserProcess) || objectOrNull(recent.browserProcess) || {};",
|
|
"const promptNetwork = objectOrNull(source.promptNetwork) || objectOrNull(recent.promptNetwork) || {};",
|
|
"const requestRateCurve = slimRequestRateCurve(source.requestRateCurve) || slimRequestRateCurve(source.requestRate) || slimRequestRateCurve(recent.requestRate);",
|
|
"const requestRateSummary = objectOrNull(requestRateCurve?.summary) || objectOrNull(source.requestRate?.summary) || objectOrNull(source.requestRate) || null;",
|
|
"const requestRatePeaks = arr(source.requestRatePeaks ?? requestRateCurve?.peaks ?? source.requestRate?.peaks ?? recent.requestRate?.peaks).slice(0, 12).map(slimRequestPeak);",
|
|
"const frontendPerformance = objectOrNull(source.frontendPerformance) || objectOrNull(recent.frontendPerformance) || null;",
|
|
"const frontendPerformanceSummary = objectOrNull(frontendPerformance?.summary) || frontendPerformance;",
|
|
"const frontendPerformanceHotspots = { scripts: arr(source.frontendPerformanceHotspots?.scripts ?? frontendPerformance?.scriptHotspots ?? recent.frontendPerformance?.scriptHotspots).slice(0, 8), profileFunctions: arr(source.frontendPerformanceHotspots?.profileFunctions ?? frontendPerformance?.profileHotspots ?? recent.frontendPerformance?.profileHotspots).slice(0, 8), profileStacks: arr(source.frontendPerformanceHotspots?.profileStacks ?? frontendPerformance?.profileStacks ?? recent.frontendPerformance?.profileStacks).slice(0, 5), valuesRedacted: true };",
|
|
"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) || {}, browserProcess: objectOrNull(archiveSummary.browserProcess) || objectOrNull(browserProcess.summary) || {}, requestRate: objectOrNull(archiveSummary.requestRate) || requestRateSummary || {}, frontendPerformance: objectOrNull(archiveSummary.frontendPerformance) || frontendPerformanceSummary || {}, 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, requestRate: requestRateSummary, requestRateCurve, requestRatePeaks, frontendPerformance: frontendPerformanceSummary, frontendPerformanceHotspots, runtimeAlerts: objectOrNull(runtimeAlerts.summary) || runtimeAlerts, browserProcess: objectOrNull(browserProcess.summary) || browserProcess, 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;
|
|
}
|