Skip to content

Commit 4bd4efc

Browse files
committed
add rpc tab with analytics (#7489)
# [Dashboard] Feature: Add RPC Edge analytics page <!-- start pr-codex --> --- ## PR-Codex overview This PR focuses on enhancing the RPC (Remote Procedure Call) analytics feature in the dashboard. It introduces new components, updates existing ones, and adds functionality for fetching and displaying RPC usage data and statistics. ### Detailed summary - Added `RpcMethodStats` and `RpcUsageTypeStats` interfaces in `analytics.ts`. - Introduced `getRpcUsageByType` function to fetch RPC usage data. - Updated `RpcMethodBarChartCardUI` to use new data structure. - Added new `RPC` link in `ProjectSidebarLayout`. - Created `RpcAnalyticsFilter` for filtering RPC analytics. - Implemented `RequestsGraph` to visualize RPC requests over time. - Developed `TopRPCMethodsTable` to display top EVM methods called. - Added `RpcFTUX` component for user onboarding in RPC section. - Updated layout files to integrate new components and improve styling. > ✨ Ask PR-Codex anything about this PR by commenting with `/codex {your question}` <!-- end pr-codex --> <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit * **New Features** * Introduced a dedicated "RPC" section in the project sidebar for easy access to RPC analytics. * Added a comprehensive RPC analytics dashboard, including: * Interactive requests graph and top RPC methods table. * Date range and interval filtering for analytics. * First-time user experience (FTUX) with ready-to-use code examples and documentation links. * Enhanced layout and navigation for the new RPC analytics section. * **Style** * Improved UI spacing, sizing, and visual consistency for analytics components and placeholders. <!-- end of auto-generated comment: release notes by coderabbit.ai -->
1 parent 84f8043 commit 4bd4efc

File tree

14 files changed

+602
-23
lines changed

14 files changed

+602
-23
lines changed

apps/dashboard/src/@/api/analytics.ts

Lines changed: 32 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,6 @@ import type {
66
EcosystemWalletStats,
77
EngineCloudStats,
88
InAppWalletStats,
9-
RpcMethodStats,
109
TransactionStats,
1110
UniversalBridgeStats,
1211
UniversalBridgeWalletStats,
@@ -40,6 +39,18 @@ interface InsightUsageStats {
4039
totalRequests: number;
4140
}
4241

42+
export interface RpcMethodStats {
43+
date: string;
44+
evmMethod: string;
45+
count: number;
46+
}
47+
48+
export interface RpcUsageTypeStats {
49+
date: string;
50+
usageType: string;
51+
count: number;
52+
}
53+
4354
async function fetchAnalytics(
4455
input: string | URL,
4556
init?: RequestInit,
@@ -251,6 +262,26 @@ export async function getRpcMethodUsage(
251262
return json.data as RpcMethodStats[];
252263
}
253264

265+
export async function getRpcUsageByType(
266+
params: AnalyticsQueryParams,
267+
): Promise<RpcUsageTypeStats[]> {
268+
const searchParams = buildSearchParams(params);
269+
const res = await fetchAnalytics(
270+
`v2/rpc/usage-types?${searchParams.toString()}`,
271+
{
272+
method: "GET",
273+
},
274+
);
275+
276+
if (res?.status !== 200) {
277+
console.error("Failed to fetch RPC usage");
278+
return [];
279+
}
280+
281+
const json = await res.json();
282+
return json.data as RpcUsageTypeStats[];
283+
}
284+
254285
export async function getWalletUsers(
255286
params: AnalyticsQueryParams,
256287
): Promise<WalletUserStats[]> {

apps/dashboard/src/@/types/analytics.ts

Lines changed: 0 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -39,12 +39,6 @@ export interface TransactionStats {
3939
count: number;
4040
}
4141

42-
export interface RpcMethodStats {
43-
date: string;
44-
evmMethod: string;
45-
count: number;
46-
}
47-
4842
export interface EngineCloudStats {
4943
date: string;
5044
chainId: string;

apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/(sidebar)/components/ProjectSidebarLayout.tsx

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import {
77
CoinsIcon,
88
HomeIcon,
99
LockIcon,
10+
RssIcon,
1011
SettingsIcon,
1112
WalletIcon,
1213
} from "lucide-react";
@@ -103,6 +104,11 @@ export function ProjectSidebarLayout(props: {
103104
icon: SmartAccountIcon,
104105
label: "Account Abstraction",
105106
},
107+
{
108+
href: `${layoutPath}/rpc`,
109+
icon: RssIcon,
110+
label: "RPC",
111+
},
106112
{
107113
href: `${layoutPath}/vault`,
108114
icon: LockIcon,

apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/(sidebar)/components/RpcMethodBarChartCard/RpcMethodBarChartCard.stories.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import type { Meta, StoryObj } from "@storybook/nextjs";
2+
import type { RpcMethodStats } from "@/api/analytics";
23
import { BadgeContainer } from "@/storybook/utils";
3-
import type { RpcMethodStats } from "@/types/analytics";
44
import { RpcMethodBarChartCardUI } from "./RpcMethodBarChartCardUI";
55

66
const meta = {

apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/(sidebar)/components/RpcMethodBarChartCard/RpcMethodBarChartCardUI.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,14 +7,14 @@ import {
77
BarChart as RechartsBarChart,
88
XAxis,
99
} from "recharts";
10+
import type { RpcMethodStats } from "@/api/analytics";
1011
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
1112
import {
1213
type ChartConfig,
1314
ChartContainer,
1415
ChartTooltip,
1516
ChartTooltipContent,
1617
} from "@/components/ui/chart";
17-
import type { RpcMethodStats } from "@/types/analytics";
1818
import { EmptyStateCard } from "../../../../../components/Analytics/EmptyStateCard";
1919

2020
export function RpcMethodBarChartCardUI({

apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/(sidebar)/insight/components/InsightAnalytics.tsx

Lines changed: 7 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -121,7 +121,7 @@ export async function InsightAnalytics(props: {
121121

122122
if (!hasVolume) {
123123
return (
124-
<div className="container flex max-w-7xl grow flex-col">
124+
<div className="flex grow flex-col">
125125
<InsightFTUX clientId={props.projectClientId} />
126126
</div>
127127
);
@@ -134,22 +134,18 @@ export async function InsightAnalytics(props: {
134134
</div>
135135
<ResponsiveSuspense
136136
fallback={
137-
<div className="flex flex-col gap-10 lg:gap-6">
137+
<div className="flex flex-col gap-6">
138138
<div className="grid grid-cols-2 gap-4 lg:gap-6">
139-
<Skeleton className="h-[120px] border rounded-xl" />
140-
<Skeleton className="h-[120px] border rounded-xl" />
141-
</div>
142-
<Skeleton className="h-[350px] border rounded-xl" />
143-
<div className="relative grid grid-cols-1 gap-6 rounded-xl border border-border bg-card p-4 lg:gap-12 xl:grid-cols-2 xl:p-6">
144-
<Skeleton className="h-[350px] border rounded-xl" />
145-
<Skeleton className="h-[350px] border rounded-xl" />
146-
<div className="absolute top-6 bottom-6 left-[50%] hidden w-[1px] bg-border xl:block" />
139+
<Skeleton className="h-[88px] border rounded-xl" />
140+
<Skeleton className="h-[88px] border rounded-xl" />
147141
</div>
142+
<Skeleton className="h-[480px] border rounded-xl" />
143+
<Skeleton className="h-[376px] border rounded-xl" />
148144
</div>
149145
}
150146
searchParamsUsed={["from", "to", "interval"]}
151147
>
152-
<div className="flex flex-col gap-10 lg:gap-6">
148+
<div className="flex flex-col gap-6">
153149
<div className="grid grid-cols-2 gap-4 lg:gap-6">
154150
<StatCard
155151
icon={ActivityIcon}

apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/(sidebar)/insight/layout.tsx

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ export default async function Layout(props: {
1919

2020
return (
2121
<div className="flex grow flex-col">
22-
<div className="pt-4 lg:pt-6">
22+
<div className="py-8 border-b">
2323
<div className="container max-w-7xl">
2424
<h1 className="mb-1 font-semibold text-2xl tracking-tight lg:text-3xl">
2525
Insight
@@ -36,8 +36,6 @@ export default async function Layout(props: {
3636
</UnderlineLink>
3737
</p>
3838
</div>
39-
40-
<div className="h-4" />
4139
</div>
4240

4341
<div className="h-6" />
Lines changed: 132 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,132 @@
1+
"use client";
2+
import { useMemo, useState } from "react";
3+
import type { ThirdwebClient } from "thirdweb";
4+
import { shortenLargeNumber } from "thirdweb/utils";
5+
import type { RpcMethodStats } from "@/api/analytics";
6+
import { PaginationButtons } from "@/components/blocks/pagination-buttons";
7+
import { Card } from "@/components/ui/card";
8+
import { SkeletonContainer } from "@/components/ui/skeleton";
9+
import {
10+
Table,
11+
TableBody,
12+
TableCell,
13+
TableContainer,
14+
TableHead,
15+
TableHeader,
16+
TableRow,
17+
} from "@/components/ui/table";
18+
import { CardHeading } from "../../universal-bridge/components/common";
19+
20+
export function TopRPCMethodsTable(props: {
21+
data: RpcMethodStats[];
22+
client: ThirdwebClient;
23+
}) {
24+
const [currentPage, setCurrentPage] = useState(1);
25+
const itemsPerPage = 30;
26+
27+
const sortedData = useMemo(() => {
28+
return props.data?.sort((a, b) => b.count - a.count) || [];
29+
}, [props.data]);
30+
31+
const totalPages = useMemo(() => {
32+
return Math.ceil(sortedData.length / itemsPerPage);
33+
}, [sortedData.length]);
34+
35+
const tableData = useMemo(() => {
36+
const startIndex = (currentPage - 1) * itemsPerPage;
37+
const endIndex = startIndex + itemsPerPage;
38+
return sortedData.slice(startIndex, endIndex);
39+
}, [sortedData, currentPage]);
40+
41+
const isEmpty = useMemo(() => sortedData.length === 0, [sortedData]);
42+
43+
return (
44+
<Card className="relative flex flex-col rounded-xl border border-border bg-card p-4">
45+
{/* header */}
46+
<div className="flex flex-col gap-2 lg:flex-row lg:items-center lg:justify-between">
47+
<CardHeading>Top EVM Methods Called </CardHeading>
48+
</div>
49+
50+
<div className="h-5" />
51+
<TableContainer scrollableContainerClassName="h-[280px]">
52+
<Table>
53+
<TableHeader className="sticky top-0 z-10 bg-background">
54+
<TableRow>
55+
<TableHead>Method</TableHead>
56+
<TableHead>Requests</TableHead>
57+
</TableRow>
58+
</TableHeader>
59+
<TableBody>
60+
{tableData.map((method, i) => {
61+
return (
62+
<MethodTableRow
63+
client={props.client}
64+
key={method.evmMethod}
65+
method={method}
66+
rowIndex={i}
67+
/>
68+
);
69+
})}
70+
</TableBody>
71+
</Table>
72+
{isEmpty && (
73+
<div className="flex min-h-[240px] w-full items-center justify-center text-muted-foreground text-sm">
74+
No data available
75+
</div>
76+
)}
77+
</TableContainer>
78+
79+
{!isEmpty && totalPages > 1 && (
80+
<div className="mt-4 flex justify-center">
81+
<PaginationButtons
82+
activePage={currentPage}
83+
onPageClick={setCurrentPage}
84+
totalPages={totalPages}
85+
/>
86+
</div>
87+
)}
88+
</Card>
89+
);
90+
}
91+
92+
function MethodTableRow(props: {
93+
method?: {
94+
evmMethod: string;
95+
count: number;
96+
};
97+
client: ThirdwebClient;
98+
rowIndex: number;
99+
}) {
100+
const delayAnim = {
101+
animationDelay: `${props.rowIndex * 100}ms`,
102+
};
103+
104+
return (
105+
<TableRow>
106+
<TableCell>
107+
<SkeletonContainer
108+
className="inline-flex"
109+
loadedData={props.method?.evmMethod}
110+
render={(v) => (
111+
<p className={"truncate max-w-[280px]"} title={v}>
112+
{v}
113+
</p>
114+
)}
115+
skeletonData="..."
116+
style={delayAnim}
117+
/>
118+
</TableCell>
119+
<TableCell>
120+
<SkeletonContainer
121+
className="inline-flex"
122+
loadedData={props.method?.count}
123+
render={(v) => {
124+
return <p>{shortenLargeNumber(v)}</p>;
125+
}}
126+
skeletonData={0}
127+
style={delayAnim}
128+
/>
129+
</TableCell>
130+
</TableRow>
131+
);
132+
}
Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
"use client";
2+
3+
import { format } from "date-fns";
4+
import { shortenLargeNumber } from "thirdweb/utils";
5+
import type { RpcUsageTypeStats } from "@/api/analytics";
6+
import { ThirdwebAreaChart } from "@/components/blocks/charts/area-chart";
7+
8+
export function RequestsGraph(props: { data: RpcUsageTypeStats[] }) {
9+
return (
10+
<ThirdwebAreaChart
11+
chartClassName="aspect-[1.5] lg:aspect-[4]"
12+
config={{
13+
requests: {
14+
color: "hsl(var(--chart-1))",
15+
label: "Count",
16+
},
17+
}}
18+
data={props.data
19+
.sort((a, b) => new Date(a.date).getTime() - new Date(b.date).getTime())
20+
.reduce(
21+
(acc, curr) => {
22+
const existingEntry = acc.find((e) => e.time === curr.date);
23+
if (existingEntry) {
24+
existingEntry.requests += curr.count;
25+
} else {
26+
acc.push({
27+
requests: curr.count,
28+
time: curr.date,
29+
});
30+
}
31+
return acc;
32+
},
33+
[] as { requests: number; time: string }[],
34+
)}
35+
header={{
36+
description: "Requests over time.",
37+
title: "RPC Requests",
38+
}}
39+
hideLabel={false}
40+
isPending={false}
41+
showLegend
42+
toolTipLabelFormatter={(label) => {
43+
return format(label, "MMM dd, HH:mm");
44+
}}
45+
toolTipValueFormatter={(value) => {
46+
return shortenLargeNumber(value as number);
47+
}}
48+
xAxis={{
49+
sameDay: true,
50+
}}
51+
yAxis
52+
/>
53+
);
54+
}

0 commit comments

Comments
 (0)