Skip to content

Commit 553d373

Browse files
committed
add rpc tab with analytics
1 parent 84f8043 commit 553d373

File tree

12 files changed

+598
-9
lines changed

12 files changed

+598
-9
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({
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+
}
Lines changed: 100 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,100 @@
1+
import { ActivityIcon } from "lucide-react";
2+
import { ResponsiveSuspense } from "responsive-rsc";
3+
import type { ThirdwebClient } from "thirdweb";
4+
import { getRpcMethodUsage, getRpcUsageByType } from "@/api/analytics";
5+
import type { Range } from "@/components/analytics/date-range-selector";
6+
import { StatCard } from "@/components/analytics/stat";
7+
import { Skeleton } from "@/components/ui/skeleton";
8+
import { TopRPCMethodsTable } from "./MethodsTable";
9+
import { RequestsGraph } from "./RequestsGraph";
10+
import { RpcAnalyticsFilter } from "./RpcAnalyticsFilter";
11+
import { RpcFTUX } from "./RpcFtux";
12+
13+
export async function RPCAnalytics(props: {
14+
projectClientId: string;
15+
client: ThirdwebClient;
16+
projectId: string;
17+
teamId: string;
18+
range: Range;
19+
interval: "day" | "week";
20+
}) {
21+
const { projectId, teamId, range, interval } = props;
22+
23+
// TODO: add requests by status code, but currently not performant enough
24+
const allRequestsByUsageTypePromise = getRpcUsageByType({
25+
from: range.from,
26+
period: "all",
27+
projectId: projectId,
28+
teamId: teamId,
29+
to: range.to,
30+
});
31+
const requestsByUsageTypePromise = getRpcUsageByType({
32+
from: range.from,
33+
period: interval,
34+
projectId: projectId,
35+
teamId: teamId,
36+
to: range.to,
37+
});
38+
const evmMethodsPromise = getRpcMethodUsage({
39+
from: range.from,
40+
period: "all",
41+
projectId: projectId,
42+
teamId: teamId,
43+
to: range.to,
44+
}).catch((error) => {
45+
console.error(error);
46+
return [];
47+
});
48+
49+
const [allUsageData, usageData, evmMethodsData] = await Promise.all([
50+
allRequestsByUsageTypePromise,
51+
requestsByUsageTypePromise,
52+
evmMethodsPromise,
53+
]);
54+
55+
const totalRequests = allUsageData.reduce((acc, curr) => acc + curr.count, 0);
56+
57+
if (totalRequests < 1) {
58+
return (
59+
<div className="container flex max-w-7xl grow flex-col">
60+
<RpcFTUX clientId={props.projectClientId} />
61+
</div>
62+
);
63+
}
64+
65+
return (
66+
<div>
67+
<div className="mb-4 flex justify-start">
68+
<RpcAnalyticsFilter />
69+
</div>
70+
<ResponsiveSuspense
71+
fallback={
72+
<div className="flex flex-col gap-10 lg:gap-6">
73+
<div className="flex flex-col gap-4 lg:gap-6">
74+
<Skeleton className="h-20 border rounded-xl" />
75+
</div>
76+
<Skeleton className="h-[350px] border rounded-xl" />
77+
<Skeleton className="h-[500px] border rounded-xl" />
78+
</div>
79+
}
80+
searchParamsUsed={["from", "to", "interval"]}
81+
>
82+
<div className="flex flex-col gap-10 lg:gap-6">
83+
<div className="flex flex-col gap-4 lg:gap-6">
84+
<StatCard
85+
icon={ActivityIcon}
86+
isPending={false}
87+
label="All Time Requests"
88+
value={totalRequests}
89+
/>
90+
</div>
91+
<RequestsGraph data={usageData} />
92+
<TopRPCMethodsTable
93+
client={props.client}
94+
data={evmMethodsData || []}
95+
/>
96+
</div>
97+
</ResponsiveSuspense>
98+
</div>
99+
);
100+
}

0 commit comments

Comments
 (0)