From 3f42f522b7b37b9674d356252cc94bea55f22401 Mon Sep 17 00:00:00 2001 From: jnsdls Date: Mon, 14 Apr 2025 20:27:11 +0000 Subject: [PATCH] Add RPC usage chart to dashboard (#6719) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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. --- ## 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}` --- apps/dashboard/src/@/api/usage/rpc.ts | 54 +++++++++++++ .../~/usage/overview/components/Usage.tsx | 76 ++++++++++++++----- .../team/[team_slug]/(team)/~/usage/page.tsx | 23 +++++- 3 files changed, 130 insertions(+), 23 deletions(-) create mode 100644 apps/dashboard/src/@/api/usage/rpc.ts diff --git a/apps/dashboard/src/@/api/usage/rpc.ts b/apps/dashboard/src/@/api/usage/rpc.ts new file mode 100644 index 00000000000..97112960966 --- /dev/null +++ b/apps/dashboard/src/@/api/usage/rpc.ts @@ -0,0 +1,54 @@ +import "server-only"; +import { unstable_cache } from "next/cache"; + +export type RPCUsageDataItem = { + date: string; + usageType: "included" | "overage" | "rate-limit"; + count: string; +}; + +export const fetchRPCUsage = unstable_cache( + async (params: { + teamId: string; + projectId?: string; + authToken: string; + from: string; + to: string; + period: "day" | "week" | "month" | "year" | "all"; + }) => { + const analyticsEndpoint = process.env.ANALYTICS_SERVICE_URL as string; + const url = new URL(`${analyticsEndpoint}/v2/rpc/usage-types`); + url.searchParams.set("teamId", params.teamId); + if (params.projectId) { + url.searchParams.set("projectId", params.projectId); + } + url.searchParams.set("from", params.from); + url.searchParams.set("to", params.to); + url.searchParams.set("period", params.period); + + const res = await fetch(url, { + headers: { + Authorization: `Bearer ${params.authToken}`, + }, + }); + + if (!res.ok) { + const error = await res.text(); + return { + ok: false as const, + error: error, + }; + } + + const resData = await res.json(); + + return { + ok: true as const, + data: resData.data as RPCUsageDataItem[], + }; + }, + ["nebula-analytics"], + { + revalidate: 60 * 60, // 1 hour + }, +); diff --git a/apps/dashboard/src/app/team/[team_slug]/(team)/~/usage/overview/components/Usage.tsx b/apps/dashboard/src/app/team/[team_slug]/(team)/~/usage/overview/components/Usage.tsx index f237f0c0604..c31345b042b 100644 --- a/apps/dashboard/src/app/team/[team_slug]/(team)/~/usage/overview/components/Usage.tsx +++ b/apps/dashboard/src/app/team/[team_slug]/(team)/~/usage/overview/components/Usage.tsx @@ -1,6 +1,8 @@ import { getInAppWalletUsage, getUserOpUsage } from "@/api/analytics"; import type { Team } from "@/api/team"; import type { TeamSubscription } from "@/api/team-subscription"; +import type { RPCUsageDataItem } from "@/api/usage/rpc"; +import { ThirdwebBarChart } from "@/components/blocks/charts/bar-chart"; import { Button } from "@/components/ui/button"; import type { Account, @@ -28,6 +30,7 @@ type UsageProps = { image: string | null; slug: string; }[]; + rpcUsage: RPCUsageDataItem[]; }; const compactNumberFormatter = new Intl.NumberFormat("en-US", { @@ -42,24 +45,8 @@ export const Usage: React.FC = ({ team, client, projects, + rpcUsage, }) => { - // TODO - get this from team instead of account - const rpcMetrics = useMemo(() => { - if (!usageData) { - return {}; - } - - return { - title: "Unlimited Requests", - total: ( - - {compactNumberFormatter.format(usageData.rateLimits.rpc)} Requests Per - Second - - ), - }; - }, [usageData]); - const gatewayMetrics = useMemo(() => { if (!usageData) { return {}; @@ -91,6 +78,37 @@ export const Usage: React.FC = ({ const storageUsage = team.capabilities.storage.upload; + const rpcUsageData = useMemo(() => { + const mappedRPCUsage = rpcUsage.reduce( + (acc, curr) => { + switch (curr.usageType) { + case "rate-limit": + acc[curr.date] = { + ...(acc[curr.date] || {}), + "rate-limit": + (acc[curr.date]?.["rate-limit"] || 0) + Number(curr.count), + included: acc[curr.date]?.included || 0, + }; + break; + default: + acc[curr.date] = { + ...(acc[curr.date] || {}), + "rate-limit": acc[curr.date]?.["rate-limit"] || 0, + included: (acc[curr.date]?.included || 0) + Number(curr.count), + }; + break; + } + return acc; + }, + {} as Record, + ); + + return Object.entries(mappedRPCUsage).map(([date, usage]) => ({ + time: new Date(date).getTime(), + ...usage, + })); + }, [rpcUsage]); + return (
= ({ variant="team" /> - Something went wrong. Please try again later. @@ -48,6 +66,7 @@ export default async function Page(props: { account={account} team={team} client={client} + rpcUsage={rpcUsage.ok ? rpcUsage.data : []} projects={projects.map((project) => ({ id: project.id, name: project.name,