Skip to content

Commit 9d45e62

Browse files
committed
feat: implement ErrorRateChart in AIGatewayAnalytics to visualize error rates over time
1 parent 6658ed1 commit 9d45e62

1 file changed

Lines changed: 132 additions & 18 deletions

File tree

src/client/components/aiGateway/AIGatewayAnalytics.tsx

Lines changed: 132 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,17 @@ import {
1414
} from '../insights/InsightQueryChart';
1515
import { Card, CardContent, CardHeader, CardTitle } from '../ui/card';
1616
import { Tabs, TabsContent, TabsList, TabsTrigger } from '../ui/tabs';
17+
import { TimeEventChart } from '../chart/TimeEventChart';
18+
import { LoadingView } from '../LoadingView';
19+
import { DateUnit, getDateArray } from '@tianji/shared';
20+
import type { InsightsRawData } from '@/hooks/useInsightsData';
21+
import type { ChartConfig } from '../ui/chart';
22+
23+
// Status color tokens used in the Errors tab.
24+
// Keep the success/failure colors aligned with the rest of the app
25+
// (see AIGatewayStatus: bg-green-* for Success, bg-red-* for Failed).
26+
const STATUS_SUCCESS_COLOR = '#22c55e';
27+
const STATUS_FAILED_COLOR = '#ef4444';
1728

1829
interface AIGatewayAnalyticsProps {
1930
gatewayId: string;
@@ -48,6 +59,22 @@ export const AIGatewayAnalytics: React.FC<AIGatewayAnalyticsProps> = React.memo(
4859
[startDate, endDate, unit]
4960
);
5061

62+
// Force Success vs Failure colors. Series keys follow the
63+
// `${alias|name}-${groupValue}` pattern from generateSeriesName.
64+
const statusChartConfig: ChartConfig = useMemo(
65+
() => ({
66+
'$all_event-Success': {
67+
label: t('Success'),
68+
color: STATUS_SUCCESS_COLOR,
69+
},
70+
'$all_event-Failed': {
71+
label: t('Failed'),
72+
color: STATUS_FAILED_COLOR,
73+
},
74+
}),
75+
[t]
76+
);
77+
5178
return (
5279
<div className="space-y-6">
5380
{/* Header */}
@@ -509,6 +536,7 @@ export const AIGatewayAnalytics: React.FC<AIGatewayAnalyticsProps> = React.memo(
509536
groups={[{ value: 'status', type: 'string' }]}
510537
time={timeConfig}
511538
chartType="pie"
539+
chartConfig={statusChartConfig}
512540
valueProcessor={defaultValueProcessor.alwaysPositive}
513541
/>
514542
</CardContent>
@@ -545,33 +573,22 @@ export const AIGatewayAnalytics: React.FC<AIGatewayAnalyticsProps> = React.memo(
545573
</Card>
546574
</div>
547575

548-
{/* Success Rate Over Time */}
576+
{/* Error Rate Over Time */}
549577
<Card>
550578
<CardHeader>
551-
<CardTitle>{t('Successful Requests Trend')}</CardTitle>
579+
<CardTitle>{t('Error Rate Trend')}</CardTitle>
552580
<p className="text-muted-foreground text-sm">
553-
{t('Number of events with Success status over time')}
581+
{t(
582+
'Percentage of failed requests out of total requests over time (Failed / Total %)'
583+
)}
554584
</p>
555585
</CardHeader>
556586
<CardContent>
557-
<InsightQueryChart
587+
<ErrorRateChart
558588
className="h-[300px] w-full"
559589
workspaceId={workspaceId}
560-
insightId={gatewayId}
561-
insightType="aigateway"
562-
metrics={[{ name: '$all_event', math: 'events' }]}
563-
filters={[
564-
{
565-
name: 'status',
566-
type: 'string',
567-
operator: 'equals',
568-
value: 'Success',
569-
},
570-
]}
571-
groups={[]}
590+
gatewayId={gatewayId}
572591
time={timeConfig}
573-
chartType="area"
574-
valueProcessor={defaultValueProcessor.alwaysPositive}
575592
/>
576593
</CardContent>
577594
</Card>
@@ -615,3 +632,100 @@ export const AIGatewayAnalytics: React.FC<AIGatewayAnalyticsProps> = React.memo(
615632
}
616633
);
617634
AIGatewayAnalytics.displayName = 'AIGatewayAnalytics';
635+
636+
interface ErrorRateChartProps {
637+
workspaceId: string;
638+
gatewayId: string;
639+
time: {
640+
startAt: number;
641+
endAt: number;
642+
unit: DateUnit;
643+
timezone?: string;
644+
};
645+
className?: string;
646+
}
647+
648+
const ErrorRateChart: React.FC<ErrorRateChartProps> = React.memo((props) => {
649+
const { workspaceId, gatewayId, time, className } = props;
650+
const { t } = useTranslation();
651+
652+
const { data: rawData = [], isLoading } = trpc.insights.query.useQuery(
653+
{
654+
workspaceId,
655+
insightId: gatewayId,
656+
insightType: 'aigateway',
657+
metrics: [{ name: '$all_event', math: 'events' }],
658+
filters: [],
659+
groups: [{ value: 'status', type: 'string' }],
660+
time: {
661+
timezone: getUserTimezone(),
662+
...time,
663+
},
664+
},
665+
{
666+
trpc: {
667+
context: {
668+
skipBatch: true,
669+
},
670+
},
671+
}
672+
);
673+
674+
const chartData = useMemo(() => {
675+
const dataArr = rawData as InsightsRawData[];
676+
// Aggregate success/failed counts per date bucket
677+
const dateMap = new Map<string, { success: number; failed: number }>();
678+
679+
dataArr.forEach((item) => {
680+
const status = (item as any).status;
681+
item.data.forEach((d) => {
682+
const entry = dateMap.get(d.date) ?? { success: 0, failed: 0 };
683+
if (status === 'Success') {
684+
entry.success += d.value;
685+
} else if (status === 'Failed') {
686+
entry.failed += d.value;
687+
}
688+
dateMap.set(d.date, entry);
689+
});
690+
});
691+
692+
const arr = Array.from(dateMap.entries()).map(
693+
([date, { success, failed }]) => {
694+
const total = success + failed;
695+
const rate = total > 0 ? (failed / total) * 100 : 0;
696+
return { date, value: Math.max(0, Math.min(100, rate)) };
697+
}
698+
);
699+
700+
if (arr.length === 0) {
701+
return [];
702+
}
703+
704+
return getDateArray(arr, time.startAt, time.endAt, time.unit);
705+
}, [rawData, time.startAt, time.endAt, time.unit]);
706+
707+
const chartConfig: ChartConfig = useMemo(
708+
() => ({
709+
value: {
710+
label: t('Error Rate'),
711+
color: STATUS_FAILED_COLOR,
712+
},
713+
}),
714+
[t]
715+
);
716+
717+
return (
718+
<LoadingView isLoading={isLoading}>
719+
<TimeEventChart
720+
className={className}
721+
data={chartData}
722+
unit={time.unit}
723+
chartConfig={chartConfig}
724+
chartType="area"
725+
yAxisDomain={[0, 100]}
726+
valueFormatter={(v) => `${v.toFixed(2)}%`}
727+
/>
728+
</LoadingView>
729+
);
730+
});
731+
ErrorRateChart.displayName = 'ErrorRateChart';

0 commit comments

Comments
 (0)