changed charts for app health

This commit is contained in:
Oliver Kieselbach
2026-04-07 17:04:47 +02:00
parent 68818ce4d1
commit a4dd59a845
3 changed files with 124 additions and 38 deletions
@@ -28,6 +28,10 @@ const AppBarChart = dynamic(() => import("../../../components/charts/AppBarChart
ssr: false,
loading: () => <div className="h-60 flex items-center justify-center text-gray-400 text-sm">Loading chart…</div>,
});
const AppStackedBarChart = dynamic(() => import("../../../components/charts/AppStackedBarChart"), {
ssr: false,
loading: () => <div className="h-64 flex items-center justify-center text-gray-400 text-sm">Loading chart…</div>,
});
interface TimeSeriesPoint {
bucketStart: string;
@@ -543,18 +547,17 @@ export default function AppDetailPage() {
</div>
)}
{/* Charts row 1: failure rate + duration over time */}
{/* Charts row 1: installs (success vs failure) + duration over time */}
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
<Card title="Failure Rate over time">
<Card title="Installs over time (success vs failure)">
{analytics.timeSeries.length > 0 ? (
<AppLineChart
<AppStackedBarChart
data={analytics.timeSeries as unknown as Array<Record<string, unknown>>}
xKey="bucketStart"
series={[
{ dataKey: "failureRate", label: "Failure rate", color: chartColors.danger },
stacks={[
{ dataKey: "succeeded", label: "Succeeded", color: chartColors.success },
{ dataKey: "failed", label: "Failed", color: chartColors.danger },
]}
yUnit="%"
yDomain={[0, "auto"]}
formatXTick={formatBucketTick}
/>
) : (
@@ -579,38 +582,39 @@ export default function AppDetailPage() {
</Card>
</div>
{/* Charts row 2: version breakdown + phase breakdown */}
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
<Card title="Failure Rate by Version">
{analytics.versionBreakdown.length > 0 ? (
<AppBarChart
data={analytics.versionBreakdown as unknown as Array<Record<string, unknown>>}
categoryKey="appVersion"
valueKey="failureRate"
valueFormatter={(v) => `${v}%`}
barColor={(row) => {
const r = Number(row.failureRate);
return r >= 20 ? chartColors.danger : r >= 5 ? chartColors.warning : chartColors.success;
}}
/>
) : (
<EmptyState text="No version data captured yet" />
{/* Charts row 2: version breakdown + phase breakdown.
Each card is hidden when empty so the row collapses gracefully:
- Version: will appear once new agents emit AppVersion data
- Phase: deferred feature, only shows once we have failure-log samples */}
{(analytics.versionBreakdown.length > 0 || analytics.installerPhaseBreakdown.length > 0) && (
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
{analytics.versionBreakdown.length > 0 && (
<Card title="Failure Rate by Version">
<AppBarChart
data={analytics.versionBreakdown as unknown as Array<Record<string, unknown>>}
categoryKey="appVersion"
valueKey="failureRate"
valueFormatter={(v) => `${v}%`}
barColor={(row) => {
const r = Number(row.failureRate);
return r >= 20 ? chartColors.danger : r >= 5 ? chartColors.warning : chartColors.success;
}}
/>
</Card>
)}
</Card>
<Card title="Installer Phase (failures only)">
{analytics.installerPhaseBreakdown.length > 0 ? (
<AppBarChart
data={analytics.installerPhaseBreakdown as unknown as Array<Record<string, unknown>>}
categoryKey="phase"
valueKey="failed"
horizontal
barColor={chartColors.danger}
/>
) : (
<EmptyState text="No failure phases recorded" />
{analytics.installerPhaseBreakdown.length > 0 && (
<Card title="Installer Phase (failures only)">
<AppBarChart
data={analytics.installerPhaseBreakdown as unknown as Array<Record<string, unknown>>}
categoryKey="phase"
valueKey="failed"
horizontal
barColor={chartColors.danger}
/>
</Card>
)}
</Card>
</div>
</div>
)}
{/* Top failure codes table */}
<Card title="Top Failure Codes">
@@ -0,0 +1,82 @@
"use client";
import {
ResponsiveContainer,
BarChart,
Bar,
XAxis,
YAxis,
CartesianGrid,
Tooltip,
Legend,
} from "recharts";
import { chartColors, tooltipStyle, axisTick, axisLine, legendStyle } from "./chartTheme";
export interface StackedSeries {
dataKey: string;
label: string;
color: string;
}
export interface AppStackedBarChartProps {
data: Array<Record<string, unknown>>;
xKey: string;
stacks: StackedSeries[];
height?: number;
formatXTick?: (value: unknown) => string;
}
/**
* Stacked bar chart used by the App Health detail page for "Installs per
* bucket (success vs failure)". Better than a failure-rate line when sample
* size per bucket is small — the rate is binary (0 % / 100 %) which produces
* useless spikes, while stacked counts immediately show both volume and
* success/failure ratio.
*/
export default function AppStackedBarChart({
data,
xKey,
stacks,
height = 260,
formatXTick,
}: AppStackedBarChartProps) {
return (
<ResponsiveContainer width="100%" height={height}>
<BarChart data={data} margin={{ top: 8, right: 16, left: 0, bottom: 4 }}>
<CartesianGrid stroke={chartColors.gridLine} strokeDasharray="3 3" vertical={false} />
<XAxis
dataKey={xKey}
tick={axisTick}
axisLine={axisLine}
tickLine={false}
tickFormatter={formatXTick ? (v: unknown) => formatXTick(v) : undefined}
minTickGap={24}
/>
<YAxis
tick={axisTick}
axisLine={axisLine}
tickLine={false}
allowDecimals={false}
width={32}
/>
<Tooltip
contentStyle={tooltipStyle}
labelStyle={{ color: chartColors.tooltipText, marginBottom: 4 }}
cursor={{ fill: chartColors.gridLine, opacity: 0.3 }}
labelFormatter={formatXTick ? (l: unknown) => formatXTick(l) : undefined}
/>
<Legend wrapperStyle={legendStyle} iconType="square" />
{stacks.map((s) => (
<Bar
key={s.dataKey}
dataKey={s.dataKey}
name={s.label}
stackId="installs"
fill={s.color}
radius={[0, 0, 0, 0]}
/>
))}
</BarChart>
</ResponsiveContainer>
);
}
File diff suppressed because one or more lines are too long