Skip to content

Commit 4cc2acc

Browse files
committed
Add RPC usage dashboard with rate limit monitoring
1 parent a3e7300 commit 4cc2acc

File tree

7 files changed

+422
-6
lines changed

7 files changed

+422
-6
lines changed

apps/dashboard/src/@/api/usage/rpc.ts

Lines changed: 60 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -47,8 +47,67 @@ export const fetchRPCUsage = unstable_cache(
4747
data: resData.data as RPCUsageDataItem[],
4848
};
4949
},
50-
["nebula-analytics"],
50+
["rpc-usage"],
5151
{
5252
revalidate: 60 * 60, // 1 hour
5353
},
5454
);
55+
56+
export type Last24HoursRPCUsageApiResponse = {
57+
peakRate: {
58+
date: string;
59+
peakRPS: number;
60+
};
61+
averageRate: {
62+
date: string;
63+
averageRate: number;
64+
includedCount: number;
65+
rateLimitedCount: number;
66+
overageCount: number;
67+
}[];
68+
totalCounts: {
69+
includedCount: number;
70+
rateLimitedCount: number;
71+
overageCount: number;
72+
};
73+
};
74+
75+
export const getLast24HoursRPCUsage = unstable_cache(
76+
async (params: {
77+
teamId: string;
78+
projectId?: string;
79+
authToken: string;
80+
}) => {
81+
const analyticsEndpoint = process.env.ANALYTICS_SERVICE_URL as string;
82+
const url = new URL(`${analyticsEndpoint}/v2/rpc/24-hours`);
83+
url.searchParams.set("teamId", params.teamId);
84+
if (params.projectId) {
85+
url.searchParams.set("projectId", params.projectId);
86+
}
87+
88+
const res = await fetch(url, {
89+
headers: {
90+
Authorization: `Bearer ${params.authToken}`,
91+
},
92+
});
93+
94+
if (!res.ok) {
95+
const error = await res.text();
96+
return {
97+
ok: false as const,
98+
error: error,
99+
};
100+
}
101+
102+
const resData = await res.json();
103+
104+
return {
105+
ok: true as const,
106+
data: resData.data as Last24HoursRPCUsageApiResponse,
107+
};
108+
},
109+
["rpc-usage-last-24-hours"],
110+
{
111+
revalidate: 60, // 1 minute
112+
},
113+
);

apps/dashboard/src/@/components/blocks/charts/area-chart.tsx

Lines changed: 40 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import {
44
Card,
55
CardContent,
66
CardDescription,
7+
CardFooter,
78
CardHeader,
89
CardTitle,
910
} from "@/components/ui/card";
@@ -17,7 +18,7 @@ import {
1718
} from "@/components/ui/chart";
1819
import { formatDate } from "date-fns";
1920
import { useMemo } from "react";
20-
import { Area, AreaChart, CartesianGrid, XAxis } from "recharts";
21+
import { Area, AreaChart, CartesianGrid, XAxis, YAxis } from "recharts";
2122
import {
2223
EmptyChartState,
2324
LoadingChartState,
@@ -30,11 +31,17 @@ type ThirdwebAreaChartProps<TConfig extends ChartConfig> = {
3031
description?: string;
3132
titleClassName?: string;
3233
};
34+
footer?: React.ReactNode;
3335
customHeader?: React.ReactNode;
3436
// chart config
3537
config: TConfig;
3638
data: Array<Record<keyof TConfig, number> & { time: number | string | Date }>;
3739
showLegend?: boolean;
40+
maxLimit?: number;
41+
yAxis?: boolean;
42+
xAxis?: {
43+
sameDay?: boolean;
44+
};
3845

3946
// chart className
4047
chartClassName?: string;
@@ -70,17 +77,33 @@ export function ThirdwebAreaChart<TConfig extends ChartConfig>(
7077
) : props.data.length === 0 ? (
7178
<EmptyChartState />
7279
) : (
73-
<AreaChart accessibilityLayer data={props.data}>
80+
<AreaChart
81+
accessibilityLayer
82+
data={
83+
props.maxLimit
84+
? props.data.map((d) => ({
85+
...d,
86+
maxLimit: props.maxLimit,
87+
}))
88+
: props.data
89+
}
90+
>
7491
<CartesianGrid vertical={false} />
92+
{props.yAxis && <YAxis tickLine={false} axisLine={false} />}
7593
<XAxis
7694
dataKey="time"
7795
tickLine={false}
7896
axisLine={false}
7997
tickMargin={20}
80-
tickFormatter={(value) => formatDate(new Date(value), "MMM dd")}
98+
tickFormatter={(value) =>
99+
formatDate(
100+
new Date(value),
101+
props.xAxis?.sameDay ? "MMM dd, HH:mm" : "MMM dd",
102+
)
103+
}
81104
/>
82105
<ChartTooltip
83-
cursor={false}
106+
cursor={true}
84107
content={
85108
<ChartTooltipContent
86109
hideLabel={
@@ -124,6 +147,16 @@ export function ThirdwebAreaChart<TConfig extends ChartConfig>(
124147
stackId="a"
125148
/>
126149
))}
150+
{props.maxLimit && (
151+
<Area
152+
type="monotone"
153+
dataKey="maxLimit"
154+
stroke="#ef4444"
155+
strokeWidth={2}
156+
strokeDasharray="5 5"
157+
fill="none"
158+
/>
159+
)}
127160

128161
{props.showLegend && (
129162
<ChartLegend
@@ -134,6 +167,9 @@ export function ThirdwebAreaChart<TConfig extends ChartConfig>(
134167
)}
135168
</ChartContainer>
136169
</CardContent>
170+
{props.footer && (
171+
<CardFooter className="w-full">{props.footer}</CardFooter>
172+
)}
137173
</Card>
138174
);
139175
}

apps/dashboard/src/@/components/ui/chart.tsx

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -243,7 +243,9 @@ const ChartTooltipContent = React.forwardRef<
243243
<div className="grid gap-1.5">
244244
{nestLabel ? tooltipLabel : null}
245245
<span className="text-muted-foreground">
246-
{itemConfig?.label || item.name}
246+
{item.name === "maxLimit"
247+
? "Upper Limit"
248+
: itemConfig?.label || item.name}
247249
</span>
248250
</div>
249251
{item.value !== undefined && (

apps/dashboard/src/app/team/[team_slug]/(team)/~/usage/layout.tsx

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,11 @@ export default async function Layout(props: {
2323
exactMatch: true,
2424
label: "Overview",
2525
},
26+
{
27+
href: `/team/${params.team_slug}/~/usage/rpc`,
28+
exactMatch: true,
29+
label: "RPC",
30+
},
2631
{
2732
href: `/team/${params.team_slug}/~/usage/storage`,
2833
exactMatch: true,
Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
"use client";
2+
3+
import { ThirdwebAreaChart } from "@/components/blocks/charts/area-chart";
4+
import { formatDate } from "date-fns";
5+
6+
export function CountGraph(props: {
7+
peakPercentage: number;
8+
currentRateLimit: number;
9+
data: {
10+
date: string;
11+
includedCount: number;
12+
overageCount: number;
13+
rateLimitedCount: number;
14+
}[];
15+
}) {
16+
return (
17+
<ThirdwebAreaChart
18+
header={{
19+
title: "Requests Over Time",
20+
description: "Requests over the last 24 hours. All times in UTC.",
21+
}}
22+
config={{
23+
includedCount: {
24+
label: "Successful Requests",
25+
color: "hsl(var(--chart-1))",
26+
},
27+
rateLimitedCount: {
28+
label: "Rate Limited Requests",
29+
color: "hsl(var(--chart-4))",
30+
},
31+
}}
32+
showLegend
33+
yAxis
34+
xAxis={{
35+
sameDay: true,
36+
}}
37+
hideLabel={false}
38+
toolTipLabelFormatter={(label) => {
39+
return formatDate(new Date(label), "MMM dd, HH:mm");
40+
}}
41+
data={props.data
42+
.slice(1, -1)
43+
.sort((a, b) => new Date(a.date).getTime() - new Date(b.date).getTime())
44+
.map((v) => ({
45+
time: v.date,
46+
includedCount: v.includedCount + v.overageCount,
47+
rateLimitedCount: v.rateLimitedCount,
48+
}))}
49+
isPending={false}
50+
/>
51+
);
52+
}
Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
"use client";
2+
3+
import { ThirdwebAreaChart } from "@/components/blocks/charts/area-chart";
4+
import { formatDate } from "date-fns";
5+
import { InfoIcon } from "lucide-react";
6+
7+
export function RateGraph(props: {
8+
peakPercentage: number;
9+
currentRateLimit: number;
10+
data: { date: string; averageRate: number }[];
11+
}) {
12+
return (
13+
<ThirdwebAreaChart
14+
header={{
15+
title: "Request Rate Over Time",
16+
description: "Request rate over the last 24 hours. All times in UTC.",
17+
}}
18+
// only show the footer if the peak usage is greater than 80%
19+
footer={
20+
props.peakPercentage > 80 ? (
21+
<div className="flex items-center justify-center gap-2">
22+
<InfoIcon className="h-4 w-4 text-muted-foreground" />
23+
<p className="text-muted-foreground text-xs">
24+
The red dashed line represents your current plan rate limit (
25+
{props.currentRateLimit} RPS)
26+
</p>
27+
</div>
28+
) : undefined
29+
}
30+
config={{
31+
averageRate: {
32+
label: "Average RPS",
33+
color: "hsl(var(--chart-1))",
34+
},
35+
}}
36+
data={props.data
37+
.slice(1, -1)
38+
.sort((a, b) => new Date(a.date).getTime() - new Date(b.date).getTime())
39+
.map((v) => ({
40+
time: v.date,
41+
averageRate: Number(v.averageRate.toFixed(2)),
42+
}))}
43+
yAxis
44+
xAxis={{
45+
sameDay: true,
46+
}}
47+
hideLabel={false}
48+
toolTipLabelFormatter={(label) => {
49+
return formatDate(new Date(label), "MMM dd, HH:mm");
50+
}}
51+
// only show the upper limit if the peak usage is greater than 80%
52+
maxLimit={props.peakPercentage > 80 ? props.currentRateLimit : undefined}
53+
isPending={false}
54+
/>
55+
);
56+
}

0 commit comments

Comments
 (0)