1100 lines
49 KiB
TypeScript
1100 lines
49 KiB
TypeScript
// SPEC: PJ2026-01040111 long-running Workbench observation.
|
||
// Responsibility: Analyzer request-rate, prompt network, runtime console/network alert, and diagnostic grouping source fragment.
|
||
|
||
export function nodeWebObserveAnalyzerRequestRuntimeSource(): string {
|
||
return String.raw`function buildRequestRateReport(network) {
|
||
const bucketMs = Math.max(1000, Number(alertThresholds.requestRateBucketMs || 0));
|
||
const naturalApiRequests = (Array.isArray(network) ? network : [])
|
||
.filter((item) => item?.observerInitiated !== true && item?.type === "request")
|
||
.map((item) => requestRateEvent(item))
|
||
.filter((item) => item !== null)
|
||
.sort((a, b) => a.tsMs - b.tsMs);
|
||
if (naturalApiRequests.length === 0) {
|
||
return {
|
||
summary: {
|
||
bucketMs,
|
||
bucketSeconds: Number((bucketMs / 1000).toFixed(3)),
|
||
requestCount: 0,
|
||
bucketCount: 0,
|
||
pageCount: 0,
|
||
apiPathCount: 0,
|
||
totalRedPerMinute: alertThresholds.requestRateTotalRedPerMinute,
|
||
pageRedPerMinute: alertThresholds.requestRatePageRedPerMinute,
|
||
apiPathRedPerMinute: alertThresholds.requestRateApiPathRedPerMinute,
|
||
totalPeakPerMinute: 0,
|
||
pagePeakPerMinute: 0,
|
||
apiPathPeakPerMinute: 0,
|
||
overThresholdPeakCount: 0,
|
||
valuesRedacted: true,
|
||
},
|
||
buckets: [],
|
||
pageCurves: [],
|
||
apiPathCurves: [],
|
||
peaks: [],
|
||
valuesRedacted: true,
|
||
};
|
||
}
|
||
const firstBucketMs = Math.floor(naturalApiRequests[0].tsMs / bucketMs) * bucketMs;
|
||
const lastBucketMs = Math.floor(naturalApiRequests[naturalApiRequests.length - 1].tsMs / bucketMs) * bucketMs;
|
||
const makeBucket = (bucketStartMs, extra = {}) => ({
|
||
bucketStartMs,
|
||
bucketEndMs: bucketStartMs + bucketMs,
|
||
startAt: new Date(bucketStartMs).toISOString(),
|
||
endAt: new Date(bucketStartMs + bucketMs).toISOString(),
|
||
count: 0,
|
||
requestPerMinute: 0,
|
||
...extra,
|
||
valuesRedacted: true,
|
||
});
|
||
const totalBuckets = new Map();
|
||
const pageBuckets = new Map();
|
||
const apiPathBuckets = new Map();
|
||
const pageTotals = new Map();
|
||
const apiPathTotals = new Map();
|
||
const allBucketStarts = [];
|
||
for (let bucketStartMs = firstBucketMs; bucketStartMs <= lastBucketMs; bucketStartMs += bucketMs) {
|
||
totalBuckets.set(bucketStartMs, makeBucket(bucketStartMs));
|
||
allBucketStarts.push(bucketStartMs);
|
||
}
|
||
for (const item of naturalApiRequests) {
|
||
const bucketStartMs = Math.floor(item.tsMs / bucketMs) * bucketMs;
|
||
const totalBucket = totalBuckets.get(bucketStartMs) || makeBucket(bucketStartMs);
|
||
totalBucket.count += 1;
|
||
totalBuckets.set(bucketStartMs, totalBucket);
|
||
|
||
const pageKey = item.pageKey;
|
||
const pageMapKey = pageKey + "|" + bucketStartMs;
|
||
const pageBucket = pageBuckets.get(pageMapKey) || makeBucket(bucketStartMs, {
|
||
pageKey,
|
||
pageRole: item.pageRole,
|
||
pageId: item.pageId,
|
||
pageEpoch: item.pageEpoch,
|
||
path: item.framePath,
|
||
});
|
||
pageBucket.count += 1;
|
||
pageBuckets.set(pageMapKey, pageBucket);
|
||
const pageTotal = pageTotals.get(pageKey) || { pageKey, pageRole: item.pageRole, pageId: item.pageId, pageEpoch: item.pageEpoch, path: item.framePath, count: 0 };
|
||
pageTotal.count += 1;
|
||
pageTotals.set(pageKey, pageTotal);
|
||
|
||
const apiKey = item.method + " " + item.apiPath;
|
||
const apiMapKey = apiKey + "|" + bucketStartMs;
|
||
const apiBucket = apiPathBuckets.get(apiMapKey) || makeBucket(bucketStartMs, {
|
||
apiKey,
|
||
method: item.method,
|
||
path: item.apiPath,
|
||
pageKey,
|
||
pageRole: item.pageRole,
|
||
pageId: item.pageId,
|
||
pageEpoch: item.pageEpoch,
|
||
});
|
||
apiBucket.count += 1;
|
||
apiPathBuckets.set(apiMapKey, apiBucket);
|
||
const apiTotal = apiPathTotals.get(apiKey) || { apiKey, method: item.method, path: item.apiPath, count: 0 };
|
||
apiTotal.count += 1;
|
||
apiPathTotals.set(apiKey, apiTotal);
|
||
}
|
||
const finalizeBucket = (bucket) => ({
|
||
...bucket,
|
||
requestPerMinute: Number((Number(bucket.count || 0) * 60000 / bucketMs).toFixed(2)),
|
||
});
|
||
const buckets = Array.from(totalBuckets.values()).map(finalizeBucket);
|
||
const pageBucketRows = Array.from(pageBuckets.values()).map(finalizeBucket);
|
||
const apiPathBucketRows = Array.from(apiPathBuckets.values()).map(finalizeBucket);
|
||
const peakForRows = (rows, thresholdPerMinute, scope, extra = {}) => rows
|
||
.filter((row) => Number(row.requestPerMinute ?? 0) >= Number(thresholdPerMinute || Infinity))
|
||
.map((row) => ({
|
||
scope,
|
||
thresholdPerMinute,
|
||
overThreshold: true,
|
||
bucketMs,
|
||
bucketStartMs: row.bucketStartMs,
|
||
bucketEndMs: row.bucketEndMs,
|
||
startAt: row.startAt,
|
||
endAt: row.endAt,
|
||
count: row.count,
|
||
requestPerMinute: row.requestPerMinute,
|
||
...extra(row),
|
||
valuesRedacted: true,
|
||
}));
|
||
const totalPeaks = peakForRows(buckets, alertThresholds.requestRateTotalRedPerMinute, "total", () => ({}));
|
||
const pagePeaks = peakForRows(pageBucketRows, alertThresholds.requestRatePageRedPerMinute, "page", (row) => ({
|
||
pageKey: row.pageKey,
|
||
pageRole: row.pageRole,
|
||
pageId: row.pageId,
|
||
pageEpoch: row.pageEpoch,
|
||
path: row.path,
|
||
}));
|
||
const apiPathPeaks = peakForRows(apiPathBucketRows, alertThresholds.requestRateApiPathRedPerMinute, "apiPath", (row) => ({
|
||
apiKey: row.apiKey,
|
||
method: row.method,
|
||
path: row.path,
|
||
pageKey: row.pageKey,
|
||
pageRole: row.pageRole,
|
||
pageId: row.pageId,
|
||
pageEpoch: row.pageEpoch,
|
||
}));
|
||
const pageCurves = Array.from(pageTotals.values())
|
||
.map((page) => {
|
||
const rows = pageBucketRows
|
||
.filter((row) => row.pageKey === page.pageKey)
|
||
.sort((a, b) => a.bucketStartMs - b.bucketStartMs);
|
||
const peak = maxByNumber(rows, (row) => row.requestPerMinute);
|
||
return {
|
||
...page,
|
||
bucketCount: rows.length,
|
||
peakRequestPerMinute: peak?.requestPerMinute ?? 0,
|
||
peakBucket: peak ?? null,
|
||
buckets: rows.slice(-60),
|
||
valuesRedacted: true,
|
||
};
|
||
})
|
||
.sort((a, b) => Number(b.peakRequestPerMinute ?? 0) - Number(a.peakRequestPerMinute ?? 0) || Number(b.count ?? 0) - Number(a.count ?? 0));
|
||
const apiPathCurves = Array.from(apiPathTotals.values())
|
||
.map((api) => {
|
||
const rows = apiPathBucketRows
|
||
.filter((row) => row.apiKey === api.apiKey)
|
||
.sort((a, b) => a.bucketStartMs - b.bucketStartMs);
|
||
const peak = maxByNumber(rows, (row) => row.requestPerMinute);
|
||
return {
|
||
...api,
|
||
bucketCount: rows.length,
|
||
peakRequestPerMinute: peak?.requestPerMinute ?? 0,
|
||
peakBucket: peak ?? null,
|
||
buckets: rows.slice(-60),
|
||
valuesRedacted: true,
|
||
};
|
||
})
|
||
.sort((a, b) => Number(b.peakRequestPerMinute ?? 0) - Number(a.peakRequestPerMinute ?? 0) || Number(b.count ?? 0) - Number(a.count ?? 0));
|
||
const totalPeak = maxByNumber(buckets, (row) => row.requestPerMinute);
|
||
const pagePeak = pageCurves[0] ?? null;
|
||
const apiPathPeak = apiPathCurves[0] ?? null;
|
||
const peaks = [...totalPeaks, ...pagePeaks, ...apiPathPeaks]
|
||
.sort((a, b) => Number(b.requestPerMinute ?? 0) - Number(a.requestPerMinute ?? 0) || String(a.scope).localeCompare(String(b.scope)));
|
||
return {
|
||
summary: {
|
||
bucketMs,
|
||
bucketSeconds: Number((bucketMs / 1000).toFixed(3)),
|
||
requestCount: naturalApiRequests.length,
|
||
bucketCount: buckets.length,
|
||
pageCount: pageCurves.length,
|
||
apiPathCount: apiPathCurves.length,
|
||
firstAt: new Date(firstBucketMs).toISOString(),
|
||
lastAt: new Date(lastBucketMs + bucketMs).toISOString(),
|
||
totalRedPerMinute: alertThresholds.requestRateTotalRedPerMinute,
|
||
pageRedPerMinute: alertThresholds.requestRatePageRedPerMinute,
|
||
apiPathRedPerMinute: alertThresholds.requestRateApiPathRedPerMinute,
|
||
totalPeakPerMinute: totalPeak?.requestPerMinute ?? 0,
|
||
totalPeakCount: totalPeak?.count ?? 0,
|
||
totalPeakAt: totalPeak?.startAt ?? null,
|
||
pagePeakPerMinute: pagePeak?.peakRequestPerMinute ?? 0,
|
||
pagePeakKey: pagePeak?.pageKey ?? null,
|
||
pagePeakPath: pagePeak?.path ?? null,
|
||
apiPathPeakPerMinute: apiPathPeak?.peakRequestPerMinute ?? 0,
|
||
apiPathPeakKey: apiPathPeak?.apiKey ?? null,
|
||
overThresholdPeakCount: peaks.length,
|
||
valuesRedacted: true,
|
||
},
|
||
buckets: buckets.slice(-120),
|
||
pageCurves: pageCurves.slice(0, 20),
|
||
apiPathCurves: apiPathCurves.slice(0, 40),
|
||
peaks: peaks.slice(0, 80),
|
||
valuesRedacted: true,
|
||
};
|
||
}
|
||
|
||
function requestRateEvent(item) {
|
||
const tsMs = Date.parse(String(item?.ts || ""));
|
||
const apiPath = normalizeApiRatePath(urlPath(item?.url));
|
||
if (!Number.isFinite(tsMs) || !isApiLikePath(apiPath)) return null;
|
||
const framePath = normalizeRoutePath(urlPath(item?.frameUrl || item?.url));
|
||
const pageRole = String(item?.pageRole || "unknown");
|
||
const pageId = String(item?.pageId || "unknown");
|
||
const pageEpoch = Number.isFinite(Number(item?.pageEpoch)) ? Number(item.pageEpoch) : null;
|
||
return {
|
||
ts: item.ts,
|
||
tsMs,
|
||
method: String(item?.method || "GET").toUpperCase(),
|
||
apiPath,
|
||
framePath,
|
||
pageRole,
|
||
pageId,
|
||
pageEpoch,
|
||
pageKey: [pageRole, pageId, pageEpoch ?? "-", framePath || "-"].join("|"),
|
||
valuesRedacted: true,
|
||
};
|
||
}
|
||
|
||
function normalizeApiRatePath(value) {
|
||
return normalizeRoutePath(value)
|
||
.replace(/\/sessions\/[^/?#]+/gu, "/sessions/:id")
|
||
.replace(/\/turns\/[^/?#]+/gu, "/turns/:id")
|
||
.replace(/\/traces\/[^/?#]+/gu, "/traces/:id")
|
||
.replace(/\/messages\/[^/?#]+/gu, "/messages/:id")
|
||
.replace(/\/files\/[^/?#]+/gu, "/files/:id")
|
||
.replace(/\/tasks\/[^/?#]+/gu, "/tasks/:id")
|
||
.replace(/\/runs\/[^/?#]+/gu, "/runs/:id")
|
||
.replace(/\/[0-9a-f]{8,}(?=\/|$)/giu, "/:id")
|
||
.replace(/\/[A-Za-z0-9_-]{24,}(?=\/|$)/gu, "/:id");
|
||
}
|
||
|
||
function normalizeRoutePath(value) {
|
||
const raw = String(value || "");
|
||
if (!raw || raw === "-") return "-";
|
||
return raw.replace(/\/+/gu, "/").replace(/\/$/u, "") || "/";
|
||
}
|
||
|
||
function compactRequestRateForOutput(report) {
|
||
if (!report || typeof report !== "object") return null;
|
||
return {
|
||
summary: report.summary ?? null,
|
||
buckets: Array.isArray(report.buckets) ? report.buckets.slice(-12) : [],
|
||
pageCurves: Array.isArray(report.pageCurves) ? report.pageCurves.slice(0, 8).map((item) => ({
|
||
pageKey: item.pageKey ?? null,
|
||
pageRole: item.pageRole ?? null,
|
||
pageId: item.pageId ?? null,
|
||
pageEpoch: item.pageEpoch ?? 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(-12) : [],
|
||
valuesRedacted: true,
|
||
})) : [],
|
||
apiPathCurves: Array.isArray(report.apiPathCurves) ? report.apiPathCurves.slice(0, 12).map((item) => ({
|
||
apiKey: item.apiKey ?? null,
|
||
method: item.method ?? 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(-12) : [],
|
||
valuesRedacted: true,
|
||
})) : [],
|
||
peaks: Array.isArray(report.peaks) ? report.peaks.slice(0, 12) : [],
|
||
valuesRedacted: true,
|
||
};
|
||
}
|
||
|
||
function buildPromptNetworkReport(control, network) {
|
||
const promptsById = new Map();
|
||
for (const item of control) {
|
||
if (item?.type !== "sendPrompt" || !item.commandId) continue;
|
||
const existing = promptsById.get(item.commandId) || {
|
||
commandId: item.commandId,
|
||
promptIndex: promptsById.size + 1,
|
||
promptTextHash: item.input?.textHash ?? null,
|
||
promptTextBytes: item.input?.textBytes ?? null,
|
||
startedAt: null,
|
||
completedAt: null,
|
||
failedAt: null,
|
||
phase: null
|
||
};
|
||
if (!existing.promptTextHash && item.input?.textHash) existing.promptTextHash = item.input.textHash;
|
||
if (!existing.promptTextBytes && item.input?.textBytes) existing.promptTextBytes = item.input.textBytes;
|
||
if (item.phase === "started") existing.startedAt = item.ts ?? existing.startedAt;
|
||
if (item.phase === "completed") existing.completedAt = item.ts ?? existing.completedAt;
|
||
if (item.phase === "failed") existing.failedAt = item.ts ?? existing.failedAt;
|
||
existing.phase = item.phase ?? existing.phase;
|
||
promptsById.set(item.commandId, existing);
|
||
}
|
||
const prompts = Array.from(promptsById.values()).sort((a, b) => Date.parse(a.startedAt || a.completedAt || a.failedAt || "") - Date.parse(b.startedAt || b.completedAt || b.failedAt || ""));
|
||
prompts.forEach((item, index) => { item.promptIndex = index + 1; });
|
||
const chatEvents = network
|
||
.filter((item) => String(item?.method || "").toUpperCase() === "POST" && promptSubmitModeForUrl(item?.url) !== null)
|
||
.map((item) => {
|
||
const failureText = item.failureKind ?? item.failure ?? item.errorText ?? null;
|
||
const urlPathValue = urlPath(item.url);
|
||
return {
|
||
ts: item.ts ?? null,
|
||
tsMs: Date.parse(item.ts),
|
||
type: item.type ?? null,
|
||
status: Number.isFinite(Number(item.status)) ? Number(item.status) : null,
|
||
commandId: item.commandId ?? null,
|
||
urlPath: urlPathValue,
|
||
submitMode: promptSubmitModeForUrl(item.url),
|
||
failureKind: failureText ? String(failureText) : null,
|
||
errorTextHash: failureText ? sha256(failureText) : null
|
||
};
|
||
})
|
||
.filter((item) => Number.isFinite(item.tsMs))
|
||
.sort((a, b) => a.tsMs - b.tsMs);
|
||
const rounds = prompts.map((prompt) => {
|
||
const startMs = Date.parse(prompt.startedAt || prompt.completedAt || prompt.failedAt || "");
|
||
const endAnchorMs = Date.parse(prompt.completedAt || prompt.failedAt || prompt.startedAt || "");
|
||
const fromMs = Number.isFinite(startMs) ? startMs - 3000 : Number.NEGATIVE_INFINITY;
|
||
const toMs = Number.isFinite(endAnchorMs) ? endAnchorMs + 30000 : Number.POSITIVE_INFINITY;
|
||
const events = chatEvents.filter((event) => {
|
||
if (event.commandId && prompt.commandId && event.commandId === prompt.commandId) return true;
|
||
return event.tsMs >= fromMs && event.tsMs <= toMs;
|
||
});
|
||
const responses = events.filter((event) => event.type === "response");
|
||
const failures = events.filter((event) => event.type === "requestfailed");
|
||
const responseStatuses = responses.map((event) => event.status).filter((status) => status !== null);
|
||
const submitModes = Array.from(new Set(events.map((event) => event.submitMode).filter(Boolean))).sort();
|
||
const chatPostOk = responseStatuses.some((status) => status >= 200 && status < 300);
|
||
const failureKind = chatPostOk
|
||
? null
|
||
: failures.length > 0
|
||
? "requestfailed"
|
||
: responseStatuses.length === 0
|
||
? "missing-response"
|
||
: "http-status";
|
||
return {
|
||
promptIndex: prompt.promptIndex,
|
||
promptCommandId: prompt.commandId,
|
||
promptTextHash: prompt.promptTextHash,
|
||
promptTextBytes: prompt.promptTextBytes,
|
||
startedAt: prompt.startedAt,
|
||
completedAt: prompt.completedAt,
|
||
failedAt: prompt.failedAt,
|
||
chatPostOk,
|
||
failureKind,
|
||
requestCount: events.filter((event) => event.type === "request").length,
|
||
responseCount: responses.length,
|
||
requestFailedCount: failures.length,
|
||
responseStatuses,
|
||
submitModes,
|
||
steerUsed: submitModes.includes("steer"),
|
||
firstChatEventAt: events[0]?.ts ?? null,
|
||
lastChatEventAt: events[events.length - 1]?.ts ?? null,
|
||
events: events.slice(0, 12).map((event) => ({ ts: event.ts, type: event.type, status: event.status, urlPath: event.urlPath, submitMode: event.submitMode, failureKind: event.failureKind, errorTextHash: event.errorTextHash }))
|
||
};
|
||
});
|
||
return {
|
||
summary: {
|
||
promptCount: rounds.length,
|
||
chatPostOk: rounds.filter((item) => item.chatPostOk === true).length,
|
||
chatPostFailed: rounds.filter((item) => item.chatPostOk === false).length,
|
||
chatPostMissing: rounds.filter((item) => item.failureKind === "missing-response").length
|
||
},
|
||
rounds
|
||
};
|
||
}
|
||
|
||
function promptSubmitModeForUrl(value) {
|
||
const pathValue = urlPath(value);
|
||
if (pathValue === "/v1/agent/chat") return "chat";
|
||
if (pathValue === "/v1/agent/chat/steer") return "steer";
|
||
return null;
|
||
}
|
||
|
||
function parseDomDiagnosticSummary(text) {
|
||
const value = String(text || "");
|
||
const traceMatch = value.match(/\b(?:trace_id=)?(trc_[A-Za-z0-9_-]+|[a-f0-9]{16,64})\b/iu);
|
||
const httpStatusMatch = value.match(/\bHTTP\s+([1-5][0-9]{2})\b/iu);
|
||
const idleMatch = value.match(/\bidle\s+(\d+)s\b/iu);
|
||
const waitingForMatch = value.match(/\bwaitingFor=([^\s;;,,)]+)/iu);
|
||
const lastEventLabelMatch = value.match(/\blastEventLabel=([^\s;;,,)]+)/iu);
|
||
const diagnosticCode = httpStatusMatch
|
||
? "http-" + httpStatusMatch[1]
|
||
: /turn\s*超过|无新活动/iu.test(value)
|
||
? "turn-idle-no-activity"
|
||
: /Failed to fetch/iu.test(value)
|
||
? "failed-to-fetch"
|
||
: "diagnostic";
|
||
return {
|
||
diagnosticCode,
|
||
traceId: traceMatch?.[1] || null,
|
||
httpStatus: httpStatusMatch ? Number(httpStatusMatch[1]) : null,
|
||
idleSeconds: idleMatch ? Number(idleMatch[1]) : null,
|
||
waitingFor: waitingForMatch?.[1] || null,
|
||
lastEventLabel: lastEventLabelMatch?.[1] || null
|
||
};
|
||
}
|
||
|
||
function isDomDiagnosticSampleText(text) {
|
||
const value = String(text || "").replace(/\s+/g, " ").trim();
|
||
if (!value) return false;
|
||
const strongDiagnostic = [
|
||
/\bHTTP\s+[45][0-9]{2}\b(?:[\s\S]{0,120}\btrace_id=|\b)/iu,
|
||
/\btrace_id=(?:trc_[A-Za-z0-9_-]+|[a-f0-9]{16,64})\b/iu,
|
||
/workbench\s+turn\s*超过\s*\d+ms\s*无新活动/iu,
|
||
/\bturn\s*超过\b[\s\S]{0,120}\b无新活动\b/iu,
|
||
/\bprojection-resume:sync-failed\b/iu,
|
||
/\bAgentRun\s+GET\b[\s\S]*\/result\b[\s\S]*timed out after\s+\d+ms\b/iu,
|
||
/\bFailed to fetch\b/iu,
|
||
/\bFailed to load resource\b[\s\S]{0,180}\bstatus of\s+[45][0-9]{2}\b/iu,
|
||
/\bserver responded with a status of\s+[45][0-9]{2}\b/iu,
|
||
/Code Agent\b[\s\S]{0,120}(?:无法连接上游|请求已结束)/iu
|
||
].some((pattern) => pattern.test(value));
|
||
if (!strongDiagnostic) return false;
|
||
const looksLikeToolStdout = /\b(?:stdout|stderr):/iu.test(value)
|
||
&& /(?:\becho\s+["']?===|\bnode\s+|\.tspy\b|tspy\/|===\s*[A-Za-z0-9_.-]+\s*===)/iu.test(value);
|
||
if (!looksLikeToolStdout) return true;
|
||
return /\b(?:trace_id=|HTTP\s+[45][0-9]{2}|workbench\s+turn\s*超过|projection-resume:sync-failed|Failed to fetch|Failed to load resource|server responded with a status of\s+[45][0-9]{2}|Code Agent\b[\s\S]{0,120}(?:无法连接上游|请求已结束))\b/iu.test(value);
|
||
}
|
||
|
||
function buildRuntimeAlerts(samples, control, network, consoleEvents, errors) {
|
||
const promptTimes = control
|
||
.filter((item) => item.type === "sendPrompt" && item.phase === "completed")
|
||
.map((item) => Date.parse(item.ts))
|
||
.filter(Number.isFinite)
|
||
.sort((a, b) => a - b);
|
||
const observerRefreshTimes = control
|
||
.filter((item) => item.type === "observer-periodic-refresh")
|
||
.map((item) => Date.parse(item.ts))
|
||
.filter(Number.isFinite)
|
||
.sort((a, b) => a - b);
|
||
const naturalNetwork = network.filter((item) => item?.observerInitiated !== true);
|
||
const httpErrors = naturalNetwork
|
||
.filter((item) => item?.type === "response" && Number(item.status) >= 400)
|
||
.map((item) => networkAlertEvent(item, promptTimes));
|
||
const requestFailed = naturalNetwork
|
||
.filter((item) => item?.type === "requestfailed")
|
||
.map((item) => networkAlertEvent(item, promptTimes));
|
||
const workbenchSessionListReadCount = naturalNetwork
|
||
.filter((item) => urlPath(item?.url) === "/v1/workbench/sessions")
|
||
.length;
|
||
const workbenchTraceEventsReadCount = naturalNetwork
|
||
.filter((item) => /^\/v1\/workbench\/traces\/[^/]+\/events$/u.test(urlPath(item?.url)))
|
||
.length;
|
||
const webPerformanceBeaconFailureCount = naturalNetwork
|
||
.filter((item) => urlPath(item?.url) === "/v1/web-performance" && (item?.type === "requestfailed" || Number(item?.status) >= 400))
|
||
.length;
|
||
const workbenchEventSourceFailureCount = naturalNetwork
|
||
.filter((item) => urlPath(item?.url) === "/v1/workbench/events" && (item?.type === "requestfailed" || Number(item?.status) >= 400))
|
||
.length;
|
||
const significantRequestFailed = requestFailed.filter(
|
||
(item) => !isBenignLongLivedStreamClosureAlert(item) && !isObserverRefreshClosureAlert(item, observerRefreshTimes),
|
||
);
|
||
const webPerformanceDiagnostics = extractWebPerformanceRuntimeDiagnostics(naturalNetwork, promptTimes);
|
||
const webPerformancePayloadStates = summarizeWebPerformancePayloadStates(naturalNetwork);
|
||
const webPerformanceDiagnosticGroups = groupWebPerformanceRuntimeDiagnostics(webPerformanceDiagnostics);
|
||
const domDiagnostics = [];
|
||
const executionErrors = [];
|
||
const baselineExecutionErrors = [];
|
||
const canarySessionIds = sessionInvariantCanarySessionIds(control);
|
||
const firstPromptMs = promptTimes.length > 0 ? promptTimes[0] : Infinity;
|
||
const firstSeenExecutionErrorMs = new Map();
|
||
for (const sample of samples) {
|
||
const tsMs = Date.parse(sample?.ts);
|
||
const promptIndex = Number.isFinite(tsMs) ? latestPromptIndex(promptTimes, tsMs) : 0;
|
||
if (Array.isArray(sample?.diagnostics)) {
|
||
for (const diagnostic of sample.diagnostics.slice(0, 12)) {
|
||
const text = diagnostic?.textPreview || diagnostic?.text || "";
|
||
if (!String(text).trim()) continue;
|
||
const parsedDiagnostic = parseDomDiagnosticSummary(text);
|
||
domDiagnostics.push({
|
||
seq: sample.seq ?? null,
|
||
ts: sample.ts ?? null,
|
||
promptIndex,
|
||
source: "diagnostic-node",
|
||
className: diagnostic.className ?? null,
|
||
diagnosticCode: diagnostic.diagnosticCode ?? parsedDiagnostic.diagnosticCode,
|
||
traceId: diagnostic.traceId ?? parsedDiagnostic.traceId,
|
||
httpStatus: diagnostic.httpStatus ?? parsedDiagnostic.httpStatus,
|
||
idleSeconds: diagnostic.idleSeconds ?? parsedDiagnostic.idleSeconds,
|
||
waitingFor: diagnostic.waitingFor ?? parsedDiagnostic.waitingFor,
|
||
lastEventLabel: diagnostic.lastEventLabel ?? parsedDiagnostic.lastEventLabel,
|
||
compact: diagnostic.compact ?? null,
|
||
expanded: diagnostic.expanded ?? null,
|
||
routeSessionId: sample.routeSessionId ?? null,
|
||
activeSessionId: sample.activeSessionId ?? null,
|
||
textHash: diagnostic.textHash || sha256(text),
|
||
preview: limitText(text, 260)
|
||
});
|
||
}
|
||
}
|
||
const texts = sampleTexts(sample).filter(isDomDiagnosticSampleText);
|
||
for (const text of texts.slice(0, 4)) {
|
||
const parsedDiagnostic = parseDomDiagnosticSummary(text);
|
||
domDiagnostics.push({
|
||
seq: sample.seq ?? null,
|
||
ts: sample.ts ?? null,
|
||
promptIndex,
|
||
source: "sample-text",
|
||
diagnosticCode: parsedDiagnostic.diagnosticCode,
|
||
traceId: parsedDiagnostic.traceId,
|
||
httpStatus: parsedDiagnostic.httpStatus,
|
||
idleSeconds: parsedDiagnostic.idleSeconds,
|
||
waitingFor: parsedDiagnostic.waitingFor,
|
||
lastEventLabel: parsedDiagnostic.lastEventLabel,
|
||
routeSessionId: sample.routeSessionId ?? null,
|
||
activeSessionId: sample.activeSessionId ?? null,
|
||
textHash: sha256(text),
|
||
preview: limitText(text, 220)
|
||
});
|
||
}
|
||
const seenExecutionErrors = new Set();
|
||
for (const candidate of sampleExecutionErrorCandidates(sample)) {
|
||
const parsed = parseExecutionErrorText(candidate.text);
|
||
if (!parsed) continue;
|
||
const textHash = sha256(candidate.text);
|
||
const dedupeKey = [candidate.source, candidate.traceId || "-", parsed.backend || "-", parsed.code || "-", parsed.status || "-", textHash].join("|");
|
||
if (seenExecutionErrors.has(dedupeKey)) continue;
|
||
seenExecutionErrors.add(dedupeKey);
|
||
const firstSeenMs = firstSeenExecutionErrorMs.has(dedupeKey) ? firstSeenExecutionErrorMs.get(dedupeKey) : tsMs;
|
||
if (!firstSeenExecutionErrorMs.has(dedupeKey) && Number.isFinite(tsMs)) firstSeenExecutionErrorMs.set(dedupeKey, tsMs);
|
||
const baseline = Number.isFinite(firstSeenMs) && firstSeenMs < firstPromptMs;
|
||
const event = {
|
||
seq: sample.seq ?? null,
|
||
ts: sample.ts ?? null,
|
||
promptIndex,
|
||
baseline,
|
||
firstSeenAt: Number.isFinite(firstSeenMs) ? new Date(firstSeenMs).toISOString() : null,
|
||
source: candidate.source,
|
||
backend: parsed.backend,
|
||
status: parsed.status,
|
||
code: parsed.code,
|
||
rawCode: parsed.rawCode,
|
||
totalSeconds: parsed.totalSeconds,
|
||
traceId: candidate.traceId || parsed.traceId || null,
|
||
messageId: candidate.messageId || null,
|
||
routeSessionId: sample.routeSessionId ?? null,
|
||
activeSessionId: sample.activeSessionId ?? null,
|
||
textHash,
|
||
preview: limitText(candidate.text, 260)
|
||
};
|
||
const eventSessions = [event.routeSessionId, event.activeSessionId].filter(Boolean);
|
||
const nonCanarySession = canarySessionIds.size > 0 && !eventSessions.some((sessionId) => canarySessionIds.has(sessionId));
|
||
if (baseline || nonCanarySession) baselineExecutionErrors.push({ ...event, baseline: true, baselineReason: nonCanarySession ? "non-canary-session" : "pre-first-prompt" });
|
||
else executionErrors.push(event);
|
||
domDiagnostics.push({
|
||
seq: sample.seq ?? null,
|
||
ts: sample.ts ?? null,
|
||
promptIndex,
|
||
source: "execution-row",
|
||
diagnosticCode: parsed.rawCode || parsed.code || "execution-error",
|
||
traceId: candidate.traceId || parsed.traceId || null,
|
||
routeSessionId: sample.routeSessionId ?? null,
|
||
activeSessionId: sample.activeSessionId ?? null,
|
||
textHash,
|
||
preview: limitText(candidate.text, 220)
|
||
});
|
||
}
|
||
}
|
||
const consoleAlerts = consoleEvents
|
||
.filter((item) => /error|warning|warn|assert/iu.test(String(item?.type || "")) || isDiagnosticText(item?.text))
|
||
.map((item) => consoleAlertEvent(item, promptTimes));
|
||
const significantConsoleAlerts = consoleAlerts.filter((item) => !isBenignLongLivedStreamClosureAlert(item) && !isObserverRefreshClosureAlert(item, observerRefreshTimes));
|
||
const pageErrors = errors.map((item) => ({
|
||
ts: item.ts ?? null,
|
||
promptIndex: promptIndexForTs(promptTimes, item.ts),
|
||
type: item.type ?? null,
|
||
pageRole: item.pageRole ?? item.error?.details?.pageRole ?? null,
|
||
pageId: item.pageId ?? item.error?.details?.pageId ?? null,
|
||
routeSessionId: item.routeSessionId ?? item.error?.details?.routeSessionId ?? null,
|
||
activeSessionId: item.activeSessionId ?? item.error?.details?.activeSessionId ?? null,
|
||
commandId: item.commandId ?? item.error?.details?.commandId ?? null,
|
||
sampleSeq: item.sampleSeq ?? item.error?.details?.sampleSeq ?? null,
|
||
timeoutMs: timeoutMsFromMessage(item.error?.message || item.message || item.error || ""),
|
||
errorName: item.error?.name ?? item.name ?? null,
|
||
messageHash: item.error?.message ? sha256(item.error.message) : item.message ? sha256(item.message) : null,
|
||
preview: limitText(item.error?.message || item.message || item.error || "", 220)
|
||
}));
|
||
return {
|
||
summary: {
|
||
httpErrorCount: httpErrors.length,
|
||
requestFailedCount: requestFailed.length,
|
||
significantRequestFailedCount: significantRequestFailed.length,
|
||
workbenchSessionListReadCount,
|
||
workbenchTraceEventsReadCount,
|
||
webPerformanceBeaconFailureCount,
|
||
webPerformancePayloadRequestCount: webPerformancePayloadStates.total,
|
||
webPerformancePayloadParsedCount: webPerformancePayloadStates.parsed,
|
||
webPerformancePayloadParseIssueCount: webPerformancePayloadStates.parseIssue,
|
||
webPerformanceRuntimeDiagnosticCount: webPerformanceDiagnostics.length,
|
||
webPerformanceRuntimeDiagnosticGroupCount: webPerformanceDiagnosticGroups.length,
|
||
workbenchEventSourceFailureCount,
|
||
benignLongLivedStreamClosureCount: requestFailed.length - significantRequestFailed.length,
|
||
domDiagnosticSampleCount: domDiagnostics.length,
|
||
domDiagnosticGroupCount: groupDomDiagnostics(domDiagnostics).length,
|
||
executionErrorCount: executionErrors.length,
|
||
baselineExecutionErrorCount: baselineExecutionErrors.length,
|
||
consoleAlertCount: consoleAlerts.length,
|
||
significantConsoleAlertCount: significantConsoleAlerts.length,
|
||
pageErrorCount: pageErrors.length,
|
||
networkErrorGroupCount: groupNetworkAlerts(httpErrors).length,
|
||
requestFailedGroupCount: groupNetworkAlerts(requestFailed).length,
|
||
significantRequestFailedGroupCount: groupNetworkAlerts(significantRequestFailed).length,
|
||
executionErrorGroupCount: groupExecutionErrors(executionErrors).length,
|
||
baselineExecutionErrorGroupCount: groupExecutionErrors(baselineExecutionErrors).length,
|
||
consoleAlertGroupCount: groupConsoleAlerts(consoleAlerts).length,
|
||
significantConsoleAlertGroupCount: groupConsoleAlerts(significantConsoleAlerts).length
|
||
},
|
||
networkHttpErrorsByPath: groupNetworkAlerts(httpErrors),
|
||
networkRequestFailedByPath: groupNetworkAlerts(requestFailed),
|
||
networkSignificantRequestFailedByPath: groupNetworkAlerts(significantRequestFailed),
|
||
webPerformancePayloadStates,
|
||
webPerformanceRuntimeDiagnostics: webPerformanceDiagnostics.slice(0, 120),
|
||
webPerformanceRuntimeDiagnosticsByCode: webPerformanceDiagnosticGroups,
|
||
domDiagnostics: domDiagnostics.slice(-80),
|
||
domDiagnosticsByText: groupDomDiagnostics(domDiagnostics),
|
||
domDiagnosticsByFingerprint: groupDomDiagnostics(domDiagnostics).slice(0, 80),
|
||
runtimeExecutionErrors: executionErrors.slice(0, 120),
|
||
runtimeExecutionErrorsByCode: groupExecutionErrors(executionErrors),
|
||
baselineRuntimeExecutionErrors: baselineExecutionErrors.slice(0, 80),
|
||
baselineRuntimeExecutionErrorsByCode: groupExecutionErrors(baselineExecutionErrors),
|
||
consoleAlerts: consoleAlerts.slice(0, 80),
|
||
consoleAlertsByPath: groupConsoleAlerts(consoleAlerts),
|
||
significantConsoleAlerts: significantConsoleAlerts.slice(0, 80),
|
||
significantConsoleAlertsByPath: groupConsoleAlerts(significantConsoleAlerts),
|
||
pageErrors: pageErrors.slice(0, 40)
|
||
};
|
||
}
|
||
|
||
function groupDomDiagnostics(events) {
|
||
const groups = new Map();
|
||
for (const item of events || []) {
|
||
const preview = String(item?.preview || "").trim();
|
||
if (!isReportableDomDiagnostic(item, preview)) continue;
|
||
const normalizedPreview = normalizeDiagnosticPreview(preview);
|
||
const key = [
|
||
item?.diagnosticCode || "",
|
||
normalizedPreview
|
||
].join("|");
|
||
const existing = groups.get(key) || {
|
||
source: item?.source || null,
|
||
sources: new Set(),
|
||
diagnosticCode: item?.diagnosticCode || null,
|
||
textHash: item?.textHash || null,
|
||
normalizedPreview,
|
||
preview,
|
||
count: 0,
|
||
firstAt: item?.ts || null,
|
||
lastAt: item?.ts || null,
|
||
promptIndexes: new Set(),
|
||
traceIds: new Set(),
|
||
sampleSeqs: []
|
||
};
|
||
if (item?.source) existing.sources.add(String(item.source));
|
||
existing.count += 1;
|
||
existing.firstAt = minIso(existing.firstAt, item?.ts || null);
|
||
existing.lastAt = maxIso(existing.lastAt, item?.ts || null);
|
||
if (Number.isFinite(Number(item?.promptIndex))) existing.promptIndexes.add(Number(item.promptIndex));
|
||
for (const traceId of extractDiagnosticTraceIds(item, preview)) existing.traceIds.add(traceId);
|
||
if (existing.sampleSeqs.length < 12 && item?.seq !== undefined && item?.seq !== null) existing.sampleSeqs.push(item.seq);
|
||
groups.set(key, existing);
|
||
}
|
||
return Array.from(groups.values())
|
||
.map((item) => ({
|
||
source: item.source,
|
||
sources: Array.from(item.sources).sort(),
|
||
diagnosticCode: item.diagnosticCode,
|
||
textHash: item.textHash,
|
||
normalizedPreview: item.normalizedPreview,
|
||
preview: item.preview,
|
||
count: item.count,
|
||
firstAt: item.firstAt,
|
||
lastAt: item.lastAt,
|
||
promptIndexes: Array.from(item.promptIndexes).sort((a, b) => a - b),
|
||
traceIds: Array.from(item.traceIds).sort(),
|
||
sampleSeqs: item.sampleSeqs
|
||
}))
|
||
.sort((a, b) => (b.count - a.count) || String(a.firstAt || "").localeCompare(String(b.firstAt || "")));
|
||
}
|
||
|
||
function extractWebPerformanceRuntimeDiagnostics(network, promptTimes) {
|
||
const rows = [];
|
||
for (const item of Array.isArray(network) ? network : []) {
|
||
const payload = item?.webPerformancePayload && typeof item.webPerformancePayload === "object" ? item.webPerformancePayload : null;
|
||
if (!payload || payload.parseStatus !== "parsed" || payload.schemaVersion !== "hwlab-web-performance-v2") continue;
|
||
const events = Array.isArray(payload.events) ? payload.events : [];
|
||
for (const event of events) {
|
||
if (!event || typeof event !== "object") continue;
|
||
const diagnosticCode = limitText(event.diagnosticCode || event.code || "", 120);
|
||
const reason = limitText(event.reason || "", 120);
|
||
const eventType = limitText(event.eventType || event.type || event.kind || "", 120);
|
||
if (!diagnosticCode && !reason && !eventType) continue;
|
||
rows.push({
|
||
ts: item.ts ?? event.ts ?? null,
|
||
promptIndex: promptIndexForTs(promptTimes, item.ts ?? event.ts),
|
||
pageRole: item.pageRole ?? null,
|
||
pageId: item.pageId ?? null,
|
||
commandId: item.commandId ?? null,
|
||
method: item.method ?? null,
|
||
urlPath: urlPath(item.url),
|
||
schemaVersion: payload.schemaVersion,
|
||
captureStatus: payload.captureStatus ?? null,
|
||
parseStatus: payload.parseStatus ?? null,
|
||
byteCount: numberOrNull(payload.byteCount),
|
||
bodyHash: payload.bodyHash ?? null,
|
||
eventType,
|
||
diagnosticCode,
|
||
reason,
|
||
module: limitText(event.module || "", 120),
|
||
traceId: event.traceId ?? null,
|
||
sessionIdHash: event.sessionIdHash ?? null,
|
||
eventIdHash: event.eventIdHash ?? null,
|
||
eventCount: numberOrNull(event.eventCount),
|
||
deliveredCount: numberOrNull(event.deliveredCount),
|
||
chunkCount: numberOrNull(event.chunkCount),
|
||
flushDurationMs: numberOrNull(event.flushDurationMs),
|
||
droppedCount: numberOrNull(event.droppedCount),
|
||
maxItemsPerChunk: numberOrNull(event.maxItemsPerChunk),
|
||
maxChunkMs: numberOrNull(event.maxChunkMs),
|
||
replacedByKey: typeof event.replacedByKey === "boolean" || typeof event.replacedByKey === "number" ? event.replacedByKey : null,
|
||
replacedByKeyHash: event.replacedByKeyHash ?? null,
|
||
valuesRedacted: true,
|
||
});
|
||
}
|
||
}
|
||
return rows.sort((left, right) => String(left.ts || "").localeCompare(String(right.ts || ""))).slice(-200);
|
||
}
|
||
|
||
function summarizeWebPerformancePayloadStates(network) {
|
||
const stateCounts = new Map();
|
||
let total = 0;
|
||
let parsed = 0;
|
||
let parseIssue = 0;
|
||
let overLimit = 0;
|
||
let invalidJson = 0;
|
||
let missingBody = 0;
|
||
let unsupportedSchema = 0;
|
||
let capturedEventCount = 0;
|
||
let storedEventCount = 0;
|
||
for (const item of Array.isArray(network) ? network : []) {
|
||
const payload = item?.webPerformancePayload && typeof item.webPerformancePayload === "object" ? item.webPerformancePayload : null;
|
||
if (!payload) continue;
|
||
total += 1;
|
||
const state = String(payload.parseStatus || payload.captureStatus || "unknown");
|
||
stateCounts.set(state, (stateCounts.get(state) || 0) + 1);
|
||
if (payload.parseStatus === "parsed") parsed += 1;
|
||
else parseIssue += 1;
|
||
if (payload.parseStatus === "not-parsed-over-limit" || payload.captureStatus === "skipped-over-limit") overLimit += 1;
|
||
if (payload.parseStatus === "invalid-json") invalidJson += 1;
|
||
if (payload.parseStatus === "missing-body") missingBody += 1;
|
||
if (payload.parseStatus === "unsupported-schema") unsupportedSchema += 1;
|
||
if (Number.isFinite(Number(payload.eventCount))) capturedEventCount += Number(payload.eventCount);
|
||
if (Number.isFinite(Number(payload.storedEventCount))) storedEventCount += Number(payload.storedEventCount);
|
||
}
|
||
return {
|
||
total,
|
||
parsed,
|
||
parseIssue,
|
||
overLimit,
|
||
invalidJson,
|
||
missingBody,
|
||
unsupportedSchema,
|
||
capturedEventCount,
|
||
storedEventCount,
|
||
states: Array.from(stateCounts.entries()).map(([state, count]) => ({ state, count })).sort((a, b) => b.count - a.count || a.state.localeCompare(b.state)),
|
||
valuesRedacted: true,
|
||
};
|
||
}
|
||
|
||
function groupWebPerformanceRuntimeDiagnostics(events) {
|
||
const groups = new Map();
|
||
for (const item of Array.isArray(events) ? events : []) {
|
||
const key = [item.diagnosticCode || item.eventType || "-", item.reason || "-", item.module || "-"].join("|");
|
||
const group = groups.get(key) || {
|
||
diagnosticCode: item.diagnosticCode || null,
|
||
reason: item.reason || null,
|
||
module: item.module || null,
|
||
eventType: item.eventType || null,
|
||
count: 0,
|
||
firstAt: item.ts || null,
|
||
lastAt: item.ts || null,
|
||
promptIndexes: new Set(),
|
||
traceIds: new Set(),
|
||
eventCount: 0,
|
||
deliveredCount: 0,
|
||
chunkCount: 0,
|
||
droppedCount: 0,
|
||
maxFlushDurationMs: null,
|
||
maxItemsPerChunk: null,
|
||
maxChunkMs: null,
|
||
replacedByKeyCount: 0,
|
||
examples: [],
|
||
};
|
||
group.count += 1;
|
||
group.firstAt = minIso(group.firstAt, item.ts || null);
|
||
group.lastAt = maxIso(group.lastAt, item.ts || null);
|
||
if (Number.isFinite(Number(item.promptIndex))) group.promptIndexes.add(Number(item.promptIndex));
|
||
if (item.traceId) group.traceIds.add(String(item.traceId));
|
||
group.eventCount += Number(item.eventCount || 0);
|
||
group.deliveredCount += Number(item.deliveredCount || 0);
|
||
group.chunkCount += Number(item.chunkCount || 0);
|
||
group.droppedCount += Number(item.droppedCount || 0);
|
||
group.maxFlushDurationMs = maxRuntimeNumber(group.maxFlushDurationMs, item.flushDurationMs);
|
||
group.maxItemsPerChunk = maxRuntimeNumber(group.maxItemsPerChunk, item.maxItemsPerChunk);
|
||
group.maxChunkMs = maxRuntimeNumber(group.maxChunkMs, item.maxChunkMs);
|
||
if (Number.isFinite(Number(item.replacedByKey))) group.replacedByKeyCount += Number(item.replacedByKey);
|
||
else if (item.replacedByKey === true || item.replacedByKeyHash) group.replacedByKeyCount += 1;
|
||
if (group.examples.length < 6) group.examples.push({
|
||
ts: item.ts || null,
|
||
traceId: item.traceId || null,
|
||
eventCount: item.eventCount ?? null,
|
||
deliveredCount: item.deliveredCount ?? null,
|
||
chunkCount: item.chunkCount ?? null,
|
||
flushDurationMs: item.flushDurationMs ?? null,
|
||
droppedCount: item.droppedCount ?? null,
|
||
maxItemsPerChunk: item.maxItemsPerChunk ?? null,
|
||
maxChunkMs: item.maxChunkMs ?? null,
|
||
replacedByKey: item.replacedByKey ?? null,
|
||
replacedByKeyHash: item.replacedByKeyHash ?? null,
|
||
valuesRedacted: true,
|
||
});
|
||
groups.set(key, group);
|
||
}
|
||
return Array.from(groups.values()).map((item) => ({
|
||
diagnosticCode: item.diagnosticCode,
|
||
reason: item.reason,
|
||
module: item.module,
|
||
eventType: item.eventType,
|
||
count: item.count,
|
||
firstAt: item.firstAt,
|
||
lastAt: item.lastAt,
|
||
promptIndexes: Array.from(item.promptIndexes).sort((a, b) => a - b),
|
||
traceIds: Array.from(item.traceIds).sort().slice(0, 12),
|
||
eventCount: item.eventCount,
|
||
deliveredCount: item.deliveredCount,
|
||
chunkCount: item.chunkCount,
|
||
droppedCount: item.droppedCount,
|
||
maxFlushDurationMs: item.maxFlushDurationMs,
|
||
maxItemsPerChunk: item.maxItemsPerChunk,
|
||
maxChunkMs: item.maxChunkMs,
|
||
replacedByKeyCount: item.replacedByKeyCount,
|
||
examples: item.examples,
|
||
valuesRedacted: true,
|
||
})).sort((left, right) => right.count - left.count || String(left.firstAt || "").localeCompare(String(right.firstAt || "")));
|
||
}
|
||
|
||
function maxRuntimeNumber(current, candidate) {
|
||
const value = Number(candidate);
|
||
if (!Number.isFinite(value)) return current ?? null;
|
||
const existing = Number(current);
|
||
return Number.isFinite(existing) ? Math.max(existing, value) : value;
|
||
}
|
||
|
||
function isReportableDomDiagnostic(item, preview) {
|
||
if (item?.source === "diagnostic-node" || item?.source === "execution-row") return true;
|
||
return /trace_id=|HTTP\s+\d{3}\b|Failed to load resource|ERR_[A-Z_]+|provider-unavailable|AgentRun error|超过\s*\d+\s*ms\s*无新活动|代理暂时无法连接上游|Trace 更新超时|加载失败/iu.test(String(preview || ""));
|
||
}
|
||
|
||
function normalizeDiagnosticPreview(text) {
|
||
return String(text || "")
|
||
.replace(/trace_id=[A-Za-z0-9_-]+/gu, "trace_id=:traceId")
|
||
.replace(/\btrc_[A-Za-z0-9_-]+\b/gu, "trc_:traceId")
|
||
.replace(/\bses_[A-Za-z0-9_-]+\b/gu, "ses_:sessionId")
|
||
.replace(/\brun_[A-Za-z0-9_-]+\b/gu, "run_:runId")
|
||
.replace(/\bcmd_[A-Za-z0-9_-]+\b/gu, "cmd_:commandId")
|
||
.replace(/[!!]+$/gu, "")
|
||
.replace(/\s+/gu, " ")
|
||
.trim();
|
||
}
|
||
|
||
function extractDiagnosticTraceIds(item, preview) {
|
||
const ids = new Set();
|
||
if (item?.traceId) ids.add(String(item.traceId));
|
||
const text = String(preview || "");
|
||
for (const match of text.matchAll(/\btrc_[A-Za-z0-9_-]+\b/gu)) ids.add(match[0]);
|
||
for (const match of text.matchAll(/trace_id=([A-Za-z0-9_-]+)/gu)) ids.add(match[1]);
|
||
return ids;
|
||
}
|
||
|
||
function minIso(a, b) {
|
||
if (!a) return b || null;
|
||
if (!b) return a || null;
|
||
return Date.parse(a) <= Date.parse(b) ? a : b;
|
||
}
|
||
|
||
function maxIso(a, b) {
|
||
if (!a) return b || null;
|
||
if (!b) return a || null;
|
||
return Date.parse(a) >= Date.parse(b) ? a : b;
|
||
}
|
||
|
||
function sampleExecutionErrorCandidates(sample) {
|
||
const candidates = [];
|
||
const add = (source, items) => {
|
||
if (!Array.isArray(items)) return;
|
||
for (const item of items) {
|
||
const text = String(item?.textPreview || item?.text || item?.preview || "").trim();
|
||
if (!text) continue;
|
||
if (!parseExecutionErrorText(text)) continue;
|
||
candidates.push({
|
||
source,
|
||
text,
|
||
traceId: item?.traceId ?? null,
|
||
messageId: item?.messageId ?? null,
|
||
status: item?.status ?? null
|
||
});
|
||
}
|
||
};
|
||
add("diagnostic-node", sample?.diagnostics);
|
||
add("message", sample?.messages);
|
||
add("trace-row", sample?.traceRows);
|
||
add("turn", sample?.turns);
|
||
const specific = candidates.filter((candidate) => {
|
||
const parsed = parseExecutionErrorText(candidate.text);
|
||
return parsed && parsed.code !== "error";
|
||
});
|
||
return specific.length > 0 ? specific : candidates;
|
||
}
|
||
|
||
function parseExecutionErrorText(text) {
|
||
const value = String(text || "");
|
||
const agentRunCodeMatch = value.match(/\bagentrun:error:([A-Za-z0-9_.:-]+)/u);
|
||
const agentRunText = /\bAgentRun\s+error\b|\bagentrun:error:/iu.test(value);
|
||
const providerUnavailable = /\bprovider[-_\s]*unavailable\b/iu.test(value);
|
||
if (!agentRunCodeMatch && !agentRunText && !providerUnavailable) return null;
|
||
const statusMatch = value.match(/\b(fail(?:ed)?|error|blocked|cancel(?:ed)?)\b/iu);
|
||
const traceMatch = value.match(/\btrc_[A-Za-z0-9_-]+\b/u);
|
||
const totalMatch = value.match(/\btotal\s*=\s*([0-9]{1,2}:[0-9]{2}(?::[0-9]{2})?)\b/iu)
|
||
|| value.match(/总耗时\s*[::]?\s*([0-9]{1,2}:[0-9]{2}(?::[0-9]{2})?)/iu);
|
||
const agentRunCode = cleanExecutionCode(agentRunCodeMatch?.[1] || "");
|
||
const rawCode = agentRunCode ? "agentrun:error:" + agentRunCode : providerUnavailable ? "provider-unavailable" : "agentrun:error";
|
||
return {
|
||
backend: agentRunText || agentRunCodeMatch ? "agentrun" : "unknown",
|
||
status: normalizeExecutionStatus(statusMatch?.[1] || "error"),
|
||
code: agentRunCode || (providerUnavailable ? "provider-unavailable" : "error"),
|
||
rawCode,
|
||
totalSeconds: totalMatch ? parseClockDurationSeconds(totalMatch[1]) : null,
|
||
traceId: traceMatch?.[0] || null
|
||
};
|
||
}
|
||
|
||
function cleanExecutionCode(code) {
|
||
const value = String(code || "").replace(/(?:AgentRun|Error|Failed).*$/u, "").replace(/[^A-Za-z0-9_.:-].*$/u, "");
|
||
return value || null;
|
||
}
|
||
|
||
function normalizeExecutionStatus(status) {
|
||
const value = String(status || "").toLowerCase();
|
||
if (value === "failed") return "fail";
|
||
if (value === "cancelled" || value === "canceled") return "cancel";
|
||
return value || "error";
|
||
}
|
||
|
||
function parseClockDurationSeconds(value) {
|
||
const parts = String(value || "").split(":").map((part) => Number(part));
|
||
if (parts.length === 2 && parts.every(Number.isFinite)) return parts[0] * 60 + parts[1];
|
||
if (parts.length === 3 && parts.every(Number.isFinite)) return parts[0] * 3600 + parts[1] * 60 + parts[2];
|
||
return null;
|
||
}
|
||
|
||
function groupExecutionErrors(events) {
|
||
const groups = new Map();
|
||
for (const event of events) {
|
||
const key = [event.backend || "-", event.status || "-", event.code || "-"].join(" ");
|
||
const group = groups.get(key) || {
|
||
backend: event.backend ?? null,
|
||
status: event.status ?? null,
|
||
code: event.code ?? null,
|
||
rawCode: event.rawCode ?? null,
|
||
count: 0,
|
||
firstAt: event.ts,
|
||
lastAt: event.ts,
|
||
promptIndexes: [],
|
||
traceIds: [],
|
||
sources: []
|
||
};
|
||
group.count += 1;
|
||
group.lastAt = event.ts;
|
||
if (event.promptIndex && !group.promptIndexes.includes(event.promptIndex)) group.promptIndexes.push(event.promptIndex);
|
||
if (event.traceId && !group.traceIds.includes(event.traceId)) group.traceIds.push(event.traceId);
|
||
if (event.source && !group.sources.includes(event.source)) group.sources.push(event.source);
|
||
groups.set(key, group);
|
||
}
|
||
return Array.from(groups.values()).sort((a, b) => b.count - a.count || String(a.code).localeCompare(String(b.code)));
|
||
}
|
||
|
||
function consoleAlertEvent(item, promptTimes) {
|
||
const text = String(item?.text || "");
|
||
const statusMatch = text.match(/\bstatus\s+of\s+([1-5][0-9]{2})\b/iu) || text.match(/\bHTTP\s+([1-5][0-9]{2})\b/iu);
|
||
const location = compactLocation(item.location);
|
||
const traceMatch = (location?.urlPath || text).match(/\btrc_[A-Za-z0-9_-]+\b/u);
|
||
return {
|
||
ts: item.ts ?? null,
|
||
promptIndex: promptIndexForTs(promptTimes, item.ts),
|
||
type: item.type ?? null,
|
||
status: statusMatch ? Number(statusMatch[1]) : null,
|
||
urlPath: location?.urlPath || "-",
|
||
traceId: traceMatch?.[0] || null,
|
||
textHash: item.text ? sha256(item.text) : null,
|
||
preview: limitText(text, 220),
|
||
location
|
||
};
|
||
}
|
||
|
||
function groupConsoleAlerts(events) {
|
||
const groups = new Map();
|
||
for (const event of events) {
|
||
const key = [event.type || "-", event.status ?? "-", event.urlPath || "-"].join(" ");
|
||
const group = groups.get(key) || {
|
||
type: event.type ?? null,
|
||
status: event.status ?? null,
|
||
urlPath: event.urlPath || "-",
|
||
count: 0,
|
||
firstAt: event.ts,
|
||
lastAt: event.ts,
|
||
promptIndexes: [],
|
||
traceIds: []
|
||
};
|
||
group.count += 1;
|
||
group.lastAt = event.ts;
|
||
if (event.promptIndex && !group.promptIndexes.includes(event.promptIndex)) group.promptIndexes.push(event.promptIndex);
|
||
if (event.traceId && !group.traceIds.includes(event.traceId)) group.traceIds.push(event.traceId);
|
||
groups.set(key, group);
|
||
}
|
||
return Array.from(groups.values()).sort((a, b) => b.count - a.count || String(a.urlPath).localeCompare(String(b.urlPath)));
|
||
}
|
||
|
||
function isBenignLongLivedStreamClosureAlert(event) {
|
||
if (event?.urlPath !== "/v1/workbench/events") return false;
|
||
if (event.status !== null && event.status !== undefined && Number(event.status) > 0) return false;
|
||
const text = String(event.failureKind || event.errorPreview || event.preview || "");
|
||
return /ERR_NETWORK_CHANGED|ERR_ABORTED|net::ERR_NETWORK_CHANGED|net::ERR_ABORTED|aborted|network changed/iu.test(text);
|
||
}
|
||
|
||
function isObserverRefreshClosureAlert(event, observerRefreshTimes) {
|
||
const urlPath = String(event?.urlPath || "");
|
||
if (!["/v1/workbench/events", "/v1/web-performance"].includes(urlPath) && !/^\/v1\/workbench\/traces\/[^/]+\/events$/u.test(urlPath)) return false;
|
||
if (event.status !== null && event.status !== undefined && Number(event.status) > 0) return false;
|
||
const text = String(event.failureKind || event.errorPreview || event.preview || "");
|
||
if (!/ERR_NETWORK_CHANGED|ERR_ABORTED|ERR_INCOMPLETE_CHUNKED_ENCODING|ERR_INVALID_CHUNKED_ENCODING|net::ERR_NETWORK_CHANGED|net::ERR_ABORTED|aborted|network changed|incomplete chunked|invalid chunked/iu.test(text)) return false;
|
||
const ts = Date.parse(String(event.ts || ""));
|
||
return Number.isFinite(ts) && observerRefreshTimes.some((refreshTs) => Math.abs(ts - refreshTs) <= 8000);
|
||
}
|
||
|
||
function networkAlertEvent(item, promptTimes) {
|
||
const failureText = item.failureKind ?? item.failure ?? item.errorText ?? null;
|
||
return {
|
||
ts: item.ts ?? null,
|
||
promptIndex: promptIndexForTs(promptTimes, item.ts),
|
||
method: String(item.method || "GET").toUpperCase(),
|
||
status: Number.isFinite(Number(item.status)) ? Number(item.status) : null,
|
||
type: item.type ?? null,
|
||
urlPath: urlPath(item.url),
|
||
urlHash: item.url ? sha256(item.url) : null,
|
||
failureKind: failureText ? String(failureText) : null,
|
||
errorTextHash: failureText ? sha256(failureText) : null,
|
||
errorPreview: failureText ? limitText(failureText, 160) : null
|
||
};
|
||
}
|
||
|
||
function groupNetworkAlerts(events) {
|
||
const groups = new Map();
|
||
for (const event of events) {
|
||
const key = [event.method, event.urlPath, event.status ?? "-", event.type].join(" ");
|
||
const group = groups.get(key) || {
|
||
method: event.method,
|
||
urlPath: event.urlPath,
|
||
status: event.status,
|
||
type: event.type,
|
||
count: 0,
|
||
firstAt: event.ts,
|
||
lastAt: event.ts,
|
||
promptIndexes: [],
|
||
failureKinds: [],
|
||
errorTextHashes: []
|
||
};
|
||
group.count += 1;
|
||
group.lastAt = event.ts;
|
||
if (event.promptIndex && !group.promptIndexes.includes(event.promptIndex)) group.promptIndexes.push(event.promptIndex);
|
||
if (event.failureKind && !group.failureKinds.includes(event.failureKind)) group.failureKinds.push(event.failureKind);
|
||
if (event.errorTextHash && !group.errorTextHashes.includes(event.errorTextHash)) group.errorTextHashes.push(event.errorTextHash);
|
||
groups.set(key, group);
|
||
}
|
||
return Array.from(groups.values()).sort((a, b) => b.count - a.count || String(a.urlPath).localeCompare(String(b.urlPath)));
|
||
}
|
||
|
||
function isDiagnosticText(text) {
|
||
const value = String(text || "");
|
||
return /Failed to (?:fetch|load resource)|request failed|net::ERR_[A-Z0-9_:-]+|server responded with a status of [45][0-9]{2}|HTTP\s+[45][0-9]{2}\b|trace_id=|workbench turn\s*超过|turn\s*超过|无新活动|idle\s+\d+s|waitingFor=|lastEventLabel=|无法连接上游|代理暂时无法连接上游|provider-unavailable|agentrun:error|AgentRun error|projection-resume|sync-failed|durable projection store|realtime-gap|Trace 更新超时|加载失败|请求失败|请求已失败/iu.test(value);
|
||
}
|
||
`;
|
||
}
|