Skip to content

Commit b40fd4f

Browse files
committed
add rpc tab with analytics
1 parent e3f5bb0 commit b40fd4f

File tree

12 files changed

+573
-9
lines changed

12 files changed

+573
-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: 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 type { RpcMethodStats } from "@/api/analytics";
6+
import { Card } from "@/components/ui/card";
7+
import { SkeletonContainer } from "@/components/ui/skeleton";
8+
import {
9+
Table,
10+
TableBody,
11+
TableCell,
12+
TableContainer,
13+
TableHead,
14+
TableHeader,
15+
TableRow,
16+
} from "@/components/ui/table";
17+
import { CardHeading } from "../../universal-bridge/components/common";
18+
19+
export function TopRPCMethodsTable(props: {
20+
data: RpcMethodStats[];
21+
client: ThirdwebClient;
22+
}) {
23+
const tableData = useMemo(() => {
24+
return props.data?.sort((a, b) => b.count - a.count).slice(0, 30);
25+
}, [props.data]);
26+
const isEmpty = useMemo(() => tableData.length === 0, [tableData]);
27+
28+
return (
29+
<Card className="relative flex flex-col rounded-xl border border-border bg-card p-4">
30+
{/* header */}
31+
<div className="flex flex-col gap-2 lg:flex-row lg:items-center lg:justify-between">
32+
<CardHeading>Top EVM Methods Called </CardHeading>
33+
</div>
34+
35+
<div className="h-5" />
36+
<TableContainer scrollableContainerClassName="h-[280px]">
37+
<Table>
38+
<TableHeader className="sticky top-0 z-10 bg-background">
39+
<TableRow>
40+
<TableHead>Method</TableHead>
41+
<TableHead>Requests</TableHead>
42+
</TableRow>
43+
</TableHeader>
44+
<TableBody>
45+
{tableData.map((method, i) => {
46+
return (
47+
<MethodTableRow
48+
client={props.client}
49+
key={method.evmMethod}
50+
method={method}
51+
rowIndex={i}
52+
/>
53+
);
54+
})}
55+
</TableBody>
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+
</TableContainer>
63+
</Card>
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+
<TableRow>
81+
<TableCell>
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+
</TableCell>
94+
<TableCell>
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+
</TableCell>
105+
</TableRow>
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 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)