Skip to content

Commit 35fd3bc

Browse files
[Dashboard] feat: Add transaction analytics and tracking (#6019)
1 parent 80a22ff commit 35fd3bc

File tree

9 files changed

+362
-13
lines changed

9 files changed

+362
-13
lines changed

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

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import type {
33
AnalyticsQueryParams,
44
InAppWalletStats,
55
RpcMethodStats,
6+
TransactionStats,
67
UserOpStats,
78
WalletStats,
89
WalletUserStats,
@@ -85,6 +86,27 @@ export async function getUserOpUsage(
8586
return json.data as UserOpStats[];
8687
}
8788

89+
export async function getClientTransactions(
90+
params: AnalyticsQueryParams,
91+
): Promise<TransactionStats[]> {
92+
const searchParams = buildSearchParams(params);
93+
const res = await fetchAnalytics(
94+
`v1/transactions/client?${searchParams.toString()}`,
95+
{
96+
method: "GET",
97+
headers: { "Content-Type": "application/json" },
98+
},
99+
);
100+
101+
if (res?.status !== 200) {
102+
console.error("Failed to fetch client transactions stats");
103+
return [];
104+
}
105+
106+
const json = await res.json();
107+
return json.data as TransactionStats[];
108+
}
109+
88110
export async function getRpcMethodUsage(
89111
params: AnalyticsQueryParams,
90112
): Promise<RpcMethodStats[]> {

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
@@ -106,7 +106,7 @@ export async function TotalSponsoredChartCardUI({
106106
return (
107107
<CombinedBarChartCard
108108
isCurrency
109-
title={title || "Total Sponsored"}
109+
title={title || "Gas Sponsored"}
110110
chartConfig={chartConfig}
111111
data={timeSeriesData}
112112
activeChart={
Lines changed: 128 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,128 @@
1+
import { cn } from "@/lib/utils";
2+
import { defineChain } from "thirdweb";
3+
import { type ChainMetadata, getChainMetadata } from "thirdweb/chains";
4+
import type { TransactionStats } from "types/analytics";
5+
import { EmptyAccountAbstractionChartContent } from "../../../../../components/smart-wallets/AccountAbstractionAnalytics/SponsoredTransactionsChartCard";
6+
import { BarChart } from "../../../components/Analytics/BarChart";
7+
import { CombinedBarChartCard } from "../../../components/Analytics/CombinedBarChartCard";
8+
9+
export async function TransactionsChartCardUI({
10+
data,
11+
aggregatedData,
12+
searchParams,
13+
className,
14+
onlyMainnet,
15+
title,
16+
description,
17+
}: {
18+
data: TransactionStats[];
19+
aggregatedData: TransactionStats[];
20+
searchParams?: { [key: string]: string | string[] | undefined };
21+
className?: string;
22+
onlyMainnet?: boolean;
23+
title?: string;
24+
description?: string;
25+
}) {
26+
const chains = await Promise.all(
27+
data.map(
28+
(item) =>
29+
// eslint-disable-next-line no-restricted-syntax
30+
item.chainId && getChainMetadata(defineChain(Number(item.chainId))),
31+
),
32+
).then((chains) => chains.filter((c) => c) as ChainMetadata[]);
33+
34+
// Process data to combine by date and chain type
35+
const dateMap = new Map<string, { mainnet: number; testnet: number }>();
36+
for (const item of data) {
37+
const chain = chains.find((c) => c.chainId === Number(item.chainId));
38+
39+
const existing = dateMap.get(item.date) || { mainnet: 0, testnet: 0 };
40+
if (chain?.testnet) {
41+
existing.testnet += item.count;
42+
} else {
43+
existing.mainnet += item.count;
44+
}
45+
dateMap.set(item.date, existing);
46+
}
47+
48+
// Convert to array and sort by date
49+
const timeSeriesData = Array.from(dateMap.entries())
50+
.map(([date, values]) => ({
51+
date,
52+
mainnet: values.mainnet,
53+
testnet: values.testnet,
54+
total: values.mainnet + values.testnet,
55+
}))
56+
.sort((a, b) => new Date(a.date).getTime() - new Date(b.date).getTime());
57+
58+
const processedAggregatedData = {
59+
mainnet: aggregatedData
60+
.filter(
61+
(d) => !chains.find((c) => c.chainId === Number(d.chainId))?.testnet,
62+
)
63+
.reduce((acc, curr) => acc + curr.count, 0),
64+
testnet: aggregatedData
65+
.filter(
66+
(d) => chains.find((c) => c.chainId === Number(d.chainId))?.testnet,
67+
)
68+
.reduce((acc, curr) => acc + curr.count, 0),
69+
total: aggregatedData.reduce((acc, curr) => acc + curr.count, 0),
70+
};
71+
72+
const chartConfig = {
73+
mainnet: {
74+
label: "Mainnet Chains",
75+
color: "hsl(var(--chart-1))",
76+
},
77+
testnet: {
78+
label: "Testnet Chains",
79+
color: "hsl(var(--chart-2))",
80+
},
81+
total: {
82+
label: "All Chains",
83+
color: "hsl(var(--chart-3))",
84+
},
85+
};
86+
87+
if (onlyMainnet) {
88+
const filteredData = timeSeriesData.filter((d) => d.mainnet > 0);
89+
return (
90+
<div className={cn("rounded-lg border p-4 lg:p-6", className)}>
91+
<h3 className="mb-1 font-semibold text-xl tracking-tight">
92+
{title || "Transactions"}
93+
</h3>
94+
<p className="text-muted-foreground"> {description}</p>
95+
<BarChart
96+
chartConfig={chartConfig}
97+
data={filteredData}
98+
activeKey="mainnet"
99+
emptyChartContent={<EmptyAccountAbstractionChartContent />}
100+
/>
101+
</div>
102+
);
103+
}
104+
105+
return (
106+
<CombinedBarChartCard
107+
title={title || "Transactions"}
108+
chartConfig={chartConfig}
109+
data={timeSeriesData}
110+
activeChart={
111+
(searchParams?.client_transactions as keyof typeof chartConfig) ??
112+
"mainnet"
113+
}
114+
queryKey="client_transactions"
115+
existingQueryParams={searchParams}
116+
aggregateFn={(_data, key) => processedAggregatedData[key]}
117+
className={className}
118+
// Get the trend from the last two COMPLETE periods
119+
trendFn={(data, key) =>
120+
data.filter((d) => (d[key] as number) > 0).length >= 3
121+
? ((data[data.length - 2]?.[key] as number) ?? 0) /
122+
((data[data.length - 3]?.[key] as number) ?? 0) -
123+
1
124+
: undefined
125+
}
126+
/>
127+
);
128+
}

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

Lines changed: 26 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import {
2+
getClientTransactions,
23
getInAppWalletUsage,
34
getUserOpUsage,
45
getWalletConnections,
@@ -31,6 +32,7 @@ import { getValidAccount } from "app/account/settings/getAccount";
3132
import { EmptyStateCard } from "app/team/components/Analytics/EmptyStateCard";
3233
import { Suspense } from "react";
3334
import { TotalSponsoredChartCardUI } from "../../_components/TotalSponsoredCard";
35+
import { TransactionsChartCardUI } from "../../_components/TransactionsCard";
3436

3537
// revalidate every 5 minutes
3638
export const revalidate = 300;
@@ -100,6 +102,8 @@ async function OverviewPageContent(props: {
100102
inAppWalletUsage,
101103
userOpUsageTimeSeries,
102104
userOpUsage,
105+
clientTransactionsTimeSeries,
106+
clientTransactions,
103107
] = await Promise.all([
104108
// Aggregated wallet connections
105109
getWalletConnections({
@@ -135,6 +139,19 @@ async function OverviewPageContent(props: {
135139
to: range.to,
136140
period: "all",
137141
}),
142+
// Client transactions
143+
getClientTransactions({
144+
accountId: account.id,
145+
from: range.from,
146+
to: range.to,
147+
period: interval,
148+
}),
149+
getClientTransactions({
150+
accountId: account.id,
151+
from: range.from,
152+
to: range.to,
153+
period: "all",
154+
}),
138155
]);
139156

140157
const isEmpty =
@@ -180,6 +197,14 @@ async function OverviewPageContent(props: {
180197
/>
181198
)}
182199
</div>
200+
{clientTransactions.length > 0 && (
201+
<TransactionsChartCardUI
202+
searchParams={searchParams}
203+
data={clientTransactionsTimeSeries}
204+
aggregatedData={clientTransactions}
205+
className="max-md:rounded-none max-md:border-r-0 max-md:border-l-0"
206+
/>
207+
)}
183208
{userOpUsage.length > 0 ? (
184209
<TotalSponsoredChartCardUI
185210
searchParams={searchParams}
@@ -189,7 +214,7 @@ async function OverviewPageContent(props: {
189214
/>
190215
) : (
191216
<EmptyStateCard
192-
metric="Sponsored Transactions"
217+
metric="Gas Sponsored"
193218
link="https://portal.thirdweb.com/typescript/v5/account-abstraction/get-started"
194219
/>
195220
)}

0 commit comments

Comments
 (0)