- 新增 GET /api/dashboard 合并原 summary+targets 首屏接口 - 新增 GET /api/targets/:id/metrics 合并原 stats+trend 概览接口 - 后端指标纯函数:可用率、百分位、故障段分析、连续状态、UTC 小时分桶 - ProbeStore 窗口取数方法替代全量历史查询 - SummaryCards 扩展为 4 卡片(新增异常事件数)+ 数据新鲜度展示 - 表格新增「连续」列(Tag 渲染 capped 状态) - OverviewTab 重构为 2×4 Statistic 多维度布局 - TrendChart 改为延迟范围面积图 + 红色异常标记点 - 删除旧路由(summary/targets/trend)和 computeTrendStats - 同步 delta specs 到主 specs 并归档变更
127 lines
3.9 KiB
TypeScript
127 lines
3.9 KiB
TypeScript
import { describe, expect, test } from "bun:test";
|
||
|
||
import {
|
||
analyzeIncidentSequence,
|
||
buildHourlyTrend,
|
||
calculateAvailability,
|
||
calculateCurrentStreak,
|
||
calculatePercentile,
|
||
type MetricCheckpoint,
|
||
} from "../../src/server/metrics";
|
||
|
||
describe("后端指标计算", () => {
|
||
test("可用率无数据返回 0,并保留两位精度", () => {
|
||
expect(calculateAvailability(0, 0)).toBe(0);
|
||
expect(calculateAvailability(2, 3)).toBe(66.67);
|
||
});
|
||
|
||
test("百分位按 ceil(count * N / 100) - 1 取值", () => {
|
||
const durations = Array.from({ length: 100 }, (_, index) => index + 1);
|
||
|
||
expect(calculatePercentile([], 95)).toBeNull();
|
||
expect(calculatePercentile([40, 10, 30, 20], 95)).toBe(40);
|
||
expect(calculatePercentile(durations, 95)).toBe(95);
|
||
expect(calculatePercentile(durations, 99)).toBe(99);
|
||
});
|
||
|
||
test("无检查数据时故障分析返回空口径", () => {
|
||
const result = analyzeIncidentSequence([], "2025-01-01T00:00:00.000Z", "2025-01-01T01:00:00.000Z");
|
||
|
||
expect(result).toEqual({ incidentCount: 0, longestOutage: null, mttr: null });
|
||
expect(calculateCurrentStreak([])).toBeNull();
|
||
});
|
||
|
||
test("窗口起始即故障计入 incident 和最长故障,但不计入 MTTR", () => {
|
||
const result = analyzeIncidentSequence(
|
||
[
|
||
checkpoint("2025-01-01T00:05:00.000Z", false),
|
||
checkpoint("2025-01-01T00:10:00.000Z", false),
|
||
checkpoint("2025-01-01T00:20:00.000Z", true),
|
||
],
|
||
"2025-01-01T00:00:00.000Z",
|
||
"2025-01-01T01:00:00.000Z",
|
||
);
|
||
|
||
expect(result.incidentCount).toBe(1);
|
||
expect(result.longestOutage).toBe(20 * 60 * 1000);
|
||
expect(result.mttr).toBeNull();
|
||
});
|
||
|
||
test("未恢复故障计算到窗口结束且不计入 MTTR", () => {
|
||
const result = analyzeIncidentSequence(
|
||
[checkpoint("2025-01-01T00:05:00.000Z", true), checkpoint("2025-01-01T00:20:00.000Z", false)],
|
||
"2025-01-01T00:00:00.000Z",
|
||
"2025-01-01T01:00:00.000Z",
|
||
);
|
||
|
||
expect(result.incidentCount).toBe(1);
|
||
expect(result.longestOutage).toBe(40 * 60 * 1000);
|
||
expect(result.mttr).toBeNull();
|
||
});
|
||
|
||
test("连续异常只计一次 incident,恢复后纳入 MTTR", () => {
|
||
const result = analyzeIncidentSequence(
|
||
[
|
||
checkpoint("2025-01-01T00:00:00.000Z", true),
|
||
checkpoint("2025-01-01T00:05:00.000Z", false),
|
||
checkpoint("2025-01-01T00:10:00.000Z", false),
|
||
checkpoint("2025-01-01T00:20:00.000Z", true),
|
||
],
|
||
"2025-01-01T00:00:00.000Z",
|
||
"2025-01-01T01:00:00.000Z",
|
||
);
|
||
|
||
expect(result.incidentCount).toBe(1);
|
||
expect(result.longestOutage).toBe(15 * 60 * 1000);
|
||
expect(result.mttr).toBe(15 * 60 * 1000);
|
||
});
|
||
|
||
test("连续状态支持 capped 标记", () => {
|
||
expect(
|
||
calculateCurrentStreak(
|
||
[
|
||
checkpoint("2025-01-01T00:00:00.000Z", true),
|
||
checkpoint("2025-01-01T00:01:00.000Z", false),
|
||
checkpoint("2025-01-01T00:02:00.000Z", false),
|
||
],
|
||
2,
|
||
),
|
||
).toEqual({ capped: true, count: 2, up: false });
|
||
});
|
||
|
||
test("UTC 小时趋势分桶返回 up/down 和延迟范围", () => {
|
||
const trend = buildHourlyTrend([
|
||
checkpoint("2025-01-01T00:10:00.000Z", true, 100),
|
||
checkpoint("2025-01-01T00:40:00.000Z", false, null),
|
||
checkpoint("2025-01-01T01:05:00.000Z", true, 300),
|
||
]);
|
||
|
||
expect(trend).toEqual([
|
||
{
|
||
availability: 50,
|
||
avgDurationMs: 100,
|
||
bucketStart: "2025-01-01T00:00:00.000Z",
|
||
downChecks: 1,
|
||
maxDurationMs: 100,
|
||
minDurationMs: 100,
|
||
totalChecks: 2,
|
||
upChecks: 1,
|
||
},
|
||
{
|
||
availability: 100,
|
||
avgDurationMs: 300,
|
||
bucketStart: "2025-01-01T01:00:00.000Z",
|
||
downChecks: 0,
|
||
maxDurationMs: 300,
|
||
minDurationMs: 300,
|
||
totalChecks: 1,
|
||
upChecks: 1,
|
||
},
|
||
]);
|
||
});
|
||
});
|
||
|
||
function checkpoint(timestamp: string, matched: boolean, durationMs: null | number = null): MetricCheckpoint {
|
||
return { durationMs, matched, timestamp };
|
||
}
|