diff --git a/scripts/src/hwlab-node-impl.ts b/scripts/src/hwlab-node-impl.ts index 373aa6ec..8dca2028 100644 --- a/scripts/src/hwlab-node-impl.ts +++ b/scripts/src/hwlab-node-impl.ts @@ -8731,15 +8731,16 @@ function renderWebObserveAnalyzeTable(value: Record): 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); diff --git a/scripts/src/hwlab-node-web-observe-runner-source.ts b/scripts/src/hwlab-node-web-observe-runner-source.ts index 20b1f76d..e2f5fdf2 100644 --- a/scripts/src/hwlab-node-web-observe-runner-source.ts +++ b/scripts/src/hwlab-node-web-observe-runner-source.ts @@ -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"