Skip to content

Commit 6e4cd21

Browse files
committed
[Dashboard] Feature: AA Analytics (#5060)
![Screenshot 2024-10-16 at 6 26 17 PM](https://github.com/user-attachments/assets/0aa9b2f3-2891-4078-9251-7ba60a45f8df) <!-- start pr-codex --> --- ## PR-Codex overview This PR focuses on enhancing the analytics dashboard by adding user operation statistics, improving chart components, and correcting minor naming inconsistencies. ### Detailed summary - Fixed spelling of `DailyConnections` in `DailyConnectionsChartCard`. - Updated the title of `DailyConnections` in its story file. - Introduced `userOpStats` functions and types in `useApi.ts` and `storyUtils.ts`. - Integrated `userOpUsage` and `aggregateUserOpUsage` in `ConnectAnalyticsDashboard`. - Added `TotalSponsoredChartCard` and `SponsoredTransactionsChartCard` components with user operation stats. - Modified `ConnectAnalyticsDashboardUI` to include new user operation metrics. - Enhanced chart data handling for user operations in `TotalSponsoredChartCard` and `SponsoredTransactionsChartCard`. > ✨ Ask PR-Codex anything about this PR by commenting with `/codex {your question}` <!-- end pr-codex -->
1 parent 7738020 commit 6e4cd21

13 files changed

+609
-9
lines changed

apps/dashboard/src/@3rdweb-sdk/react/cache-keys.ts

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,21 @@ export const accountKeys = {
2121
to,
2222
period,
2323
] as const,
24+
userOpStats: (
25+
walletAddress: string,
26+
clientId: string,
27+
from: string,
28+
to: string,
29+
period: string,
30+
) =>
31+
[
32+
...accountKeys.wallet(walletAddress),
33+
"userOps",
34+
clientId,
35+
from,
36+
to,
37+
period,
38+
] as const,
2439
credits: (walletAddress: string) =>
2540
[...accountKeys.wallet(walletAddress), "credits"] as const,
2641
billingSession: (walletAddress: string) =>

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

Lines changed: 101 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -234,6 +234,13 @@ export interface WalletStats {
234234
walletType: string;
235235
}
236236

237+
export interface UserOpStats {
238+
date: string;
239+
successful: number;
240+
failed: number;
241+
sponsoredUsd: number;
242+
}
243+
237244
interface BillingProduct {
238245
name: string;
239246
id: string;
@@ -383,6 +390,100 @@ async function getWalletUsage(args: {
383390
return json.data;
384391
}
385392

393+
async function getUserOpUsage(args: {
394+
clientId: string;
395+
from?: Date;
396+
to?: Date;
397+
period?: "day" | "week" | "month" | "year" | "all";
398+
}) {
399+
const { clientId, from, to, period } = args;
400+
401+
const searchParams = new URLSearchParams();
402+
searchParams.append("clientId", clientId);
403+
if (from) {
404+
searchParams.append("from", from.toISOString());
405+
}
406+
if (to) {
407+
searchParams.append("to", to.toISOString());
408+
}
409+
if (period) {
410+
searchParams.append("period", period);
411+
}
412+
const res = await fetch(
413+
`${THIRDWEB_ANALYTICS_API_HOST}/v1/user-ops?${searchParams.toString()}`,
414+
{
415+
method: "GET",
416+
headers: {
417+
"Content-Type": "application/json",
418+
},
419+
},
420+
);
421+
const json = await res.json();
422+
423+
if (res.status !== 200) {
424+
throw new Error(json.message);
425+
}
426+
427+
return json.data;
428+
}
429+
430+
export function useUserOpUsageAggregate(args: {
431+
clientId: string;
432+
from?: Date;
433+
to?: Date;
434+
}) {
435+
const { clientId, from, to } = args;
436+
const { user, isLoggedIn } = useLoggedInUser();
437+
438+
return useQuery({
439+
queryKey: accountKeys.userOpStats(
440+
user?.address as string,
441+
clientId as string,
442+
from?.toISOString() || "",
443+
to?.toISOString() || "",
444+
"all",
445+
),
446+
queryFn: async () => {
447+
return getUserOpUsage({
448+
clientId,
449+
from,
450+
to,
451+
period: "all",
452+
});
453+
},
454+
enabled: !!clientId && !!user?.address && isLoggedIn,
455+
});
456+
}
457+
458+
export function useUserOpUsagePeriod(args: {
459+
clientId: string;
460+
from?: Date;
461+
to?: Date;
462+
period: "day" | "week" | "month" | "year";
463+
}) {
464+
const { clientId, from, to, period } = args;
465+
const { user, isLoggedIn } = useLoggedInUser();
466+
467+
return useQuery({
468+
queryKey: accountKeys.userOpStats(
469+
user?.address as string,
470+
clientId as string,
471+
from?.toISOString() || "",
472+
to?.toISOString() || "",
473+
period,
474+
),
475+
queryFn: async () => {
476+
return getUserOpUsage({
477+
clientId,
478+
from,
479+
to,
480+
period,
481+
});
482+
},
483+
enabled: !!clientId && !!user?.address && isLoggedIn,
484+
});
485+
}
486+
386487
export function useWalletUsageAggregate(args: {
387488
clientId: string;
388489
from?: Date;

apps/dashboard/src/app/team/[team_slug]/[project_slug]/connect/analytics/ConnectAnalyticsDashboard.tsx

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,8 @@ import {
88
SelectValue,
99
} from "@/components/ui/select";
1010
import {
11+
useUserOpUsageAggregate,
12+
useUserOpUsagePeriod,
1113
useWalletUsageAggregate,
1214
useWalletUsagePeriod,
1315
} from "@3rdweb-sdk/react/hooks/useApi";
@@ -42,6 +44,19 @@ export function ConnectAnalyticsDashboard(props: {
4244
to: range.to,
4345
});
4446

47+
const userOpUsageQuery = useUserOpUsagePeriod({
48+
clientId: props.clientId,
49+
from: range.from,
50+
to: range.to,
51+
period: intervalType,
52+
});
53+
54+
const userOpUsageAggregateQuery = useUserOpUsageAggregate({
55+
clientId: props.clientId,
56+
from: range.from,
57+
to: range.to,
58+
});
59+
4560
return (
4661
<div>
4762
<div className="flex gap-3">
@@ -62,6 +77,8 @@ export function ConnectAnalyticsDashboard(props: {
6277
<ConnectAnalyticsDashboardUI
6378
walletUsage={walletUsageQuery.data || []}
6479
aggregateWalletUsage={walletUsageAggregateQuery.data || []}
80+
userOpUsage={userOpUsageQuery.data || []}
81+
aggregateUserOpUsage={userOpUsageAggregateQuery.data || []}
6582
isPending={
6683
walletUsageQuery.isPending || walletUsageAggregateQuery.isPending
6784
}

apps/dashboard/src/app/team/[team_slug]/[project_slug]/connect/analytics/ConnectAnalyticsDashboardUI.tsx

Lines changed: 56 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,23 @@
1-
import type { WalletStats } from "@3rdweb-sdk/react/hooks/useApi";
2-
import { CableIcon, WalletCardsIcon } from "lucide-react";
1+
import type { UserOpStats, WalletStats } from "@3rdweb-sdk/react/hooks/useApi";
2+
import {
3+
ActivityIcon,
4+
CableIcon,
5+
CoinsIcon,
6+
WalletCardsIcon,
7+
} from "lucide-react";
38
import type React from "react";
49
import { useMemo } from "react";
510
import { DailyConnectionsChartCard } from "./_components/DailyConnectionsChartCard";
11+
import { SponsoredTransactionsChartCard } from "./_components/SponsoredTransactionsChartCard";
12+
import { TotalSponsoredChartCard } from "./_components/TotalSponsoredChartCard";
613
import { WalletConnectorsChartCard } from "./_components/WalletConnectorsChartCard";
714
import { WalletDistributionChartCard } from "./_components/WalletDistributionChartCard";
815

916
export function ConnectAnalyticsDashboardUI(props: {
1017
walletUsage: WalletStats[];
1118
aggregateWalletUsage: WalletStats[];
19+
userOpUsage: UserOpStats[];
20+
aggregateUserOpUsage: UserOpStats[];
1221
isPending: boolean;
1322
}) {
1423
const { totalWallets, uniqueWallets } = useMemo(() => {
@@ -22,9 +31,20 @@ export function ConnectAnalyticsDashboardUI(props: {
2231
);
2332
}, [props.aggregateWalletUsage]);
2433

34+
const { totalSponsoredTransactions, totalSponsoredUsd } = useMemo(() => {
35+
return props.aggregateUserOpUsage.reduce(
36+
(acc, curr) => {
37+
acc.totalSponsoredTransactions += curr.successful;
38+
acc.totalSponsoredUsd += curr.sponsoredUsd;
39+
return acc;
40+
},
41+
{ totalSponsoredTransactions: 0, totalSponsoredUsd: 0 },
42+
);
43+
}, [props.aggregateUserOpUsage]);
44+
2545
return (
2646
<div className="flex flex-col gap-4 lg:gap-6">
27-
{/* Summary Stat Cards */}
47+
{/* Connections */}
2848
<div className="grid grid-cols-2 gap-4 lg:gap-6">
2949
<Stat label="Connections" value={totalWallets} icon={CableIcon} />
3050
<Stat
@@ -48,6 +68,36 @@ export function ConnectAnalyticsDashboardUI(props: {
4868
walletStats={props.walletUsage}
4969
isPending={props.isPending}
5070
/>
71+
72+
{/* Connections */}
73+
<div className="grid grid-cols-2 gap-4 lg:gap-6">
74+
<Stat
75+
label="Sponsored Transactions"
76+
value={totalSponsoredTransactions}
77+
icon={ActivityIcon}
78+
/>
79+
<Stat
80+
label="Total Sponsored"
81+
value={totalSponsoredUsd}
82+
formatter={(value) =>
83+
new Intl.NumberFormat("en-US", {
84+
style: "currency",
85+
currency: "USD",
86+
}).format(value)
87+
}
88+
icon={CoinsIcon}
89+
/>
90+
</div>
91+
92+
<TotalSponsoredChartCard
93+
userOpStats={props.userOpUsage}
94+
isPending={props.isPending}
95+
/>
96+
97+
<SponsoredTransactionsChartCard
98+
userOpStats={props.userOpUsage}
99+
isPending={props.isPending}
100+
/>
51101
</div>
52102
);
53103
}
@@ -56,12 +106,13 @@ const Stat: React.FC<{
56106
label: string;
57107
value?: number;
58108
icon: React.FC<{ className?: string }>;
59-
}> = ({ label, value, icon: Icon }) => {
109+
formatter?: (value: number) => string;
110+
}> = ({ label, value, formatter, icon: Icon }) => {
60111
return (
61112
<dl className="flex items-center justify-between gap-4 rounded-lg border border-border bg-muted/50 p-4 lg:p-6">
62113
<div>
63114
<dd className="font-semibold text-3xl tracking-tight lg:text-5xl">
64-
{value?.toLocaleString()}
115+
{value && formatter ? formatter(value) : value?.toLocaleString()}
65116
</dd>
66117
<dt className="font-medium text-muted-foreground text-sm tracking-tight lg:text-lg">
67118
{label}

apps/dashboard/src/app/team/[team_slug]/[project_slug]/connect/analytics/_components/ConnectAnalyticsDashboard.stories.tsx

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import type { Meta, StoryObj } from "@storybook/react";
22
import { mobileViewport } from "../../../../../../../stories/utils";
33
import { ConnectAnalyticsDashboardUI } from "../ConnectAnalyticsDashboardUI";
4-
import { createWalletStatsStub } from "./storyUtils";
4+
import { createUserOpStatsStub, createWalletStatsStub } from "./storyUtils";
55

66
const meta = {
77
title: "Charts/Connect/Analytics Dashboard",
@@ -31,6 +31,8 @@ function Component() {
3131
<ConnectAnalyticsDashboardUI
3232
walletUsage={createWalletStatsStub(30)}
3333
aggregateWalletUsage={createWalletStatsStub(30)}
34+
userOpUsage={createUserOpStatsStub(30)}
35+
aggregateUserOpUsage={createUserOpStatsStub(1)}
3436
isPending={false}
3537
/>
3638
</div>

apps/dashboard/src/app/team/[team_slug]/[project_slug]/connect/analytics/_components/DailyConnectionsChartCard.stories.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ import { DailyConnectionsChartCard } from "./DailyConnectionsChartCard";
77
import { createWalletStatsStub } from "./storyUtils";
88

99
const meta = {
10-
title: "Charts/Connect/DailyConnections",
10+
title: "Charts/Connect/Daily Connections",
1111
component: Component,
1212
parameters: {
1313
layout: "centered",

apps/dashboard/src/app/team/[team_slug]/[project_slug]/connect/analytics/_components/DailyConnectionsChartCard.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -117,7 +117,7 @@ export function DailyConnectionsChartCard(props: {
117117
]);
118118
return { header, rows };
119119
}}
120-
fileName="DialyConnections"
120+
fileName="DailyConnections"
121121
/>
122122
</div>
123123

Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
import type { Meta, StoryObj } from "@storybook/react";
2+
import {
3+
BadgeContainer,
4+
mobileViewport,
5+
} from "../../../../../../../stories/utils";
6+
import { SponsoredTransactionsChartCard } from "./SponsoredTransactionsChartCard";
7+
import { createUserOpStatsStub } from "./storyUtils";
8+
9+
const meta = {
10+
title: "Charts/Connect/Sponsored Transactions",
11+
component: Component,
12+
parameters: {
13+
layout: "centered",
14+
},
15+
} satisfies Meta<typeof Component>;
16+
17+
export default meta;
18+
type Story = StoryObj<typeof meta>;
19+
20+
export const Desktop: Story = {
21+
args: {},
22+
};
23+
24+
export const Mobile: Story = {
25+
args: {},
26+
parameters: {
27+
viewport: mobileViewport("iphone14"),
28+
},
29+
};
30+
31+
function Component() {
32+
return (
33+
<div className="container flex max-w-[1150px] flex-col gap-10 py-10">
34+
<BadgeContainer label="30 days">
35+
<SponsoredTransactionsChartCard
36+
userOpStats={createUserOpStatsStub(30)}
37+
isPending={false}
38+
/>
39+
</BadgeContainer>
40+
41+
<BadgeContainer label="60 days">
42+
<SponsoredTransactionsChartCard
43+
userOpStats={createUserOpStatsStub(60)}
44+
isPending={false}
45+
/>
46+
</BadgeContainer>
47+
48+
<BadgeContainer label="10 days">
49+
<SponsoredTransactionsChartCard
50+
userOpStats={createUserOpStatsStub(10)}
51+
isPending={false}
52+
/>
53+
</BadgeContainer>
54+
55+
<BadgeContainer label="0 days">
56+
<SponsoredTransactionsChartCard
57+
userOpStats={createUserOpStatsStub(0)}
58+
isPending={false}
59+
/>
60+
</BadgeContainer>
61+
62+
<BadgeContainer label="Loading">
63+
<SponsoredTransactionsChartCard
64+
userOpStats={createUserOpStatsStub(0)}
65+
isPending={true}
66+
/>
67+
</BadgeContainer>
68+
</div>
69+
);
70+
}

0 commit comments

Comments
 (0)