fix(web-probe): expose recent update sawtooth jumps
This commit is contained in:
@@ -853,7 +853,7 @@ const report = {
|
||||
await writeFile(reportJsonPath, JSON.stringify(report, null, 2) + "\n", { mode: 0o600 });
|
||||
await writeFile(reportMdPath, renderMarkdown(report), { mode: 0o600 });
|
||||
const [jsonMeta, mdMeta] = await Promise.all([fileMeta(reportJsonPath), fileMeta(reportMdPath)]);
|
||||
console.log(JSON.stringify({ ok: true, command: "web-probe-observe analyze", stateDir, reportJsonPath, reportJsonSha256: jsonMeta.sha256, reportMdPath, reportMdSha256: mdMeta.sha256, counts: report.counts, sampleMetrics: sampleMetrics.summary, promptNetwork: promptNetwork.summary, runtimeAlerts: runtimeAlerts.summary, findings: findings.slice(0, 20), valuesRedacted: true }, null, 2));
|
||||
console.log(JSON.stringify({ ok: true, command: "web-probe-observe analyze", stateDir, reportJsonPath, reportJsonSha256: jsonMeta.sha256, reportMdPath, reportMdSha256: mdMeta.sha256, counts: report.counts, sampleMetrics: sampleMetrics.summary, promptNetwork: promptNetwork.summary, runtimeAlerts: runtimeAlerts.summary, turnTimingRecentUpdateSawtoothJumps: sampleMetrics.turnTimingRecentUpdateSawtoothJumps.slice(0, 20), findings: findings.slice(0, 20), valuesRedacted: true }, null, 2));
|
||||
|
||||
async function readJson(file) {
|
||||
try { return JSON.parse(await readFile(file, "utf8")); } catch { return null; }
|
||||
@@ -898,6 +898,12 @@ function buildFindings(samples, control, network, errors, sampleMetrics, promptN
|
||||
if (traceSeen && !traceTerminal) findings.push({ id: "trace-without-terminal", severity: "amber", summary: "trace rows were visible but no terminal status was sampled", firstTraceSample: ref(samples.find((item) => Array.isArray(item.traceRows) && item.traceRows.length > 0)) });
|
||||
const promptFailures = Array.isArray(promptNetwork?.rounds) ? promptNetwork.rounds.filter((item) => item.chatPostOk === false) : [];
|
||||
if (promptFailures.length > 0) findings.push({ id: "prompt-chat-submit-failed", severity: "red", summary: "sendPrompt command had no successful /v1/agent/chat POST response in the sampling window", count: promptFailures.length, rounds: promptFailures.slice(0, 10) });
|
||||
const recentUpdateSawtoothJumps = Array.isArray(sampleMetrics?.turnTimingRecentUpdateSawtoothJumps)
|
||||
? sampleMetrics.turnTimingRecentUpdateSawtoothJumps
|
||||
: Array.isArray(sampleMetrics?.turnTimingNonMonotonic)
|
||||
? sampleMetrics.turnTimingNonMonotonic.filter((item) => item.metric === "recentUpdateSeconds" && item.anomaly === "jump")
|
||||
: [];
|
||||
if (recentUpdateSawtoothJumps.length > 0) findings.push({ id: "turn-timing-recent-update-sawtooth-jump", severity: "amber", summary: "最近更新 value jumped faster than sample interval; expected sawtooth increase-or-reset", count: recentUpdateSawtoothJumps.length, samples: recentUpdateSawtoothJumps.slice(0, 20) });
|
||||
if ((runtimeAlerts?.summary?.httpErrorCount ?? 0) > 0) findings.push({ id: "runtime-http-errors", severity: "amber", summary: "natural page requests returned HTTP error status during observation", count: runtimeAlerts.summary.httpErrorCount, groups: runtimeAlerts.networkHttpErrorsByPath.slice(0, 12) });
|
||||
if ((runtimeAlerts?.summary?.requestFailedCount ?? 0) > 0) findings.push({ id: "runtime-requestfailed", severity: "amber", summary: "browser requestfailed events were captured during observation", count: runtimeAlerts.summary.requestFailedCount, groups: runtimeAlerts.networkRequestFailedByPath.slice(0, 12) });
|
||||
if ((runtimeAlerts?.summary?.domDiagnosticSampleCount ?? 0) > 0) findings.push({ id: "runtime-dom-diagnostics", severity: "amber", summary: "diagnostic/error/warning-like text was visible in sampled DOM", count: runtimeAlerts.summary.domDiagnosticSampleCount, samples: runtimeAlerts.domDiagnostics.slice(0, 12) });
|
||||
@@ -1361,6 +1367,7 @@ function buildSampleMetrics(samples, control) {
|
||||
const turnTiming = buildTurnTimingTable(samples, timeline);
|
||||
const turnCells = turnTiming.rows.flatMap((row) => Object.values(row.cells || {}));
|
||||
const turnTimingNonMonotonic = Array.isArray(turnTiming.nonMonotonic) ? turnTiming.nonMonotonic : [];
|
||||
const turnTimingRecentUpdateSawtoothJumps = turnTimingNonMonotonic.filter((item) => item.metric === "recentUpdateSeconds" && item.anomaly === "jump");
|
||||
const turnTimingRecentUpdateResets = Array.isArray(turnTiming.recentUpdateResets) ? turnTiming.recentUpdateResets : [];
|
||||
const withTotal = timeline.filter((item) => item.totalElapsedSeconds !== null).length;
|
||||
const withRecent = timeline.filter((item) => item.recentUpdateSeconds !== null).length;
|
||||
@@ -1369,7 +1376,7 @@ function buildSampleMetrics(samples, control) {
|
||||
nonMonotonic: turnTimingNonMonotonic,
|
||||
recentUpdateResets: turnTimingRecentUpdateResets
|
||||
});
|
||||
const recentUpdateJumpCount = turnTimingNonMonotonic.filter((item) => item.metric === "recentUpdateSeconds" && item.anomaly === "jump").length;
|
||||
const recentUpdateJumpCount = turnTimingRecentUpdateSawtoothJumps.length;
|
||||
return {
|
||||
summary: {
|
||||
sampleCount: timeline.length,
|
||||
@@ -1395,6 +1402,7 @@ function buildSampleMetrics(samples, control) {
|
||||
turnColumns: turnTiming.columns,
|
||||
turnTimingTable: turnTiming.rows,
|
||||
turnTimingNonMonotonic,
|
||||
turnTimingRecentUpdateSawtoothJumps,
|
||||
turnTimingRecentUpdateResets,
|
||||
timeline
|
||||
};
|
||||
@@ -1701,7 +1709,7 @@ function parseTotalElapsedSeconds(text) {
|
||||
for (const match of String(text || "").matchAll(/(?:总耗时|耗时)\s*[=::]?\s*(\d{1,2}):(\d{2})(?!:)/giu)) {
|
||||
values.push(Number(match[1]) * 60 + Number(match[2]));
|
||||
}
|
||||
for (const match of String(text || "").matchAll(/(?:总耗时|耗时)\s*[=::]?\s*(?:(\d+)\s*天)?\s*(?:(\d+)\s*小时)?\s*(?:(\d+)\s*分)?\s*(?:(\d+)\s*秒)?/giu)) {
|
||||
for (const match of String(text || "").matchAll(/(?:总耗时|耗时)\s*[=::]?\s*(?:(\d+)\s*天)?\s*(?:(\d+)\s*小时)?\s*(?:(\d+)\s*(?:分钟|分))?\s*(?:(\d+)\s*秒)?/giu)) {
|
||||
const days = Number(match[1] || 0);
|
||||
const hours = Number(match[2] || 0);
|
||||
const minutes = Number(match[3] || 0);
|
||||
@@ -1718,7 +1726,7 @@ function lastNonNull(values) {
|
||||
|
||||
function parseRecentUpdateSeconds(text) {
|
||||
const values = [];
|
||||
for (const match of String(text || "").matchAll(/最近\s*(?:(\d+)\s*天)?\s*(?:(\d+)\s*小时)?\s*(?:(\d+)\s*分)?\s*(?:(\d+)\s*秒)?\s*前/giu)) {
|
||||
for (const match of String(text || "").matchAll(/最近\s*(?:(\d+)\s*天)?\s*(?:(\d+)\s*小时)?\s*(?:(\d+)\s*(?:分钟|分))?\s*(?:(\d+)\s*秒)?\s*前/giu)) {
|
||||
const days = Number(match[1] || 0);
|
||||
const hours = Number(match[2] || 0);
|
||||
const minutes = Number(match[3] || 0);
|
||||
@@ -1839,11 +1847,17 @@ function renderTurnTimingTable(sampleMetrics) {
|
||||
const nonMonotonicLines = nonMonotonic.length > 0
|
||||
? nonMonotonic.slice(0, 80).map((item) => "- " + (item.columnLabel || item.columnId || "-") + " " + item.metric + (item.anomaly ? " " + item.anomaly : "") + " " + (item.fromValue ?? "-") + " -> " + (item.toValue ?? "-") + " delta=" + (item.delta ?? "-") + " sampleDelta=" + (item.sampleDeltaSeconds ?? "-") + " allowed=" + (item.allowedIncreaseSeconds ?? "-") + " seq " + (item.fromSeq ?? "-") + " -> " + (item.toSeq ?? "-") + " ts " + (item.fromTs || "-") + " -> " + (item.toTs || "-") + " traceId=" + (item.traceId || "-")).join("\n")
|
||||
: "- 未观察到总耗时下降或最近更新异常跳增。";
|
||||
const sawtoothJumps = Array.isArray(sampleMetrics?.turnTimingRecentUpdateSawtoothJumps)
|
||||
? sampleMetrics.turnTimingRecentUpdateSawtoothJumps
|
||||
: nonMonotonic.filter((item) => item.metric === "recentUpdateSeconds" && item.anomaly === "jump");
|
||||
const sawtoothJumpLines = sawtoothJumps.length > 0
|
||||
? sawtoothJumps.slice(0, 80).map((item) => "- " + (item.columnLabel || item.columnId || "-") + " recentUpdate sawtooth-jump " + (item.fromValue ?? "-") + " -> " + (item.toValue ?? "-") + " delta=" + (item.delta ?? "-") + " sampleDelta=" + (item.sampleDeltaSeconds ?? "-") + " allowed=" + (item.allowedIncreaseSeconds ?? "-") + " seq " + (item.fromSeq ?? "-") + " -> " + (item.toSeq ?? "-") + " ts " + (item.fromTs || "-") + " -> " + (item.toTs || "-") + " traceId=" + (item.traceId || "-")).join("\n")
|
||||
: "- 未观察到最近更新三角波异常跳增。";
|
||||
const recentUpdateResets = Array.isArray(sampleMetrics?.turnTimingRecentUpdateResets) ? sampleMetrics.turnTimingRecentUpdateResets : [];
|
||||
const resetLines = recentUpdateResets.length > 0
|
||||
? recentUpdateResets.slice(0, 80).map((item) => "- " + (item.columnLabel || item.columnId || "-") + " reset " + (item.fromValue ?? "-") + " -> " + (item.toValue ?? "-") + " delta=" + (item.delta ?? "-") + " sampleDelta=" + (item.sampleDeltaSeconds ?? "-") + " seq " + (item.fromSeq ?? "-") + " -> " + (item.toSeq ?? "-") + " ts " + (item.fromTs || "-") + " -> " + (item.toTs || "-") + " traceId=" + (item.traceId || "-")).join("\n")
|
||||
: "- 未观察到最近更新归零/回落。";
|
||||
return lines.join("\n") + "\n\n列说明:\n" + columnLines + "\n\n异常事件(仅报表暴露,不做下游 repair;最近更新按三角波模型检测异常跳增):\n" + nonMonotonicLines + "\n\n最近更新 reset 事件(预期三角波归零,不计为异常):\n" + resetLines;
|
||||
return lines.join("\n") + "\n\n列说明:\n" + columnLines + "\n\n异常事件(仅报表暴露,不做下游 repair;最近更新按三角波模型检测异常跳增):\n" + nonMonotonicLines + "\n\n最近更新 sawtooth jump 事件(预期每秒增长约 1,遇到新活动归零;例如 1 秒 -> 1 分 4 秒应被列入这里):\n" + sawtoothJumpLines + "\n\n最近更新 reset 事件(预期三角波归零,不计为异常):\n" + resetLines;
|
||||
}
|
||||
|
||||
function formatMetricCell(value) {
|
||||
|
||||
Reference in New Issue
Block a user