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