Skip to content

Commit 3f42f52

Browse files
committed
Add RPC usage chart to dashboard (#6719)
closes: TOOL-4079 ### TL;DR Added RPC usage chart to the team usage overview page. ### What changed? - Created a new API endpoint `fetchRPCUsage` to retrieve RPC usage data from the analytics service - Added a stacked bar chart to visualize RPC usage in the team usage overview page - The chart displays both regular RPC requests and rate-limited requests - Replaced the static "Unlimited Requests" display with an interactive visualization ### How to test? 1. Navigate to a team's usage overview page 2. Verify that the RPC Requests section now shows a stacked bar chart 3. Confirm that the chart displays data for the past 7 days 4. Check that the chart properly differentiates between regular RPC requests and rate-limited requests ### Why make this change? This change provides teams with better visibility into their RPC usage patterns over time, allowing them to monitor both successful requests and rate-limited requests. This visualization helps teams understand their API consumption patterns and make informed decisions about their plan requirements. <!-- start pr-codex --> --- ## PR-Codex overview This PR introduces a new feature to fetch and display RPC usage data in the dashboard. It enhances the usage overview by integrating RPC metrics, providing insights into included and rate-limited requests. ### Detailed summary - Added `RPCUsageDataItem` type and `fetchRPCUsage` function in `rpc.ts`. - Updated `page.tsx` to fetch RPC usage data alongside account usage and subscriptions. - Passed `rpcUsage` data to the `Usage` component. - Modified `Usage` component to process and display RPC usage data using `ThirdwebBarChart`. > ✨ Ask PR-Codex anything about this PR by commenting with `/codex {your question}` <!-- end pr-codex -->
1 parent bb29e1f commit 3f42f52

File tree

3 files changed

+130
-23
lines changed

3 files changed

+130
-23
lines changed

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

+54
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
import "server-only";
2+
import { unstable_cache } from "next/cache";
3+
4+
export type RPCUsageDataItem = {
5+
date: string;
6+
usageType: "included" | "overage" | "rate-limit";
7+
count: string;
8+
};
9+
10+
export const fetchRPCUsage = unstable_cache(
11+
async (params: {
12+
teamId: string;
13+
projectId?: string;
14+
authToken: string;
15+
from: string;
16+
to: string;
17+
period: "day" | "week" | "month" | "year" | "all";
18+
}) => {
19+
const analyticsEndpoint = process.env.ANALYTICS_SERVICE_URL as string;
20+
const url = new URL(`${analyticsEndpoint}/v2/rpc/usage-types`);
21+
url.searchParams.set("teamId", params.teamId);
22+
if (params.projectId) {
23+
url.searchParams.set("projectId", params.projectId);
24+
}
25+
url.searchParams.set("from", params.from);
26+
url.searchParams.set("to", params.to);
27+
url.searchParams.set("period", params.period);
28+
29+
const res = await fetch(url, {
30+
headers: {
31+
Authorization: `Bearer ${params.authToken}`,
32+
},
33+
});
34+
35+
if (!res.ok) {
36+
const error = await res.text();
37+
return {
38+
ok: false as const,
39+
error: error,
40+
};
41+
}
42+
43+
const resData = await res.json();
44+
45+
return {
46+
ok: true as const,
47+
data: resData.data as RPCUsageDataItem[],
48+
};
49+
},
50+
["nebula-analytics"],
51+
{
52+
revalidate: 60 * 60, // 1 hour
53+
},
54+
);

apps/dashboard/src/app/team/[team_slug]/(team)/~/usage/overview/components/Usage.tsx

+55-21
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
import { getInAppWalletUsage, getUserOpUsage } from "@/api/analytics";
22
import type { Team } from "@/api/team";
33
import type { TeamSubscription } from "@/api/team-subscription";
4+
import type { RPCUsageDataItem } from "@/api/usage/rpc";
5+
import { ThirdwebBarChart } from "@/components/blocks/charts/bar-chart";
46
import { Button } from "@/components/ui/button";
57
import type {
68
Account,
@@ -28,6 +30,7 @@ type UsageProps = {
2830
image: string | null;
2931
slug: string;
3032
}[];
33+
rpcUsage: RPCUsageDataItem[];
3134
};
3235

3336
const compactNumberFormatter = new Intl.NumberFormat("en-US", {
@@ -42,24 +45,8 @@ export const Usage: React.FC<UsageProps> = ({
4245
team,
4346
client,
4447
projects,
48+
rpcUsage,
4549
}) => {
46-
// TODO - get this from team instead of account
47-
const rpcMetrics = useMemo(() => {
48-
if (!usageData) {
49-
return {};
50-
}
51-
52-
return {
53-
title: "Unlimited Requests",
54-
total: (
55-
<span className="text-muted-foreground">
56-
{compactNumberFormatter.format(usageData.rateLimits.rpc)} Requests Per
57-
Second
58-
</span>
59-
),
60-
};
61-
}, [usageData]);
62-
6350
const gatewayMetrics = useMemo(() => {
6451
if (!usageData) {
6552
return {};
@@ -91,6 +78,37 @@ export const Usage: React.FC<UsageProps> = ({
9178

9279
const storageUsage = team.capabilities.storage.upload;
9380

81+
const rpcUsageData = useMemo(() => {
82+
const mappedRPCUsage = rpcUsage.reduce(
83+
(acc, curr) => {
84+
switch (curr.usageType) {
85+
case "rate-limit":
86+
acc[curr.date] = {
87+
...(acc[curr.date] || {}),
88+
"rate-limit":
89+
(acc[curr.date]?.["rate-limit"] || 0) + Number(curr.count),
90+
included: acc[curr.date]?.included || 0,
91+
};
92+
break;
93+
default:
94+
acc[curr.date] = {
95+
...(acc[curr.date] || {}),
96+
"rate-limit": acc[curr.date]?.["rate-limit"] || 0,
97+
included: (acc[curr.date]?.included || 0) + Number(curr.count),
98+
};
99+
break;
100+
}
101+
return acc;
102+
},
103+
{} as Record<string, { included: number; "rate-limit": number }>,
104+
);
105+
106+
return Object.entries(mappedRPCUsage).map(([date, usage]) => ({
107+
time: new Date(date).getTime(),
108+
...usage,
109+
}));
110+
}, [rpcUsage]);
111+
94112
return (
95113
<div className="flex grow flex-col gap-8">
96114
<InAppWalletUsersChartCard
@@ -117,10 +135,26 @@ export const Usage: React.FC<UsageProps> = ({
117135
variant="team"
118136
/>
119137

120-
<UsageCard
121-
{...rpcMetrics}
122-
name="RPC"
123-
description="Amount of RPC requests allowed per second in your plan"
138+
<ThirdwebBarChart
139+
header={{
140+
title: "RPC Requests",
141+
description: `Your plan allows for ${usageData.rateLimits.rpc} requests per second`,
142+
titleClassName: "text-xl mb-0.5",
143+
}}
144+
data={rpcUsageData}
145+
isPending={false}
146+
variant="stacked"
147+
config={{
148+
included: {
149+
label: "RPC Requests",
150+
color: "hsl(var(--chart-1))",
151+
},
152+
"rate-limit": {
153+
label: "Rate Limited Requests",
154+
color: "hsl(var(--chart-3))",
155+
},
156+
}}
157+
chartClassName="aspect-[1.5] lg:aspect-[4]"
124158
/>
125159

126160
<UsageCard

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

+21-2
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,13 @@
11
import { getProjects } from "@/api/projects";
22
import { getTeamBySlug } from "@/api/team";
33
import { getTeamSubscriptions } from "@/api/team-subscription";
4+
import { fetchRPCUsage } from "@/api/usage/rpc";
45
import { getThirdwebClient } from "@/constants/thirdweb.server";
6+
import { normalizeTimeISOString } from "lib/time";
57
import { redirect } from "next/navigation";
68
import { getValidAccount } from "../../../../../account/settings/getAccount";
79
import { getAuthToken } from "../../../../../api/lib/getAuthToken";
10+
import { loginRedirect } from "../../../../../login/loginRedirect";
811
import { getAccountUsage } from "./getAccountUsage";
912
import { Usage } from "./overview/components/Usage";
1013

@@ -24,13 +27,28 @@ export default async function Page(props: {
2427
redirect("/team");
2528
}
2629

27-
const [accountUsage, subscriptions, projects] = await Promise.all([
30+
if (!authToken) {
31+
loginRedirect(`/team/${params.team_slug}/~/usage`);
32+
}
33+
34+
const [accountUsage, subscriptions, projects, rpcUsage] = await Promise.all([
2835
getAccountUsage(),
2936
getTeamSubscriptions(team.slug),
3037
getProjects(team.slug),
38+
fetchRPCUsage({
39+
authToken,
40+
period: "day",
41+
// 7 days ago
42+
from: normalizeTimeISOString(
43+
new Date(Date.now() - 1000 * 60 * 60 * 24 * 7),
44+
),
45+
// now
46+
to: normalizeTimeISOString(new Date()),
47+
teamId: team.id,
48+
}),
3149
]);
3250

33-
if (!accountUsage || !subscriptions || !authToken) {
51+
if (!accountUsage || !subscriptions) {
3452
return (
3553
<div className="flex min-h-[350px] items-center justify-center rounded-lg border p-4 text-destructive-text">
3654
Something went wrong. Please try again later.
@@ -48,6 +66,7 @@ export default async function Page(props: {
4866
account={account}
4967
team={team}
5068
client={client}
69+
rpcUsage={rpcUsage.ok ? rpcUsage.data : []}
5170
projects={projects.map((project) => ({
5271
id: project.id,
5372
name: project.name,

0 commit comments

Comments
 (0)