fix: harden web probe observe diagnostics (#684)
Co-authored-by: Codex <codex@noreply.local>
This commit is contained in:
@@ -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 });
|
||||
|
||||
Reference in New Issue
Block a user