Skip to content

Commit 348e3ab

Browse files
committed
Add RPC usage dashboard with rate limit monitoring (#6733)
currently blocked by analytics API needing to go to prod <!-- start pr-codex --> --- ## PR-Codex overview This PR introduces a new RPC usage dashboard for monitoring requests over the last 24 hours. It adds new components for visualizing request data and enhances the existing layout to accommodate this functionality. ### Detailed summary - Added a new `RPC` tab in the usage layout. - Introduced `CountGraph` and `RateGraph` components for visualizing RPC data. - Implemented API call to fetch last 24 hours RPC usage data. - Enhanced `ThirdwebAreaChart` to support max limit display. - Updated error handling and user feedback for API data fetching. > ✨ Ask PR-Codex anything about this PR by commenting with `/codex {your question}` <!-- end pr-codex -->
1 parent e4590a7 commit 348e3ab

File tree

7 files changed

+427
-6
lines changed

7 files changed

+427
-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+
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: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
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+
chartClassName="aspect-[1.5] lg:aspect-[4]"
19+
header={{
20+
title: "Requests Over Time",
21+
description: "Requests over the last 24 hours. All times in UTC.",
22+
}}
23+
config={{
24+
includedCount: {
25+
label: "Successful Requests",
26+
color: "hsl(var(--chart-1))",
27+
},
28+
rateLimitedCount: {
29+
label: "Rate Limited Requests",
30+
color: "hsl(var(--chart-4))",
31+
},
32+
}}
33+
showLegend
34+
yAxis
35+
xAxis={{
36+
sameDay: true,
37+
}}
38+
hideLabel={false}
39+
toolTipLabelFormatter={(label) => {
40+
return formatDate(new Date(label), "MMM dd, HH:mm");
41+
}}
42+
data={props.data
43+
.slice(1, -1)
44+
.sort((a, b) => new Date(a.date).getTime() - new Date(b.date).getTime())
45+
.map((v) => ({
46+
time: v.date,
47+
includedCount: v.includedCount + v.overageCount,
48+
rateLimitedCount: v.rateLimitedCount,
49+
}))}
50+
isPending={false}
51+
/>
52+
);
53+
}
Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
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+
chartClassName="aspect-[1.5] lg:aspect-[4]"
15+
header={{
16+
title: "Request Rate Over Time",
17+
description: "Request rate over the last 24 hours. All times in UTC.",
18+
}}
19+
// only show the footer if the peak usage is greater than 80%
20+
footer={
21+
props.peakPercentage > 80 ? (
22+
<div className="flex items-center justify-center gap-2">
23+
<InfoIcon className="h-4 w-4 text-muted-foreground" />
24+
<p className="text-muted-foreground text-xs">
25+
The red dashed line represents your current plan rate limit (
26+
{props.currentRateLimit} RPS)
27+
</p>
28+
</div>
29+
) : undefined
30+
}
31+
config={{
32+
averageRate: {
33+
label: "Average RPS",
34+
color: "hsl(var(--chart-1))",
35+
},
36+
}}
37+
data={props.data
38+
.slice(1, -1)
39+
.sort((a, b) => new Date(a.date).getTime() - new Date(b.date).getTime())
40+
.map((v) => ({
41+
time: v.date,
42+
averageRate: Number(v.averageRate.toFixed(2)),
43+
}))}
44+
yAxis
45+
xAxis={{
46+
sameDay: true,
47+
}}
48+
hideLabel={false}
49+
toolTipLabelFormatter={(label) => {
50+
return formatDate(new Date(label), "MMM dd, HH:mm");
51+
}}
52+
// only show the upper limit if the peak usage is greater than 80%
53+
maxLimit={props.peakPercentage > 80 ? props.currentRateLimit : undefined}
54+
isPending={false}
55+
/>
56+
);
57+
}

0 commit comments

Comments
 (0)