Skip to content

Commit 21f7987

Browse files
committed
feat(dashboard): rpc chart
1 parent 56eaad7 commit 21f7987

File tree

27 files changed

+250
-227
lines changed

27 files changed

+250
-227
lines changed

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

Lines changed: 8 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -1,48 +1,12 @@
11
import { fetchAnalytics } from "data/analytics/fetch-analytics";
2-
3-
export interface WalletStats {
4-
date: string;
5-
uniqueWalletsConnected: number;
6-
totalConnections: number;
7-
walletType: string;
8-
}
9-
10-
export interface WalletUserStats {
11-
date: string;
12-
newUsers: number;
13-
returningUsers: number;
14-
totalUsers: number;
15-
}
16-
17-
export interface InAppWalletStats {
18-
date: string;
19-
authenticationMethod: string;
20-
uniqueWalletsConnected: number;
21-
}
22-
23-
export interface EcosystemWalletStats extends InAppWalletStats { }
24-
25-
export interface UserOpStats {
26-
date: string;
27-
successful: number;
28-
failed: number;
29-
sponsoredUsd: number;
30-
chainId?: string;
31-
}
32-
33-
export interface RpcMethodStats {
34-
date: string;
35-
evmMethod: string;
36-
count: number;
37-
}
38-
39-
export interface AnalyticsQueryParams {
40-
clientId?: string;
41-
accountId?: string;
42-
from?: Date;
43-
to?: Date;
44-
period?: "day" | "week" | "month" | "year" | "all";
45-
}
2+
import type {
3+
AnalyticsQueryParams,
4+
InAppWalletStats,
5+
RpcMethodStats,
6+
UserOpStats,
7+
WalletStats,
8+
WalletUserStats,
9+
} from "types/analytics";
4610

4711
function buildSearchParams(params: AnalyticsQueryParams): URLSearchParams {
4812
const searchParams = new URLSearchParams();

apps/dashboard/src/@3rdweb-sdk/react/hooks/useApi.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,3 @@
1-
import type { UserOpStats } from "@/api/analytics";
21
import type { Team } from "@/api/team";
32
import {
43
type Query,
@@ -9,6 +8,7 @@ import {
98
import { THIRDWEB_ANALYTICS_API_HOST, THIRDWEB_API_HOST } from "constants/urls";
109
import { useAllChainsData } from "hooks/chains/allChains";
1110
import invariant from "tiny-invariant";
11+
import type { UserOpStats } from "types/analytics";
1212
import { accountKeys, apiKeys, authorizedWallets } from "../cache-keys";
1313
import { useLoggedInUser } from "./useLoggedInUser";
1414

apps/dashboard/src/app/team/[team_slug]/(team)/_components/TotalSponsoredCard.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
1-
import type { UserOpStats } from "@/api/analytics";
21
import { cn } from "@/lib/utils";
32
import { defineChain } from "thirdweb";
43
import { type ChainMetadata, getChainMetadata } from "thirdweb/chains";
4+
import type { UserOpStats } from "types/analytics";
55
import { EmptyAccountAbstractionChartContent } from "../../../../../components/smart-wallets/AccountAbstractionAnalytics/SponsoredTransactionsChartCard";
66
import { BarChart } from "../../../components/Analytics/BarChart";
77
import { CombinedBarChartCard } from "../../../components/Analytics/CombinedBarChartCard";

apps/dashboard/src/app/team/[team_slug]/(team)/page.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ import type {
1010
InAppWalletStats,
1111
WalletStats,
1212
WalletUserStats,
13-
} from "@/api/analytics";
13+
} from "types/analytics";
1414

1515
import {
1616
type DurationId,

apps/dashboard/src/app/team/[team_slug]/(team)/~/ecosystem/[slug]/(active)/analytics/components/EcosystemWalletUsersChartCard.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,4 @@
11
"use client";
2-
import type { EcosystemWalletStats } from "@/api/analytics";
32
import { ExportToCSVButton } from "@/components/blocks/ExportToCSVButton";
43
import {
54
type ChartConfig,
@@ -21,6 +20,7 @@ import { format } from "date-fns";
2120
import { formatTickerNumber } from "lib/format-utils";
2221
import { useMemo } from "react";
2322
import { Bar, BarChart, CartesianGrid, XAxis, YAxis } from "recharts";
23+
import type { EcosystemWalletStats } from "types/analytics";
2424

2525
type ChartData = Record<string, number> & {
2626
time: string; // human readable date

apps/dashboard/src/app/team/[team_slug]/(team)/~/ecosystem/[slug]/(active)/analytics/components/Summary.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
1-
import type { EcosystemWalletStats } from "@/api/analytics";
21
import { Stat } from "components/analytics/stat";
32
import { ActivityIcon, UserIcon } from "lucide-react";
3+
import type { EcosystemWalletStats } from "types/analytics";
44

55
export function EcosystemWalletsSummary(props: {
66
allTimeStats: EcosystemWalletStats[] | undefined;

apps/dashboard/src/app/team/[team_slug]/(team)/~/ecosystem/[slug]/(active)/components/ecosystem-header.client.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,4 @@
11
"use client";
2-
import type { EcosystemWalletStats } from "@/api/analytics";
32
import { CopyButton } from "@/components/ui/CopyButton";
43
import { Spinner } from "@/components/ui/Spinner/Spinner";
54
import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert";
@@ -24,6 +23,7 @@ import {
2423
} from "lucide-react";
2524
import Image from "next/image";
2625
import Link from "next/link";
26+
import type { EcosystemWalletStats } from "types/analytics";
2727
import { useEcosystemList } from "../../../hooks/use-ecosystem-list";
2828
import type { Ecosystem } from "../../../types";
2929
import { EcosystemWalletsSummary } from "../analytics/components/Summary";
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,86 @@
1+
import type { Meta, StoryObj } from "@storybook/react";
2+
import { BadgeContainer, mobileViewport } from "stories/utils";
3+
import type { RpcMethodStats } from "types/analytics";
4+
import { RpcMethodBarChartCardUI } from "./RpcMethodBarChartCardUI";
5+
6+
const meta = {
7+
title: "Analytics/RpcMethodBarChartCard",
8+
component: Component,
9+
parameters: {
10+
layout: "centered",
11+
},
12+
} satisfies Meta<typeof Component>;
13+
14+
export default meta;
15+
type Story = StoryObj<typeof meta>;
16+
17+
export const Desktop: Story = {
18+
parameters: {
19+
viewport: { defaultViewport: "desktop" },
20+
},
21+
};
22+
23+
export const Mobile: Story = {
24+
parameters: {
25+
viewport: mobileViewport("iphone14"),
26+
},
27+
};
28+
29+
const generateTimeSeriesData = (
30+
days: number,
31+
methods: string[],
32+
emptyData = false,
33+
) => {
34+
const data: RpcMethodStats[] = [];
35+
const today = new Date();
36+
37+
for (let i = days - 1; i >= 0; i--) {
38+
const date = new Date(today);
39+
date.setDate(date.getDate() - i);
40+
41+
for (const method of methods) {
42+
data.push({
43+
date: date.toISOString(),
44+
evmMethod: method,
45+
count: emptyData ? 0 : Math.floor(Math.random() * 1000) + 100,
46+
});
47+
}
48+
}
49+
50+
return data;
51+
};
52+
53+
const commonMethods = [
54+
"eth_call",
55+
"eth_getBalance",
56+
"eth_getTransactionReceipt",
57+
"eth_blockNumber",
58+
];
59+
60+
function Component() {
61+
return (
62+
<div className="container space-y-8 py-8">
63+
<BadgeContainer label="Normal Usage">
64+
<RpcMethodBarChartCardUI
65+
rawData={generateTimeSeriesData(30, commonMethods)}
66+
/>
67+
</BadgeContainer>
68+
69+
<BadgeContainer label="Empty Data">
70+
<RpcMethodBarChartCardUI rawData={[]} />
71+
</BadgeContainer>
72+
73+
<BadgeContainer label="Zero Values">
74+
<RpcMethodBarChartCardUI
75+
rawData={generateTimeSeriesData(30, commonMethods, true)}
76+
/>
77+
</BadgeContainer>
78+
79+
<BadgeContainer label="Single Method">
80+
<RpcMethodBarChartCardUI
81+
rawData={generateTimeSeriesData(30, ["eth_call"])}
82+
/>
83+
</BadgeContainer>
84+
</div>
85+
);
86+
}

apps/dashboard/src/app/team/components/Analytics/RpcMethodBarChartCard.tsx renamed to apps/dashboard/src/app/team/[team_slug]/[project_slug]/components/RpcMethodBarChartCard/RpcMethodBarChartCardUI.tsx

Lines changed: 48 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -1,37 +1,22 @@
1-
import {
2-
Bar,
3-
CartesianGrid,
4-
BarChart as RechartsBarChart,
5-
XAxis,
6-
YAxis,
7-
} from "recharts";
1+
"use client";
82
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
93
import {
10-
ChartConfig,
4+
type ChartConfig,
115
ChartContainer,
126
ChartTooltip,
137
ChartTooltipContent,
148
} from "@/components/ui/chart";
15-
import {
16-
AnalyticsQueryParams,
17-
getRpcMethodUsage,
18-
RpcMethodStats,
19-
} from "@/api/analytics";
20-
import { EmptyState } from "./EmptyState";
219
import { formatTickerNumber } from "lib/format-utils";
10+
import {
11+
Bar,
12+
CartesianGrid,
13+
BarChart as RechartsBarChart,
14+
XAxis,
15+
YAxis,
16+
} from "recharts";
17+
import type { RpcMethodStats } from "types/analytics";
18+
import { EmptyStateCard } from "../../../../components/Analytics/EmptyStateCard";
2219

23-
const chartConfig = {
24-
evmMethod: { label: "EVM Method", color: "hsl(var(--chart-1))" },
25-
count: { label: "Count", color: "hsl(var(--chart-2))" },
26-
};
27-
28-
export async function RpcMethodBarChartCard(props: AnalyticsQueryParams) {
29-
const rawData = await getRpcMethodUsage(props);
30-
31-
return <RpcMethodBarChartCardUI rawData={rawData} />;
32-
}
33-
34-
// Split the UI out for storybook mocking
3520
export function RpcMethodBarChartCardUI({
3621
rawData,
3722
}: { rawData: RpcMethodStats[] }) {
@@ -46,6 +31,28 @@ export function RpcMethodBarChartCardUI({
4631
);
4732
dateData[method] = methodData?.count ?? 0;
4833
}
34+
35+
// If we have too many methods to display well, add "other" and group the lowest keys for each time period
36+
if (uniqueMethods.length > 5) {
37+
// If we haven't added "other" as a key yet, add it
38+
if (!uniqueMethods.includes("Other")) {
39+
uniqueMethods.push("Other");
40+
}
41+
42+
// Sort the methods by their count for the time period
43+
const sortedMethods = uniqueMethods
44+
.filter((m) => m !== "Other")
45+
.sort(
46+
(a, b) =>
47+
((dateData[b] as number) ?? 0) - ((dateData[a] as number) ?? 0),
48+
);
49+
50+
dateData.Other = 0;
51+
for (const method of sortedMethods.slice(5, sortedMethods.length)) {
52+
dateData.Other += (dateData[method] as number) ?? 0;
53+
delete dateData[method];
54+
}
55+
}
4956
return dateData;
5057
});
5158

@@ -56,8 +63,13 @@ export function RpcMethodBarChartCardUI({
5663
};
5764
}
5865

59-
if (rawData.length === 0 || rawData.every((d) => d.count === 0)) {
60-
return <EmptyState />;
66+
if (
67+
data.length === 0 ||
68+
data.every((date) =>
69+
Object.keys(date).every((k) => k === "date" || date[k] === 0),
70+
)
71+
) {
72+
return <EmptyStateCard metric="RPC" link="https://portal.thirdweb.com/" />;
6173
}
6274

6375
return (
@@ -69,9 +81,7 @@ export function RpcMethodBarChartCardUI({
6981
</CardHeader>
7082
<CardContent className="px-2 sm:p-6 sm:pl-0">
7183
<ChartContainer
72-
config={{
73-
...chartConfig,
74-
}}
84+
config={config}
7585
className="aspect-auto h-[250px] w-full pt-6"
7686
>
7787
<RechartsBarChart
@@ -106,7 +116,6 @@ export function RpcMethodBarChartCardUI({
106116
<ChartTooltip
107117
content={
108118
<ChartTooltipContent
109-
className="w-[200px]"
110119
labelFormatter={(value) => {
111120
return new Date(value).toLocaleDateString("en-US", {
112121
month: "short",
@@ -125,8 +134,13 @@ export function RpcMethodBarChartCardUI({
125134
key={method}
126135
stackId="a"
127136
dataKey={method}
128-
radius={4}
129-
fill={`hsl(var(--chart-${idx}))`}
137+
radius={[
138+
idx === uniqueMethods.length - 1 ? 4 : 0,
139+
idx === uniqueMethods.length - 1 ? 4 : 0,
140+
idx === 0 ? 4 : 0,
141+
idx === 0 ? 4 : 0,
142+
]}
143+
fill={`hsl(var(--chart-${idx + 1}))`}
130144
/>
131145
))}
132146
</RechartsBarChart>
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
import { getRpcMethodUsage } from "@/api/analytics";
2+
import { LoadingChartState } from "components/analytics/empty-chart-state";
3+
import { Suspense } from "react";
4+
import type { AnalyticsQueryParams } from "types/analytics";
5+
import { RpcMethodBarChartCardUI } from "./RpcMethodBarChartCardUI";
6+
7+
export function RpcMethodBarChartCard(props: AnalyticsQueryParams) {
8+
return (
9+
// TODO: Add better LoadingChartState
10+
<Suspense
11+
fallback={
12+
<div className="h-[400px]">
13+
<LoadingChartState />
14+
</div>
15+
}
16+
>
17+
<RpcMethodBarChartCardAsync {...props} />
18+
</Suspense>
19+
);
20+
}
21+
22+
async function RpcMethodBarChartCardAsync(props: AnalyticsQueryParams) {
23+
const rawData = await getRpcMethodUsage(props);
24+
25+
return <RpcMethodBarChartCardUI rawData={rawData} />;
26+
}

0 commit comments

Comments
 (0)