fix: harden web probe observe diagnostics (#684)

Co-authored-by: Codex <codex@noreply.local>
This commit is contained in:
Lyon
2026-06-22 22:22:27 +08:00
committed by GitHub
parent 3c92f64f21
commit 30613bb515
3 changed files with 299 additions and 41 deletions
@@ -326,7 +326,19 @@ async function runControlCommand(command, fn) {
await appendJsonl(files.control, controlRecord(command, "completed", { beforeUrl, afterUrl: currentPageUrl(), durationMs: Date.now() - started, result: sanitize(result) }));
return result;
} catch (error) {
await appendJsonl(files.control, controlRecord(command, "failed", { beforeUrl, afterUrl: currentPageUrl(), durationMs: Date.now() - started, error: errorSummary(error) }));
let failurePageProvenance = null;
let failureScreenshot = null;
try {
failurePageProvenance = compactPageProvenance(await refreshPageProvenance("command-failed", null));
} catch (captureError) {
await appendJsonl(files.errors, eventRecord("failure-provenance-error", { commandId: command.id, pageRole: "control", pageId, error: errorSummary(captureError) }));
}
try {
failureScreenshot = await captureScreenshot("command-failed", "jpeg");
} catch (captureError) {
await appendJsonl(files.errors, eventRecord("failure-screenshot-error", { commandId: command.id, pageRole: "control", pageId, error: errorSummary(captureError) }));
}
await appendJsonl(files.control, controlRecord(command, "failed", { beforeUrl, afterUrl: currentPageUrl(), durationMs: Date.now() - started, error: errorSummary(error), failurePageProvenance, failureScreenshot }));
throw error;
} finally {
activeCommandId = null;
@@ -552,12 +564,12 @@ async function gotoTarget(rawTarget) {
const target = new URL(String(rawTarget || targetPath), baseUrl).toString();
const beforeUrl = currentPageUrl();
const attempts = [];
for (let attempt = 1; attempt <= 3; attempt += 1) {
for (let attempt = 1; attempt <= 2; attempt += 1) {
try {
const response = await page.goto(target, { waitUntil: "domcontentloaded", timeout: 30000 });
const response = await page.goto(target, { waitUntil: "domcontentloaded", timeout: 45000 });
await page.waitForTimeout(1000).catch(() => {});
const httpStatus = response ? response.status() : null;
const readiness = await waitForTargetPageReady(page, target, { timeoutMs: 15000 });
const readiness = await waitForTargetPageReady(page, target, { timeoutMs: 45000 });
if (!readiness.ok) {
const error = new Error("workbench-app-not-ready: " + (readiness.reason || "target page did not expose the Workbench shell"));
error.navigationReadiness = readiness;
@@ -569,15 +581,51 @@ async function gotoTarget(rawTarget) {
} catch (error) {
const message = error instanceof Error ? error.message : String(error);
attempts.push({ attempt, ok: false, failureKind: navigationFailureKind(message), message: redactErrorMessage(message), readiness: error?.navigationReadiness ?? null });
if (attempt >= 3 || !isRetryableNavigationError(message)) {
if (/workbench-app-not-ready/iu.test(message)) {
const lateReadiness = await waitForTargetPageReady(page, target, { timeoutMs: 5000 }).catch(() => null);
if (lateReadiness?.ok) {
const pageProvenance = await refreshPageProvenance("goto-late-ready", null);
attempts.push({ attempt, ok: true, lateReady: true, httpStatus: null, readiness: lateReadiness });
return { beforeUrl, afterUrl: currentPageUrl(), httpStatus: null, pageId, pageProvenance: compactPageProvenance(pageProvenance), readiness: lateReadiness, attempts };
}
}
if (attempt >= 2 || !isRetryableNavigationError(message)) {
throw Object.assign(new Error(message), { attempts, target });
}
await page.waitForTimeout(500 * attempt).catch(() => {});
if (!observerPage) {
await recreateAuthenticatedContextForNavigation("retryable-navigation-" + navigationFailureKind(message), attempt).catch((resetError) => appendJsonl(files.errors, eventRecord("navigation-context-reset-error", { commandId: activeCommandId, attempt, error: errorSummary(resetError) })));
} else {
await recreateControlPageForNavigation("retryable-navigation-" + navigationFailureKind(message), attempt).catch((resetError) => appendJsonl(files.errors, eventRecord("navigation-page-reset-error", { commandId: activeCommandId, attempt, error: errorSummary(resetError) })));
}
await page.waitForTimeout(1500 * attempt).catch(() => {});
}
}
return { beforeUrl, afterUrl: currentPageUrl(), httpStatus: null, pageId, attempts };
}
async function recreateControlPageForNavigation(reason, attempt) {
const before = currentPageUrl();
if (page && !page.isClosed()) await page.close().catch(() => {});
page = await context.newPage();
attachPassiveListeners(page, "control", pageId);
currentPageProvenance = null;
await appendJsonl(files.control, eventRecord("page-recreated", { reason, attempt, beforeUrl: before, afterUrl: currentPageUrl(), pageRole: "control", pageId, valuesRedacted: true }));
}
async function recreateAuthenticatedContextForNavigation(reason, attempt) {
const before = currentPageUrl();
if (page && !page.isClosed()) await page.close().catch(() => {});
if (observerPage && !observerPage.isClosed()) await observerPage.close().catch(() => {});
observerPage = null;
if (context) await context.close().catch(() => {});
context = await browser.newContext({ viewport, ...(playwrightProxy === null ? {} : { proxy: playwrightProxy }) });
auth = await authenticate(context);
page = await context.newPage();
attachPassiveListeners(page, "control", pageId);
currentPageProvenance = null;
await appendJsonl(files.control, eventRecord("context-recreated", { reason, attempt, beforeUrl: before, afterUrl: currentPageUrl(), pageRole: "control", pageId, auth: publicAuth(auth), valuesRedacted: true }));
}
async function refreshPageProvenance(reason, httpStatus = null) {
if (!page || page.isClosed()) return currentPageProvenance;
const observed = await page.evaluate(() => {
@@ -699,14 +747,11 @@ async function waitForTargetPageReady(targetPage, targetUrl, options = {}) {
return rect.width > 0 && rect.height > 0 && style.visibility !== "hidden" && style.display !== "none";
};
const workspace = document.querySelector("#workspace, .workbench-route");
const sessionCreate = document.querySelector("#session-create");
const commandInput = document.querySelector("#command-input");
const activeTab = document.querySelector(".session-tab[data-active='true'], .session-tab[aria-selected='true']");
const warning = document.querySelector(".composer-warning");
return Boolean(visible(workspace) && (visible(sessionCreate) || visible(commandInput) || visible(activeTab) || visible(warning)));
const login = document.querySelector("form.login-card, .login-card, [data-testid='login']");
return Boolean(visible(workspace) || visible(login));
}, null, { timeout: timeoutMs }).catch(() => null);
const snapshot = await workbenchReadinessSnapshot(targetPage);
const ok = snapshot.workbenchShellVisible === true && (snapshot.sessionCreateVisible === true || snapshot.commandInputPresent === true || snapshot.activeTabPresent === true || snapshot.warningPresent === true);
const ok = snapshot.workbenchShellVisible === true;
return {
ok,
reason: ok ? "workbench-ready" : snapshot.loginVisible ? "login-visible" : "workbench-app-not-ready",
@@ -717,7 +762,7 @@ async function waitForTargetPageReady(targetPage, targetUrl, options = {}) {
}
async function workbenchReadinessSnapshot(targetPage) {
return targetPage.evaluate(() => {
const snapshot = await targetPage.evaluate(() => {
const visible = (element) => {
if (!element) return false;
const rect = element.getBoundingClientRect();
@@ -734,10 +779,15 @@ async function workbenchReadinessSnapshot(targetPage) {
activeTabPresent: visible(document.querySelector(".session-tab[data-active='true'], .session-tab[aria-selected='true']")),
warningPresent: visible(document.querySelector(".composer-warning")),
loginVisible: visible(document.querySelector("form.login-card, .login-card, [data-testid='login']")),
bodyTextHash: sha256Text(String(document.body?.innerText || "").slice(0, 2000)),
bodyTextPreview: String(document.body?.innerText || "").slice(0, 2000),
valuesRedacted: true
};
}).catch((error) => ({ error: errorSummary(error), valuesRedacted: true }));
if (snapshot && typeof snapshot.bodyTextPreview === "string") {
snapshot.bodyTextHash = sha256Text(snapshot.bodyTextPreview);
delete snapshot.bodyTextPreview;
}
return snapshot;
}
function isWorkbenchPathname(value) {
@@ -1591,6 +1641,10 @@ function sanitize(value) {
function errorSummary(error) {
const summary = { name: error && error.name ? error.name : "Error", message: error && error.message ? truncate(error.message, 1000) : truncate(String(error), 1000), stackTail: error && error.stack ? truncate(error.stack, 2000) : null };
if (error && error.webProbeAuth) summary.auth = sanitize(error.webProbeAuth);
if (error && Array.isArray(error.attempts)) summary.attempts = sanitize(error.attempts.slice(-5));
if (error && error.navigationReadiness) summary.navigationReadiness = sanitize(error.navigationReadiness);
if (error && error.details) summary.details = sanitize(error.details);
if (error && error.target) summary.target = sanitize(error.target);
return summary;
}
@@ -1629,16 +1683,46 @@ const pageProvenance = buildPageProvenanceReport(samples, control, manifest);
const pagePerformance = buildPagePerformanceReport(samples, manifest);
const promptNetwork = buildPromptNetworkReport(control, network);
const runtimeAlerts = buildRuntimeAlerts(samples, control, network, consoleEvents, errors);
const runnerErrors = errors.slice(-8).map((item) => ({
ts: item.ts ?? null,
type: item.type ?? null,
commandId: item.commandId ?? null,
sampleSeq: item.sampleSeq ?? null,
message: limitText(item.error?.message ?? item.message ?? "", 240),
retry: item.error?.auth?.lastRetryLabel ?? null,
retryExhausted: item.error?.auth?.retryExhausted === true,
lastError: limitText(item.error?.auth?.lastError ?? "", 160),
}));
const runnerErrors = errors.slice(-8).map((item) => {
const attempts = Array.isArray(item.error?.attempts) ? item.error.attempts : [];
const lastAttempt = attempts.length > 0 ? attempts[attempts.length - 1] : null;
const readiness = lastAttempt?.readiness || item.error?.navigationReadiness || null;
const readinessSnapshot = readiness?.snapshot || readiness;
return {
ts: item.ts ?? null,
type: item.type ?? null,
commandId: item.commandId ?? null,
sampleSeq: item.sampleSeq ?? null,
message: limitText(item.error?.message ?? item.message ?? "", 240),
retry: item.error?.auth?.lastRetryLabel ?? null,
retryExhausted: item.error?.auth?.retryExhausted === true,
lastError: limitText(item.error?.auth?.lastError ?? "", 160),
attemptCount: attempts.length,
lastFailureKind: lastAttempt?.failureKind ?? null,
lastReadinessReason: readiness?.reason ?? null,
lastReadiness: readinessSnapshot ? {
reason: readiness?.reason ?? readinessSnapshot.reason ?? null,
path: readinessSnapshot.path ?? null,
readyState: readinessSnapshot.readyState ?? null,
workbenchShellVisible: readinessSnapshot.workbenchShellVisible === true,
sessionCreateVisible: readinessSnapshot.sessionCreateVisible === true,
commandInputPresent: readinessSnapshot.commandInputPresent === true,
activeTabPresent: readinessSnapshot.activeTabPresent === true,
warningPresent: readinessSnapshot.warningPresent === true,
loginVisible: readinessSnapshot.loginVisible === true,
bodyTextHash: readinessSnapshot.bodyTextHash ?? null,
valuesRedacted: true
} : null,
navigationAttempts: attempts.slice(-5).map((attempt) => ({
attempt: attempt?.attempt ?? null,
ok: attempt?.ok === true,
failureKind: attempt?.failureKind ?? null,
message: limitText(attempt?.message ?? "", 160),
readinessReason: attempt?.readiness?.reason ?? null,
valuesRedacted: true
})),
};
});
const findings = buildFindings(samples, control, network, errors, sampleMetrics, promptNetwork, runtimeAlerts, pagePerformance, pageProvenance);
if (jsonlReadIssues.length > 0) findings.unshift({ id: "jsonl-read-issues", severity: "red", summary: "observer analyzer hit JSONL read/parse issues", count: jsonlReadIssues.length, issues: jsonlReadIssues.slice(0, 20) });
const recentWindow = buildRecentAnalysisWindow({ samples, control, network, consoleEvents, errors, manifest });