fix: 统一 Ops 请求错误图表 SLA 口径

Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent)

Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
This commit is contained in:
benjamin 2026-05-18 16:52:44 +08:00
parent 6acb46c113
commit 09cec311e8
3 changed files with 154 additions and 5 deletions

View File

@ -30,7 +30,11 @@ const colors = computed(() => ({
text: isDarkMode.value ? '#9ca3af' : '#6b7280'
}))
const hasData = computed(() => (props.data?.total ?? 0) > 0)
const totalSlaErrors = computed(() =>
(props.data?.items ?? []).reduce((total, item) => total + Number(item.sla || 0), 0)
)
const hasData = computed(() => totalSlaErrors.value > 0)
const state = computed<ChartState>(() => {
if (hasData.value) return 'ready'
@ -54,7 +58,7 @@ const categories = computed<ErrorCategory[]>(() => {
for (const item of props.data.items || []) {
const code = Number(item.status_code || 0)
const count = Number(item.total || 0)
const count = Number(item.sla || 0)
if (!Number.isFinite(code) || !Number.isFinite(count)) continue
if ([502, 503, 504].includes(code)) upstream += count

View File

@ -45,9 +45,7 @@ const colors = computed(() => ({
text: isDarkMode.value ? '#9ca3af' : '#6b7280'
}))
const totalRequestErrors = computed(() =>
sumNumbers(props.points.map((p) => (p.error_count_sla ?? 0) + (p.business_limited_count ?? 0)))
)
const totalRequestErrors = computed(() => sumNumbers(props.points.map((p) => p.error_count_sla ?? 0)))
const totalUpstreamErrors = computed(() =>
sumNumbers(

View File

@ -0,0 +1,147 @@
import { mount } from '@vue/test-utils'
import { describe, expect, it, vi } from 'vitest'
import { defineComponent } from 'vue'
import OpsErrorDistributionChart from '../OpsErrorDistributionChart.vue'
import OpsErrorTrendChart from '../OpsErrorTrendChart.vue'
vi.mock('chart.js', () => ({
Chart: { register: vi.fn() },
ArcElement: {},
CategoryScale: {},
Filler: {},
Legend: {},
LineElement: {},
LinearScale: {},
PointElement: {},
Title: {},
Tooltip: {},
}))
vi.mock('vue-chartjs', async () => {
const { defineComponent } = await import('vue')
return {
Doughnut: defineComponent({
name: 'Doughnut',
props: {
data: { type: Object, required: true },
options: { type: Object, default: () => ({}) },
},
template: '<div class="doughnut-stub" />',
}),
Line: defineComponent({
name: 'Line',
props: {
data: { type: Object, required: true },
options: { type: Object, default: () => ({}) },
},
template: '<div class="line-stub" />',
}),
}
})
vi.mock('../../utils/opsFormatters', () => ({
formatHistoryLabel: (date: string | undefined) => date ?? '',
sumNumbers: (values: Array<number | null | undefined>) =>
values.reduce<number>((total, value) => total + (typeof value === 'number' && Number.isFinite(value) ? value : 0), 0),
}))
vi.mock('vue-i18n', async (importOriginal) => {
const actual = await importOriginal<typeof import('vue-i18n')>()
return {
...actual,
useI18n: () => ({
t: (key: string) => key,
}),
}
})
const HelpTooltipStub = defineComponent({
name: 'HelpTooltip',
props: {
content: { type: String, default: '' },
},
template: '<span class="help-tooltip-stub" />',
})
const EmptyStateStub = defineComponent({
name: 'EmptyState',
props: {
title: { type: String, default: '' },
description: { type: String, default: '' },
},
template: '<div class="empty-state-stub" />',
})
const globalStubs = {
stubs: {
HelpTooltip: HelpTooltipStub,
EmptyState: EmptyStateStub,
},
}
describe('Ops SLA-scoped error charts', () => {
it('错误分布图按 SLA 错误数统计,不把业务限制错误算进请求错误分布', () => {
const wrapper = mount(OpsErrorDistributionChart, {
props: {
loading: false,
data: {
total: 10,
items: [
{ status_code: 400, total: 7, sla: 2, business_limited: 5 },
{ status_code: 503, total: 3, sla: 0, business_limited: 3 },
],
},
},
global: globalStubs,
})
const doughnut = wrapper.findComponent({ name: 'Doughnut' })
expect(doughnut.exists()).toBe(true)
expect(doughnut.props('data')).toMatchObject({
labels: ['admin.ops.client'],
datasets: [{ data: [2] }],
})
})
it('错误分布图在只有业务限制错误时显示为空态', () => {
const wrapper = mount(OpsErrorDistributionChart, {
props: {
loading: false,
data: {
total: 4,
items: [{ status_code: 500, total: 4, sla: 0, business_limited: 4 }],
},
},
global: globalStubs,
})
expect(wrapper.findComponent({ name: 'Doughnut' }).exists()).toBe(false)
expect(wrapper.find('.empty-state-stub').exists()).toBe(true)
})
it('错误趋势图的请求错误详情按钮只按 SLA 错误启用', () => {
const wrapper = mount(OpsErrorTrendChart, {
props: {
loading: false,
timeRange: '1h',
points: [
{
bucket_start: '2026-05-18T00:00:00Z',
error_count_total: 5,
business_limited_count: 5,
error_count_sla: 0,
upstream_error_count_excl_429_529: 0,
upstream_429_count: 0,
upstream_529_count: 0,
},
],
},
global: globalStubs,
})
const requestErrorsButton = wrapper.findAll('button')[0]
expect(requestErrorsButton.attributes('disabled')).toBeDefined()
})
})