fix(web-probe): retry observe auth diagnostics (#642)

Co-authored-by: Codex <codex@noreply.local>
This commit is contained in:
Lyon
2026-06-22 09:12:54 +08:00
committed by GitHub
parent 8434d62d07
commit 9abff705ef
2 changed files with 179 additions and 20 deletions
@@ -262,28 +262,141 @@ async function runControlCommand(command, fn) {
async function authenticate(browserContext) {
const loginUrl = new URL("/auth/login", baseUrl).toString();
const response = await browserContext.request.post(loginUrl, {
data: { username, password },
headers: { accept: "application/json", "content-type": "application/json" },
timeout: 15000,
});
const cookies = await browserContext.cookies(baseUrl);
const cookieNames = cookies.map((cookie) => cookie.name).filter((name) => /session|auth|token/iu.test(name));
return {
ok: response.ok() && cookieNames.length > 0,
const attempts = [];
const maxAttempts = 5;
const initialDelayMs = 250;
const maxDelayMs = 5000;
for (let attempt = 1; attempt <= maxAttempts; attempt += 1) {
const retryDelayMs = attempt < maxAttempts ? Math.min(maxDelayMs, initialDelayMs * (2 ** (attempt - 1))) : 0;
try {
const response = await browserContext.request.post(loginUrl, {
data: { username, password },
headers: { accept: "application/json", "content-type": "application/json" },
timeout: 12000,
});
const cookieState = await readAuthCookieState(browserContext);
const retryable = isRetryableAuthStatus(response.status());
const item = {
attempt,
retryAttempt: attempt,
retryMaxAttempts: maxAttempts,
retryLabel: attempt + "/" + maxAttempts,
retryDelayMs: retryable && attempt < maxAttempts ? retryDelayMs : 0,
method: "api",
status: response.status(),
statusText: response.statusText(),
retryable,
cookiePresent: cookieState.cookiePresent,
cookieNames: cookieState.cookieNames,
valuesRedacted: true,
};
attempts.push(item);
if (response.ok() && cookieState.cookiePresent) {
return {
ok: true,
method: "api",
loginPath: new URL(loginUrl).pathname,
status: response.status(),
statusText: response.statusText(),
cookiePresent: true,
cookieNames: cookieState.cookieNames,
attempts,
retryCount: attempt - 1,
retryMaxAttempts: maxAttempts,
lastRetryLabel: attempt + "/" + maxAttempts,
retryExhausted: false,
retryable: false,
valuesRedacted: true,
};
}
if (!retryable) break;
} catch (error) {
const retryable = isRetryableAuthError(error);
attempts.push({
attempt,
retryAttempt: attempt,
retryMaxAttempts: maxAttempts,
retryLabel: attempt + "/" + maxAttempts,
retryDelayMs: retryable && attempt < maxAttempts ? retryDelayMs : 0,
method: "api",
status: 0,
statusText: "request-error",
retryable,
error: error && error.message ? truncate(error.message, 500) : truncate(String(error), 500),
cookiePresent: false,
cookieNames: [],
valuesRedacted: true,
});
if (!retryable) break;
}
if (attempt < maxAttempts && attempts[attempts.length - 1]?.retryable === true) await sleep(retryDelayMs);
}
const cookieState = await readAuthCookieState(browserContext);
const last = attempts[attempts.length - 1] || null;
const retryable = attempts.some((attempt) => attempt && attempt.retryable === true);
const failure = {
ok: false,
method: "api",
loginPath: new URL(loginUrl).pathname,
status: response.status(),
statusText: response.statusText(),
cookiePresent: cookieNames.length > 0,
cookieNames,
status: typeof last?.status === "number" ? last.status : 0,
statusText: typeof last?.statusText === "string" ? last.statusText : "api-login-failed",
cookiePresent: cookieState.cookiePresent,
cookieNames: cookieState.cookieNames,
attempts,
retryCount: Math.max(0, attempts.length - 1),
retryMaxAttempts: maxAttempts,
lastRetryLabel: last?.retryLabel || null,
retryExhausted: retryable && attempts.length >= maxAttempts,
retryable,
lastError: last?.error || null,
valuesRedacted: true,
};
const error = new Error(authFailureMessage(failure));
error.webProbeAuth = failure;
throw error;
}
function publicAuth(value) {
if (!value) return null;
return { ok: value.ok === true, method: value.method, status: value.status, cookiePresent: value.cookiePresent === true, cookieNames: value.cookieNames || [], valuesRedacted: true };
return {
ok: value.ok === true,
method: value.method,
status: value.status,
cookiePresent: value.cookiePresent === true,
cookieNames: value.cookieNames || [],
retryCount: value.retryCount ?? null,
retryMaxAttempts: value.retryMaxAttempts ?? null,
lastRetryLabel: value.lastRetryLabel ?? null,
retryExhausted: value.retryExhausted === true,
valuesRedacted: true
};
}
async function readAuthCookieState(browserContext) {
const cookies = await browserContext.cookies(baseUrl);
const cookieNames = cookies.map((cookie) => cookie.name).sort();
return {
cookiePresent: cookieNames.includes("hwlab_session") || cookieNames.some((name) => /session|auth|token/iu.test(name)),
cookieNames: cookieNames.filter((name) => /session|auth|token/iu.test(name)),
};
}
function isRetryableAuthStatus(status) {
return status === 0 || status === 408 || status === 409 || status === 425 || status === 429 || status >= 500;
}
function isRetryableAuthError(error) {
const message = error && error.message ? String(error.message) : String(error || "");
return /EAI_AGAIN|ETIMEDOUT|ECONNRESET|ECONNREFUSED|ECONNABORTED|socket hang up|ERR_NETWORK_CHANGED|fetch failed|network|timeout/iu.test(message);
}
function authFailureMessage(failure) {
const last = Array.isArray(failure.attempts) && failure.attempts.length > 0 ? failure.attempts[failure.attempts.length - 1] : null;
const retry = failure.lastRetryLabel ? " retry=" + failure.lastRetryLabel : "";
const exhausted = failure.retryExhausted ? " exhausted=true" : "";
const status = last ? " status=" + (last.status ?? "-") + " " + (last.statusText ?? "") : "";
const error = last?.error ? " error=" + truncate(last.error, 160) : "";
return ("auth login failed:" + retry + exhausted + status + error).trim();
}
function proxyConfigFromEnv(targetBaseUrl) {
@@ -1095,7 +1208,9 @@ function sanitize(value) {
}
function errorSummary(error) {
return { 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 };
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);
return summary;
}
function sleep(ms) {
@@ -1133,6 +1248,16 @@ 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 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 });
@@ -1152,6 +1277,7 @@ const report = {
pagePerformance,
promptNetwork,
runtimeAlerts,
runnerErrors,
findings,
windows: { recent: recentWindow },
readIssues: jsonlReadIssues,
@@ -1189,6 +1315,7 @@ console.log(JSON.stringify({
pagePerformance: recentWindow.pagePerformance.summary,
promptNetwork: recentWindow.promptNetwork.summary,
runtimeAlerts: recentWindow.runtimeAlerts.summary,
runnerErrors,
httpErrorGroups: recentWindow.runtimeAlerts.networkHttpErrorsByPath.slice(0, 8).map((item) => ({
method: item.method ?? null,
status: item.status ?? null,