1
0

feat: 动态粒度趋势图,支持 auto bucket 选择 + P95 延迟 + 状态条

This commit is contained in:
2026-05-23 23:53:18 +08:00
parent 6601ab458d
commit 4f33fba793
16 changed files with 315 additions and 106 deletions

View File

@@ -319,10 +319,12 @@ describe("API 路由", () => {
expect(body.trend[0]).toMatchObject({
availability: 50,
avgDurationMs: 150,
bucketEnd: "2025-01-01T01:00:00.000Z",
bucketStart: "2025-01-01T00:00:00.000Z",
downChecks: 2,
maxDurationMs: 200,
minDurationMs: 100,
p95DurationMs: 200,
totalChecks: 4,
upChecks: 2,
});
@@ -350,7 +352,11 @@ describe("API 路由", () => {
totalChecks: 0,
upChecks: 0,
});
expect(body.trend).toEqual([]);
expect(body.trend.length).toBeGreaterThan(0);
body.trend.forEach((point: { availability: number; totalChecks: number }) => {
expect(point.totalChecks).toBe(0);
expect(point.availability).toBe(0);
});
});
test("查询不存在的目标返回 404", async () => {
@@ -394,7 +400,7 @@ describe("API 路由", () => {
test("metrics 无效 bucket 和不存在目标返回错误", async () => {
const targets = store.getTargets();
const invalidBucket = await fetch(
`${baseUrl}/api/targets/${targets[0]!.id}/metrics?from=2025-01-01T00:00:00.000Z&to=2025-01-01T01:00:00.000Z&bucket=5m`,
`${baseUrl}/api/targets/${targets[0]!.id}/metrics?from=2025-01-01T00:00:00.000Z&to=2025-01-01T01:00:00.000Z&bucket=invalid`,
);
const missingTarget = await fetch(
`${baseUrl}/api/targets/99999/metrics?from=2025-01-01T00:00:00.000Z&to=2025-01-01T01:00:00.000Z`,

View File

@@ -2,11 +2,12 @@ import { describe, expect, test } from "bun:test";
import {
analyzeIncidentSequence,
buildHourlyTrend,
buildTrend,
calculateAvailability,
calculateCurrentStreak,
calculatePercentile,
type MetricCheckpoint,
resolveAutoBucket,
} from "../../src/server/metrics";
describe("后端指标计算", () => {
@@ -90,35 +91,47 @@ describe("后端指标计算", () => {
});
test("UTC 小时趋势分桶返回 up/down 和延迟范围", () => {
const trend = buildHourlyTrend([
const checkpoints = [
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),
]);
];
const trend = buildTrend(checkpoints, "2025-01-01T00:00:00.000Z", "2025-01-01T01:59:59.999Z", "1h");
expect(trend).toEqual([
{
availability: 50,
avgDurationMs: 100,
bucketEnd: "2025-01-01T01:00:00.000Z",
bucketStart: "2025-01-01T00:00:00.000Z",
downChecks: 1,
maxDurationMs: 100,
minDurationMs: 100,
p95DurationMs: 100,
totalChecks: 2,
upChecks: 1,
},
{
availability: 100,
avgDurationMs: 300,
bucketEnd: "2025-01-01T01:59:59.999Z",
bucketStart: "2025-01-01T01:00:00.000Z",
downChecks: 0,
maxDurationMs: 300,
minDurationMs: 300,
p95DurationMs: 300,
totalChecks: 1,
upChecks: 1,
},
]);
});
test("resolveAutoBucket 按窗口大小选择合适桶", () => {
expect(resolveAutoBucket(30_000, 7 * 24 * 60 * 60 * 1000)).toBe("1h");
expect(resolveAutoBucket(30_000, 60 * 60 * 1000)).toBe("30s");
expect(resolveAutoBucket(30_000, 24 * 60 * 60 * 1000)).toBe("15m");
});
});
function checkpoint(timestamp: string, matched: boolean, durationMs: null | number = null): MetricCheckpoint {

View File

@@ -154,9 +154,9 @@ describe("validateDashboardWindow", () => {
});
describe("validateMetricsBucket", () => {
test("默认值bucket=1h", () => {
test("默认值bucket=auto", () => {
const result = validateMetricsBucket(null, "production");
expect(result).toEqual({ bucket: "1h" });
expect(result).toEqual({ bucket: "auto" });
});
test("bucket=1h 返回成功", () => {
@@ -165,7 +165,7 @@ describe("validateMetricsBucket", () => {
});
test("不支持的 bucket 参数返回 400", () => {
const result = validateMetricsBucket("5m", "production");
const result = validateMetricsBucket("invalid", "production");
expect(result).toHaveProperty("status", 400);
});
});