fix: web-probe 报警阈值强制走 YAML (#731)

* fix(hwlab): show runtime blockers in node status

* fix: require yaml web-probe alert thresholds

---------

Co-authored-by: Codex <codex@noreply.local>
This commit is contained in:
Lyon
2026-06-23 10:52:05 +08:00
committed by GitHub
parent 3a2ae479b8
commit 09e9f10b84
2 changed files with 56 additions and 43 deletions
+7 -6
View File
@@ -8731,15 +8731,16 @@ function renderWebObserveAnalyzeTable(value: Record<string, unknown>): string {
const pagePerformance = record(analysis?.pagePerformance);
const pagePerformanceSummary = record(pagePerformance?.summary);
const alertThresholds = record(analysis?.alertThresholds ?? pagePerformanceSummary?.alertThresholds ?? value.alertThresholds);
const budgetLabel = (rawValue: unknown, fallbackMs: number): string => {
const budgetLabel = (rawValue: unknown): string => {
const parsed = Number(rawValue);
const ms = Number.isFinite(parsed) && parsed > 0 ? parsed : fallbackMs;
if (!Number.isFinite(parsed) || parsed <= 0) return "unconfigured";
const ms = parsed;
return ms % 1000 === 0 ? `${Math.round(ms / 1000)}s` : `${ms}ms`;
};
const sameOriginApiBudgetLabel = budgetLabel(alertThresholds?.sameOriginApiSlowMs ?? pagePerformanceSummary?.budgetMs, 10000);
const partialApiBudgetLabel = budgetLabel(alertThresholds?.partialApiSlowMs, 10000);
const streamOpenBudgetLabel = budgetLabel(alertThresholds?.longLivedStreamOpenSlowMs, 10000);
const loadingBudgetLabel = budgetLabel(alertThresholds?.visibleLoadingSlowMs, 10000);
const sameOriginApiBudgetLabel = budgetLabel(alertThresholds?.sameOriginApiSlowMs ?? pagePerformanceSummary?.budgetMs);
const partialApiBudgetLabel = budgetLabel(alertThresholds?.partialApiSlowMs);
const streamOpenBudgetLabel = budgetLabel(alertThresholds?.longLivedStreamOpenSlowMs);
const loadingBudgetLabel = budgetLabel(alertThresholds?.visibleLoadingSlowMs);
const promptNetwork = record(analysis?.promptNetwork);
const loading = record(sampleMetrics?.loading);
const loadingSummary = record(loading?.summary);
@@ -1881,27 +1881,33 @@ function positiveNumber(value, fallback) {
return Number.isFinite(parsed) && parsed > 0 ? parsed : fallback;
}
function requiredPositiveThreshold(raw, key) {
const parsed = Number(raw?.[key]);
if (!Number.isFinite(parsed) || parsed <= 0) {
throw new Error("UNIDESK_WEB_OBSERVE_ALERT_THRESHOLDS_JSON requires positive " + key + "; configure config/hwlab-node-lanes.yaml webProbe.alertThresholds");
}
return parsed;
}
function parseAlertThresholds(value) {
if (!value) {
throw new Error("UNIDESK_WEB_OBSERVE_ALERT_THRESHOLDS_JSON is required; configure config/hwlab-node-lanes.yaml webProbe.alertThresholds for the selected node/lane");
}
const raw = (() => {
try { return value ? JSON.parse(value) : {}; } catch { return {}; }
try { return JSON.parse(value); } catch (error) { throw new Error("UNIDESK_WEB_OBSERVE_ALERT_THRESHOLDS_JSON is invalid JSON: " + (error instanceof Error ? error.message : String(error))); }
})();
const fallback = {
sameOriginApiSlowMs: 10000,
partialApiSlowMs: 10000,
longLivedStreamOpenSlowMs: 10000,
visibleLoadingSlowMs: 10000,
turnTimingSampleSlackSeconds: 3,
sessionRailFallbackRatio: 0.5,
};
const sessionRailFallbackRatio = positiveNumber(raw.sessionRailFallbackRatio, fallback.sessionRailFallbackRatio);
const sessionRailFallbackRatio = requiredPositiveThreshold(raw, "sessionRailFallbackRatio");
if (sessionRailFallbackRatio > 1) {
throw new Error("UNIDESK_WEB_OBSERVE_ALERT_THRESHOLDS_JSON sessionRailFallbackRatio must be <= 1");
}
return {
sameOriginApiSlowMs: positiveNumber(raw.sameOriginApiSlowMs, fallback.sameOriginApiSlowMs),
partialApiSlowMs: positiveNumber(raw.partialApiSlowMs, fallback.partialApiSlowMs),
longLivedStreamOpenSlowMs: positiveNumber(raw.longLivedStreamOpenSlowMs, fallback.longLivedStreamOpenSlowMs),
visibleLoadingSlowMs: positiveNumber(raw.visibleLoadingSlowMs, fallback.visibleLoadingSlowMs),
turnTimingSampleSlackSeconds: positiveNumber(raw.turnTimingSampleSlackSeconds, fallback.turnTimingSampleSlackSeconds),
sessionRailFallbackRatio: Math.min(1, sessionRailFallbackRatio),
source: value ? "yaml-env" : "runner-default",
sameOriginApiSlowMs: requiredPositiveThreshold(raw, "sameOriginApiSlowMs"),
partialApiSlowMs: requiredPositiveThreshold(raw, "partialApiSlowMs"),
longLivedStreamOpenSlowMs: requiredPositiveThreshold(raw, "longLivedStreamOpenSlowMs"),
visibleLoadingSlowMs: requiredPositiveThreshold(raw, "visibleLoadingSlowMs"),
turnTimingSampleSlackSeconds: requiredPositiveThreshold(raw, "turnTimingSampleSlackSeconds"),
sessionRailFallbackRatio,
source: "yaml-env",
};
}
@@ -2216,27 +2222,33 @@ function positiveNumber(value, fallback) {
return Number.isFinite(parsed) && parsed > 0 ? parsed : fallback;
}
function requiredPositiveThreshold(raw, key) {
const parsed = Number(raw?.[key]);
if (!Number.isFinite(parsed) || parsed <= 0) {
throw new Error("UNIDESK_WEB_OBSERVE_ALERT_THRESHOLDS_JSON requires positive " + key + "; configure config/hwlab-node-lanes.yaml webProbe.alertThresholds");
}
return parsed;
}
function parseAlertThresholds(value) {
if (!value) {
throw new Error("UNIDESK_WEB_OBSERVE_ALERT_THRESHOLDS_JSON is required; configure config/hwlab-node-lanes.yaml webProbe.alertThresholds for the selected node/lane");
}
const raw = (() => {
try { return value ? JSON.parse(value) : {}; } catch { return {}; }
try { return JSON.parse(value); } catch (error) { throw new Error("UNIDESK_WEB_OBSERVE_ALERT_THRESHOLDS_JSON is invalid JSON: " + (error instanceof Error ? error.message : String(error))); }
})();
const fallback = {
sameOriginApiSlowMs: 10000,
partialApiSlowMs: 10000,
longLivedStreamOpenSlowMs: 10000,
visibleLoadingSlowMs: 10000,
turnTimingSampleSlackSeconds: 3,
sessionRailFallbackRatio: 0.5,
};
const sessionRailFallbackRatio = positiveNumber(raw.sessionRailFallbackRatio, fallback.sessionRailFallbackRatio);
const sessionRailFallbackRatio = requiredPositiveThreshold(raw, "sessionRailFallbackRatio");
if (sessionRailFallbackRatio > 1) {
throw new Error("UNIDESK_WEB_OBSERVE_ALERT_THRESHOLDS_JSON sessionRailFallbackRatio must be <= 1");
}
return {
sameOriginApiSlowMs: positiveNumber(raw.sameOriginApiSlowMs, fallback.sameOriginApiSlowMs),
partialApiSlowMs: positiveNumber(raw.partialApiSlowMs, fallback.partialApiSlowMs),
longLivedStreamOpenSlowMs: positiveNumber(raw.longLivedStreamOpenSlowMs, fallback.longLivedStreamOpenSlowMs),
visibleLoadingSlowMs: positiveNumber(raw.visibleLoadingSlowMs, fallback.visibleLoadingSlowMs),
turnTimingSampleSlackSeconds: positiveNumber(raw.turnTimingSampleSlackSeconds, fallback.turnTimingSampleSlackSeconds),
sessionRailFallbackRatio: Math.min(1, sessionRailFallbackRatio),
source: value ? "yaml-env" : "analyzer-default",
sameOriginApiSlowMs: requiredPositiveThreshold(raw, "sameOriginApiSlowMs"),
partialApiSlowMs: requiredPositiveThreshold(raw, "partialApiSlowMs"),
longLivedStreamOpenSlowMs: requiredPositiveThreshold(raw, "longLivedStreamOpenSlowMs"),
visibleLoadingSlowMs: requiredPositiveThreshold(raw, "visibleLoadingSlowMs"),
turnTimingSampleSlackSeconds: requiredPositiveThreshold(raw, "turnTimingSampleSlackSeconds"),
sessionRailFallbackRatio,
source: "yaml-env",
};
}
@@ -5019,8 +5031,8 @@ function renderMarkdown(report) {
: "- 无页面 provenance segment。";
const ordinaryPerformanceItems = Array.isArray(report.pagePerformance?.sameOriginApiByPath) ? report.pagePerformance.sameOriginApiByPath.filter((item) => item.isLongLivedStream !== true) : [];
const streamPerformanceItems = Array.isArray(report.pagePerformance?.sameOriginApiByPath) ? report.pagePerformance.sameOriginApiByPath.filter((item) => item.isLongLivedStream === true) : [];
const sameOriginApiBudgetMs = Number(report.alertThresholds?.sameOriginApiSlowMs ?? report.pagePerformance?.summary?.budgetMs ?? 10000);
const streamOpenBudgetMs = Number(report.alertThresholds?.longLivedStreamOpenSlowMs ?? 10000);
const sameOriginApiBudgetMs = Number(report.alertThresholds?.sameOriginApiSlowMs ?? report.pagePerformance?.summary?.budgetMs);
const streamOpenBudgetMs = Number(report.alertThresholds?.longLivedStreamOpenSlowMs);
const performanceLines = ordinaryPerformanceItems.length > 0
? ordinaryPerformanceItems.slice(0, 80).map((item) => "- " + item.path + " kind=" + (item.routeKind || "same-origin-api") + " budgetMetric=" + (item.budgetMetric || "durationMs") + " samples=" + item.sampleCount + " p50=" + (item.p50Ms ?? "-") + "ms p75=" + (item.p75Ms ?? "-") + "ms p95=" + (item.p95Ms ?? "-") + "ms max=" + (item.maxMs ?? "-") + "ms >budget=" + (item.overBudgetCount ?? item.overFiveSecondCount ?? 0) + " budgetMs=" + (item.budgetMs ?? sameOriginApiBudgetMs) + " legacy>5s=" + (item.overFiveSecondCount ?? 0) + " window=" + (item.firstAt || "-") + ".." + (item.lastAt || "-")).join("\n")
: "- 无同源 API Resource Timing 样本。";
@@ -5083,7 +5095,7 @@ function renderMarkdown(report) {
+ "- overFiveSecondSegmentCount: " + (loadingSummary.overFiveSecondSegmentCount ?? 0) + "\n"
+ "- longestContinuousSeconds: " + (loadingSummary.longestContinuousSeconds ?? 0) + "\n"
+ "- currentContinuousSeconds: " + (loadingSummary.currentContinuousSeconds ?? 0) + "\n"
+ "- budgetSeconds: " + (loadingSummary.budgetSeconds ?? (Number(report.alertThresholds?.visibleLoadingSlowMs ?? 10000) / 1000)) + "\n"
+ "- budgetSeconds: " + (loadingSummary.budgetSeconds ?? (Number.isFinite(Number(report.alertThresholds?.visibleLoadingSlowMs)) ? Number(report.alertThresholds.visibleLoadingSlowMs) / 1000 : "unconfigured")) + "\n"
+ "- policy: 该指标只能证明用户真实看到“加载中”的持续时间;修复必须降低真实请求/投影/渲染耗时,禁止提前展示未加载完内容来压低该指标。\n\n"
+ "#### Loading segments\n\n" + loadingSegmentLines + "\n\n"
+ "#### Loading owners\n\n" + loadingOwnerLines + "\n\n"