fix: make sentinel trend chart show duration
This commit is contained in:
@@ -343,30 +343,12 @@ select {
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.trend-red {
|
||||
fill: none;
|
||||
stroke: var(--red);
|
||||
stroke-width: 3;
|
||||
stroke-linecap: round;
|
||||
stroke-linejoin: round;
|
||||
}
|
||||
|
||||
.trend-warning {
|
||||
fill: none;
|
||||
stroke: var(--amber);
|
||||
stroke-width: 3;
|
||||
stroke-linecap: round;
|
||||
stroke-linejoin: round;
|
||||
}
|
||||
|
||||
.trend-total {
|
||||
.trend-duration {
|
||||
fill: none;
|
||||
stroke: var(--blue);
|
||||
stroke-width: 2;
|
||||
stroke-dasharray: 5 6;
|
||||
stroke-width: 3;
|
||||
stroke-linecap: round;
|
||||
stroke-linejoin: round;
|
||||
opacity: 0.75;
|
||||
}
|
||||
|
||||
.trend-grid-line {
|
||||
@@ -374,12 +356,10 @@ select {
|
||||
stroke-width: 1;
|
||||
}
|
||||
|
||||
.trend-dot-red {
|
||||
fill: var(--red);
|
||||
}
|
||||
|
||||
.trend-dot-warning {
|
||||
fill: var(--amber);
|
||||
.trend-dot-duration {
|
||||
fill: var(--blue);
|
||||
stroke: #ffffff;
|
||||
stroke-width: 1.5;
|
||||
}
|
||||
|
||||
.trend-dot-hit {
|
||||
@@ -450,6 +430,10 @@ select {
|
||||
background: var(--red);
|
||||
}
|
||||
|
||||
.legend-swatch.duration {
|
||||
background: var(--blue);
|
||||
}
|
||||
|
||||
.legend-swatch.warning {
|
||||
background: var(--amber);
|
||||
}
|
||||
|
||||
@@ -4,123 +4,6 @@ import { createApp, computed, onMounted, ref, watch } from "./vendor/vue.esm-bro
|
||||
|
||||
const bootstrap = readBootstrap();
|
||||
const internalTextPattern = /水合|投影|Trace|trace|Shell|API|DOM|Console|console|Runner|runner|JSONL|steer|facts|分页|HTTP|http|requestfailed|pageerror|Final Response|Code Agent|web-probe|observe|analyzer|终态/u;
|
||||
const checkDisplayCatalog = {
|
||||
"code-agent-card-elapsed-missing": {
|
||||
code: "CHECK-101",
|
||||
title: "耗时未显示",
|
||||
summary: "运行卡片没有显示总耗时,影响判断等待时间。",
|
||||
},
|
||||
"code-agent-card-running-recent-update-missing": {
|
||||
code: "CHECK-102",
|
||||
title: "最近更新时间未显示",
|
||||
summary: "运行中的卡片没有显示最近更新时间,影响判断是否仍在推进。",
|
||||
},
|
||||
"turn-timing-total-elapsed-decrease": {
|
||||
code: "CHECK-103",
|
||||
title: "耗时显示回退",
|
||||
summary: "相邻采样中的总耗时出现回退,作为非阻塞计时报警展示。",
|
||||
},
|
||||
"page-loading-concurrent": {
|
||||
code: "CHECK-201",
|
||||
title: "多个加载状态同时出现",
|
||||
summary: "同一采样点看到多个加载提示,用户可能难以判断当前页面是否已就绪。",
|
||||
},
|
||||
"trace-events-page-read-requestfailed-root-cause": {
|
||||
code: "CHECK-301",
|
||||
title: "运行事件读取失败",
|
||||
summary: "运行事件页面读取失败,需要结合服务端链路确认具体失败位置。",
|
||||
},
|
||||
"runtime-requestfailed": {
|
||||
code: "CHECK-302",
|
||||
title: "页面资源加载失败",
|
||||
summary: "巡检期间捕获到页面资源加载失败,可能影响用户看到的运行状态。",
|
||||
},
|
||||
"runtime-console-alerts": {
|
||||
code: "CHECK-303",
|
||||
title: "页面脚本报警",
|
||||
summary: "巡检期间捕获到页面脚本报警,需要评估是否影响用户操作。",
|
||||
},
|
||||
"workbench-app-shell-transient-not-ready": {
|
||||
code: "CHECK-304",
|
||||
title: "工作台短暂未就绪",
|
||||
summary: "工作台短时间内未完成主界面就绪,作为启动过程上下文保留。",
|
||||
},
|
||||
"cross-page-projection-divergence": {
|
||||
code: "CHECK-401",
|
||||
title: "多页面状态不一致",
|
||||
summary: "同一会话在两个页面中显示的状态持续不一致,可能影响用户判断。",
|
||||
},
|
||||
"cross-page-projection-transient-divergence": {
|
||||
code: "CHECK-402",
|
||||
title: "多页面短暂不一致",
|
||||
summary: "页面切换附近短暂出现显示差异,作为上下文保留。",
|
||||
},
|
||||
"cross-page-projection-controlled-navigation-hydration": {
|
||||
code: "CHECK-403",
|
||||
title: "跳转后页面短暂空白",
|
||||
summary: "页面跳转后短暂空白,当前按非阻塞上下文展示。",
|
||||
},
|
||||
"cross-page-trace-visibility-divergence": {
|
||||
code: "CHECK-404",
|
||||
title: "运行记录显示数量不一致",
|
||||
summary: "两个页面看到的运行记录数量不同,但未直接证明消息状态不一致。",
|
||||
},
|
||||
"page-performance-slow-same-origin-api": {
|
||||
code: "CHECK-501",
|
||||
title: "页面数据加载慢",
|
||||
summary: "页面数据加载超过配置预算,用户可能感觉等待时间过长。",
|
||||
},
|
||||
"page-performance-long-lived-streams": {
|
||||
code: "CHECK-502",
|
||||
title: "长连接持续保持",
|
||||
summary: "长时间连接单独统计,不直接作为页面加载慢处理。",
|
||||
},
|
||||
"natural-api-dom-lag-baseline": {
|
||||
code: "CHECK-503",
|
||||
title: "数据变化可关联",
|
||||
summary: "已采集到数据响应和页面变化样本,可用于分析显示延迟。",
|
||||
},
|
||||
"natural-api-dom-lag-candidates": {
|
||||
code: "CHECK-504",
|
||||
title: "数据更新显示偏慢",
|
||||
summary: "部分数据变化到页面显示的时间超过预算,作为非阻塞调查报警。",
|
||||
},
|
||||
"browser-console-or-page-errors": {
|
||||
code: "CHECK-601",
|
||||
title: "页面脚本错误",
|
||||
summary: "巡检期间捕获到页面脚本错误,需要确认是否影响用户流程。",
|
||||
},
|
||||
"quick-verify-no-business-turn": {
|
||||
code: "CHECK-701",
|
||||
title: "业务轮次未完成",
|
||||
summary: "巡检未确认到稳定的业务交互结果,不能作为恢复证据。",
|
||||
},
|
||||
"frontend-browser-memory-rss-red": {
|
||||
code: "CHECK-801",
|
||||
title: "页面内存超出预算",
|
||||
summary: "页面有效内存超过配置红线,已按阻塞错误展示。",
|
||||
},
|
||||
"frontend-browser-memory-growth-red": {
|
||||
code: "CHECK-802",
|
||||
title: "页面内存持续增长",
|
||||
summary: "页面有效内存在观察窗口内持续增长并超过配置预算,符合卡死风险特征。",
|
||||
},
|
||||
"frontend-browser-freeze-runner-blocker": {
|
||||
code: "CHECK-803",
|
||||
title: "浏览器冻结已阻塞",
|
||||
summary: "浏览器冻结策略已触发,巡检中止页面并报红,不能用刷新或兜底掩盖。",
|
||||
},
|
||||
"frontend-playwright-responsiveness-red": {
|
||||
code: "CHECK-804",
|
||||
title: "页面响应探测超时",
|
||||
summary: "页面响应探测超过配置预算,已按前端卡死处理并报红。",
|
||||
},
|
||||
"frontend-cdp-metrics-timeout-red": {
|
||||
code: "CHECK-805",
|
||||
title: "浏览器指标采集超时",
|
||||
summary: "浏览器侧指标连续采集超时,说明页面运行时可能已经无响应。",
|
||||
},
|
||||
};
|
||||
|
||||
createApp({
|
||||
setup() {
|
||||
@@ -165,19 +48,18 @@ createApp({
|
||||
const selectedRun = computed(() => runs.value.find((run) => run.id === selectedRunId.value) || latestRun.value);
|
||||
const trendRows = computed(() => runs.value.slice().sort((a, b) => Date.parse(a.updatedAt || a.createdAt || "") - Date.parse(b.updatedAt || b.createdAt || "")).slice(-48));
|
||||
const latestTrendRun = computed(() => trendRows.value.length > 0 ? trendRows.value[trendRows.value.length - 1] : latestRun.value);
|
||||
const trendMax = computed(() => Math.max(1, ...trendRows.value.flatMap((run) => [trendErrorCount(run), trendWarningCount(run), trendTotalCount(run)])));
|
||||
const trendDurationMax = computed(() => Math.max(0, ...trendRows.value.map((run) => trendDurationMinutes(run))));
|
||||
const trendMax = computed(() => Math.max(1, trendDurationMax.value));
|
||||
const trendPolylines = computed(() => ({
|
||||
red: trendPolyline((run) => trendErrorCount(run)),
|
||||
warning: trendPolyline((run) => trendWarningCount(run)),
|
||||
total: trendPolyline((run) => trendTotalCount(run)),
|
||||
duration: trendPolyline((run) => trendDurationMinutes(run)),
|
||||
}));
|
||||
const trendDots = computed(() => trendRows.value.map((run, index) => {
|
||||
const red = trendErrorCount(run);
|
||||
const warning = trendWarningCount(run);
|
||||
const total = trendTotalCount(run);
|
||||
const duration = trendDurationMinutes(run);
|
||||
const x = trendX(index, trendRows.value.length);
|
||||
const redY = trendY(red);
|
||||
const warningY = trendY(warning);
|
||||
const durationY = trendY(duration);
|
||||
const rawTime = run.updatedAt || run.createdAt || "";
|
||||
const durationText = runDurationText(run);
|
||||
const configTimingText = runTimingConfigText(run);
|
||||
@@ -185,10 +67,10 @@ createApp({
|
||||
id: run.id || String(index),
|
||||
runId: run.id || "",
|
||||
x,
|
||||
redY,
|
||||
warningY,
|
||||
durationY,
|
||||
duration,
|
||||
tooltipLeft: `${clamp((x / 720) * 100, 16, 84)}%`,
|
||||
tooltipTop: `${clamp(((Math.min(redY, warningY) + 18) / 142) * 100, 24, 76)}%`,
|
||||
tooltipTop: `${clamp(((durationY + 18) / 142) * 100, 24, 76)}%`,
|
||||
red,
|
||||
warning,
|
||||
total,
|
||||
@@ -363,6 +245,10 @@ createApp({
|
||||
return runCheckAlertCount(run);
|
||||
}
|
||||
|
||||
function trendDurationMinutes(run) {
|
||||
return number(run?.runDurationMinutes ?? run?.durationMinutes ?? run?.timing?.durationMinutes);
|
||||
}
|
||||
|
||||
function openCheckDetail(item) {
|
||||
activeCheckItem.value = item || null;
|
||||
}
|
||||
@@ -459,6 +345,7 @@ createApp({
|
||||
selectedRun,
|
||||
trendRows,
|
||||
latestTrendRun,
|
||||
trendDurationMax,
|
||||
trendPolylines,
|
||||
trendDots,
|
||||
timelineRuns,
|
||||
@@ -492,12 +379,14 @@ createApp({
|
||||
trendTotalCount,
|
||||
trendErrorCount,
|
||||
trendWarningCount,
|
||||
trendDurationMinutes,
|
||||
runDurationText,
|
||||
runTimingConfigText,
|
||||
severityClass,
|
||||
formatDate,
|
||||
formatAbsoluteDate,
|
||||
formatDuration,
|
||||
formatMinutes,
|
||||
shortId,
|
||||
rootCauseText,
|
||||
findingTitle,
|
||||
@@ -566,19 +455,17 @@ createApp({
|
||||
<section class="trend-panel" aria-labelledby="trend-heading">
|
||||
<div class="panel-header">
|
||||
<div>
|
||||
<h2 id="trend-heading">错误 / 告警监测项曲线</h2>
|
||||
<p>按运行更新时间展示最近 {{ trendRows.length }} 次变化,点位为单次运行监测项行数</p>
|
||||
<h2 id="trend-heading">运行耗时曲线</h2>
|
||||
<p>按运行更新时间展示最近 {{ trendRows.length }} 次巡检耗时,单位为分钟</p>
|
||||
</div>
|
||||
<span class="pill" :class="cadence.stale ? 'warning' : 'healthy'">{{ cadence.stale ? "非阻塞报警" : "新鲜" }}</span>
|
||||
</div>
|
||||
<div class="trend-chart-wrap">
|
||||
<svg class="trend-chart" viewBox="0 0 720 142" role="img" aria-label="错误和告警监测项数量趋势" data-monitor-trend-curve="true">
|
||||
<svg class="trend-chart" viewBox="0 0 720 142" role="img" aria-label="运行耗时分钟趋势" data-monitor-trend-curve="true">
|
||||
<line class="trend-grid-line" x1="24" x2="696" y1="24" y2="24"></line>
|
||||
<line class="trend-grid-line" x1="24" x2="696" y1="75" y2="75"></line>
|
||||
<line class="trend-grid-line" x1="24" x2="696" y1="126" y2="126"></line>
|
||||
<polyline v-if="trendPolylines.total" class="trend-total" :points="trendPolylines.total"></polyline>
|
||||
<polyline v-if="trendPolylines.warning" class="trend-warning" :points="trendPolylines.warning"></polyline>
|
||||
<polyline v-if="trendPolylines.red" class="trend-red" :points="trendPolylines.red"></polyline>
|
||||
<polyline v-if="trendPolylines.duration" class="trend-duration" :points="trendPolylines.duration"></polyline>
|
||||
<g
|
||||
v-for="dot in trendDots"
|
||||
:key="dot.id"
|
||||
@@ -591,11 +478,8 @@ createApp({
|
||||
@mouseleave="hideTrendTooltip"
|
||||
@focusout="hideTrendTooltip"
|
||||
>
|
||||
<circle class="trend-hit-area" :cx="dot.x" :cy="Math.min(dot.redY, dot.warningY)" r="12"></circle>
|
||||
<circle class="trend-dot-warning" :cx="dot.x" :cy="dot.warningY" r="3">
|
||||
<title>{{ dot.title }}</title>
|
||||
</circle>
|
||||
<circle class="trend-dot-red" :cx="dot.x" :cy="dot.redY" r="3">
|
||||
<circle class="trend-hit-area" :cx="dot.x" :cy="dot.durationY" r="12"></circle>
|
||||
<circle class="trend-dot-duration" :cx="dot.x" :cy="dot.durationY" r="4">
|
||||
<title>{{ dot.title }}</title>
|
||||
</circle>
|
||||
</g>
|
||||
@@ -617,9 +501,9 @@ createApp({
|
||||
<div v-if="trendRows.length === 0" class="trend-empty">暂无运行数据</div>
|
||||
</div>
|
||||
<div class="trend-legend">
|
||||
<span class="legend-item"><span class="legend-swatch red"></span>最新点错误 {{ trendErrorCount(latestTrendRun) }}</span>
|
||||
<span class="legend-item"><span class="legend-swatch warning"></span>最新点告警 {{ trendWarningCount(latestTrendRun) }}</span>
|
||||
<span class="legend-item"><span class="legend-swatch total"></span>最新点错误+告警合计 {{ trendTotalCount(latestTrendRun) }}</span>
|
||||
<span class="legend-item"><span class="legend-swatch duration"></span>最新运行耗时 {{ runDurationText(latestTrendRun) }}</span>
|
||||
<span class="legend-item">最近最高耗时 {{ formatMinutes(trendDurationMax) }}</span>
|
||||
<span class="legend-item">最新错误 {{ trendErrorCount(latestTrendRun) }} / 告警 {{ trendWarningCount(latestTrendRun) }}</span>
|
||||
<span class="legend-item">历史样本累计 错误 {{ redCount({ severityCounts: severityTotals }) }} / 告警 {{ warningCount({ severityCounts: severityTotals }) }}</span>
|
||||
<span class="legend-item">{{ cadence.alert }}</span>
|
||||
</div>
|
||||
@@ -1127,7 +1011,7 @@ function clamp(value, min, max) {
|
||||
function rootCauseText(item) {
|
||||
const display = checkDisplay(item);
|
||||
if (display.summary) return display.summary;
|
||||
const safeSummary = safeUserText(item?.checkSummaryZh || item?.summary || item?.evidenceSummary);
|
||||
const safeSummary = safeUserText(item?.checkSummaryZh || item?.check?.summaryZh);
|
||||
if (safeSummary) return safeSummary;
|
||||
if (item?.rootCause) return "已记录内部根因,见报告详情。";
|
||||
return "尚未记录用户可见问题摘要。";
|
||||
@@ -1201,7 +1085,8 @@ function checkTimeText(item) {
|
||||
}
|
||||
|
||||
function checkActionText(item) {
|
||||
return safeUserText(item?.nextAction || item?.action || item?.recommendation) || "查看详情后处理";
|
||||
const display = checkDisplay(item);
|
||||
return display.action || safeUserText(item?.checkActionZh || item?.check?.actionZh) || "查看详情后处理";
|
||||
}
|
||||
|
||||
function checkDetailRows(item) {
|
||||
@@ -1223,7 +1108,7 @@ function checkEvidenceRows(item) {
|
||||
if (!item) return [{ key: "empty", label: "状态", value: "未选择监测项" }];
|
||||
const evidence = item?.evidence && typeof item.evidence === "object" && !Array.isArray(item.evidence) ? item.evidence : {};
|
||||
const rows = [
|
||||
{ key: "summary", label: "证据摘要", value: safeUserText(item?.evidenceSummary || evidence.summary || item?.summary) || rootCauseText(item) },
|
||||
{ key: "summary", label: "证据摘要", value: rootCauseText(item) || safeUserText(item?.evidenceSummary || evidence.summary) },
|
||||
{ key: "sample", label: "样本序号", value: safeDetailValue(item?.sampleSeq ?? evidence.sampleSeq) },
|
||||
{ key: "page", label: "页面", value: safeDetailValue(item?.pageRole || evidence.pageRole) },
|
||||
{ key: "command", label: "命令编号", value: safeDetailValue(item?.commandId || evidence.commandId) },
|
||||
@@ -1254,20 +1139,12 @@ function safeDetailValue(value) {
|
||||
function checkDisplay(item) {
|
||||
const rawCode = rawCheckCode(item);
|
||||
const rawId = rawFindingId(item);
|
||||
const registered = checkDisplayCatalog[rawId] || checkDisplayCatalog[rawCode];
|
||||
const serviceCode = publicCheckCode(item?.checkCode || item?.check?.code);
|
||||
if (registered) {
|
||||
return {
|
||||
...registered,
|
||||
code: serviceCode || registered.code,
|
||||
title: safeUserText(item?.checkTitleZh || item?.check?.titleZh) || registered.title,
|
||||
summary: safeUserText(item?.checkSummaryZh || item?.summary || item?.evidenceSummary || item?.check?.summaryZh) || registered.summary,
|
||||
};
|
||||
}
|
||||
return {
|
||||
code: serviceCode || publicCheckCode(rawId || rawCode) || stableCheckCode(rawCode || rawId),
|
||||
title: safeUserText(item?.checkTitleZh || item?.check?.titleZh) || "未登记监测项",
|
||||
summary: safeUserText(item?.checkSummaryZh || item?.summary || item?.evidenceSummary) || "已记录监测项详情,见报告原文。",
|
||||
summary: safeUserText(item?.checkSummaryZh || item?.check?.summaryZh) || "已记录监测项详情,见报告原文。",
|
||||
action: safeUserText(item?.checkActionZh || item?.check?.actionZh) || "查看详情后处理",
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user