Skip to content

Commit 29f86d8

Browse files
committed
add rpc tab with analytics
1 parent 0ac3a8e commit 29f86d8

File tree

10 files changed

+650
-0
lines changed

10 files changed

+650
-0
lines changed

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

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import type {
1111
InsightStatusCodeStats,
1212
InsightUsageStats,
1313
RpcMethodStats,
14+
RpcUsageTypeStats,
1415
TransactionStats,
1516
UniversalBridgeStats,
1617
UniversalBridgeWalletStats,
@@ -229,6 +230,26 @@ export async function getRpcMethodUsage(
229230
return json.data as RpcMethodStats[];
230231
}
231232

233+
export async function getRpcUsageByType(
234+
params: AnalyticsQueryParams,
235+
): Promise<RpcUsageTypeStats[]> {
236+
const searchParams = buildSearchParams(params);
237+
const res = await fetchAnalytics(
238+
`v2/rpc/usage-types?${searchParams.toString()}`,
239+
{
240+
method: "GET",
241+
},
242+
);
243+
244+
if (res?.status !== 200) {
245+
console.error("Failed to fetch RPC usage");
246+
return [];
247+
}
248+
249+
const json = await res.json();
250+
return json.data as RpcUsageTypeStats[];
251+
}
252+
232253
export async function getWalletUsers(
233254
params: AnalyticsQueryParams,
234255
): Promise<WalletUserStats[]> {

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

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,12 @@ export interface RpcMethodStats {
4545
count: number;
4646
}
4747

48+
export interface RpcUsageTypeStats {
49+
date: string;
50+
usageType: string;
51+
count: number;
52+
}
53+
4854
export interface EngineCloudStats {
4955
date: string;
5056
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 Edge",
111+
},
106112
{
107113
href: `${layoutPath}/vault`,
108114
icon: LockIcon,
Lines changed: 107 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,107 @@
1+
"use client";
2+
import { useMemo } from "react";
3+
import type { ThirdwebClient } from "thirdweb";
4+
import { shortenLargeNumber } from "thirdweb/utils";
5+
import { ScrollShadow } from "@/components/ui/ScrollShadow/ScrollShadow";
6+
import { SkeletonContainer } from "@/components/ui/skeleton";
7+
import type { RpcMethodStats } from "@/types/analytics";
8+
import {
9+
CardHeading,
10+
TableData,
11+
TableHeading,
12+
TableHeadingRow,
13+
} from "../../universal-bridge/components/common";
14+
15+
export function TopRPCMethodsTable(props: {
16+
data: RpcMethodStats[];
17+
client: ThirdwebClient;
18+
}) {
19+
const tableData = useMemo(() => {
20+
return props.data?.sort((a, b) => b.count - a.count).slice(0, 30);
21+
}, [props.data]);
22+
const isEmpty = useMemo(() => tableData.length === 0, [tableData]);
23+
24+
return (
25+
<div className="flex flex-col">
26+
{/* header */}
27+
<div className="flex flex-col gap-2 lg:flex-row lg:items-center lg:justify-between">
28+
<CardHeading>Top EVM Methods Called </CardHeading>
29+
</div>
30+
31+
<div className="h-5" />
32+
<ScrollShadow
33+
className="overflow-hidden rounded-lg border"
34+
disableTopShadow={true}
35+
scrollableClassName="h-[280px]"
36+
>
37+
<table className="w-full">
38+
<thead>
39+
<TableHeadingRow>
40+
<TableHeading> Method </TableHeading>
41+
<TableHeading> Requests </TableHeading>
42+
</TableHeadingRow>
43+
</thead>
44+
<tbody className="relative">
45+
{tableData.map((method, i) => {
46+
return (
47+
<MethodTableRow
48+
client={props.client}
49+
method={method}
50+
key={method.evmMethod}
51+
rowIndex={i}
52+
/>
53+
);
54+
})}
55+
</tbody>
56+
</table>
57+
{isEmpty && (
58+
<div className="flex min-h-[240px] w-full items-center justify-center text-muted-foreground text-sm">
59+
No data available
60+
</div>
61+
)}
62+
</ScrollShadow>
63+
</div>
64+
);
65+
}
66+
67+
function MethodTableRow(props: {
68+
method?: {
69+
evmMethod: string;
70+
count: number;
71+
};
72+
client: ThirdwebClient;
73+
rowIndex: number;
74+
}) {
75+
const delayAnim = {
76+
animationDelay: `${props.rowIndex * 100}ms`,
77+
};
78+
79+
return (
80+
<tr className="border-border border-b">
81+
<TableData>
82+
<SkeletonContainer
83+
className="inline-flex"
84+
loadedData={props.method?.evmMethod}
85+
render={(v) => (
86+
<p className={"truncate max-w-[280px]"} title={v}>
87+
{v}
88+
</p>
89+
)}
90+
skeletonData="..."
91+
style={delayAnim}
92+
/>
93+
</TableData>
94+
<TableData>
95+
<SkeletonContainer
96+
className="inline-flex"
97+
loadedData={props.method?.count}
98+
render={(v) => {
99+
return <p>{shortenLargeNumber(v)}</p>;
100+
}}
101+
skeletonData={0}
102+
style={delayAnim}
103+
/>
104+
</TableData>
105+
</tr>
106+
);
107+
}
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 { ThirdwebAreaChart } from "@/components/blocks/charts/area-chart";
6+
import type { RpcUsageTypeStats } from "@/types/analytics";
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: 105 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,105 @@
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-6">
73+
<Skeleton className="h-[350px] border rounded-xl" />
74+
<div className="grid grid-cols-1 gap-6 xl:grid-cols-2">
75+
<Skeleton className="h-[350px] border rounded-xl" />
76+
<Skeleton className="h-[350px] border rounded-xl" />
77+
</div>
78+
<div className="grid grid-cols-1 gap-6 xl:grid-cols-2">
79+
<Skeleton className="h-[350px] border rounded-xl" />
80+
<Skeleton className="h-[350px] border rounded-xl" />
81+
</div>
82+
<Skeleton className="h-[500px] border rounded-xl" />
83+
</div>
84+
}
85+
searchParamsUsed={["from", "to", "interval"]}
86+
>
87+
<div className="flex flex-col gap-10 lg:gap-6">
88+
<div className="grid grid-cols-2 gap-4 lg:gap-6">
89+
<StatCard
90+
icon={ActivityIcon}
91+
isPending={false}
92+
label="All Time Requests"
93+
value={totalRequests}
94+
/>
95+
</div>
96+
<RequestsGraph data={usageData} />
97+
<TopRPCMethodsTable
98+
client={props.client}
99+
data={evmMethodsData || []}
100+
/>
101+
</div>
102+
</ResponsiveSuspense>
103+
</div>
104+
);
105+
}

0 commit comments

Comments
 (0)