Skip to content

Commit 458246f

Browse files
committed
Dashboard: Project analytics code organization refactor (#8487)
<!-- ## title your PR with this format: "[SDK/Dashboard/Portal] Feature/Fix: Concise title for the changes" If you did not copy the branch name from Linear, paste the issue tag here (format is TEAM-0000): ## Notes for the reviewer Anything important to call out? Be sure to also clarify these in your comments. ## How to test Unit tests, playground, etc. --> <!-- start pr-codex --> --- ## PR-Codex overview This PR introduces several analytics components for a dashboard application, enhancing the project's data visualization capabilities. It includes new types for page parameters and search parameters, as well as multiple asynchronous chart components for different metrics. ### Detailed summary - Added `PageParams` and `PageSearchParams` types for handling routing parameters. - Implemented `AsyncRPCRequestsChartCard` and `RPCRequestsChartCard` for RPC usage analytics. - Created `AsyncAiAnalytics` and `AIAnalyticsChartCard` for AI usage analytics. - Introduced `AsyncIndexerRequestsChartCard` and `IndexerRequestsChartCard` for indexer request analytics. - Developed `AsyncX402RequestsChart` and `X402RequestsChartCard` for x402 settlements analytics. - Added `AsyncAppHighlightsCard` and `ProjectHighlightCard` for displaying project highlights. - Refactored `ProjectAnalytics` function to integrate new chart components. - Removed redundant code and improved structure for better maintainability. > ✨ Ask PR-Codex anything about this PR by commenting with `/codex {your question}` <!-- end pr-codex --> <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit ## Release Notes * **New Features** * Added AI token volume analytics chart to project dashboard * Added indexer/gateway request analytics chart * Added RPC usage analytics chart * Added X402 settlements analytics chart * Introduced project highlights card displaying wallet and bridge usage metrics * **Refactor** * Reorganized project analytics dashboard structure for improved layout and user experience <sub>✏️ Tip: You can customize this high-level summary in your review settings.</sub> <!-- end of auto-generated comment: release notes by coderabbit.ai -->
1 parent 1b3e938 commit 458246f

File tree

11 files changed

+598
-471
lines changed

11 files changed

+598
-471
lines changed
Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
import { ResponsiveSuspense } from "responsive-rsc";
2+
import { getAiUsage } from "@/api/analytics";
3+
import { AiTokenUsageChartCardUI } from "../ai/analytics/chart/AiTokenUsageChartCard";
4+
5+
async function AsyncAiAnalytics(props: {
6+
from: Date;
7+
to: Date;
8+
interval: "day" | "week";
9+
projectId: string;
10+
teamId: string;
11+
authToken: string;
12+
teamSlug: string;
13+
projectSlug: string;
14+
}) {
15+
const stats = await getAiUsage(
16+
{
17+
from: props.from,
18+
period: props.interval,
19+
projectId: props.projectId,
20+
teamId: props.teamId,
21+
to: props.to,
22+
},
23+
props.authToken,
24+
).catch((error) => {
25+
console.error(error);
26+
return [];
27+
});
28+
29+
return (
30+
<AiTokenUsageChartCardUI
31+
title="AI token volume"
32+
isPending={false}
33+
aiUsageStats={stats}
34+
viewMoreLink={`/team/${props.teamSlug}/${props.projectSlug}/ai/analytics`}
35+
/>
36+
);
37+
}
38+
39+
export function AIAnalyticsChartCard(props: {
40+
from: Date;
41+
to: Date;
42+
interval: "day" | "week";
43+
projectId: string;
44+
teamId: string;
45+
authToken: string;
46+
teamSlug: string;
47+
projectSlug: string;
48+
}) {
49+
return (
50+
<ResponsiveSuspense
51+
fallback={
52+
<AiTokenUsageChartCardUI
53+
isPending={true}
54+
title="AI token volume"
55+
viewMoreLink={`/team/${props.teamSlug}/${props.projectSlug}/ai/analytics`}
56+
aiUsageStats={[]}
57+
/>
58+
}
59+
searchParamsUsed={["from", "to", "interval"]}
60+
>
61+
<AsyncAiAnalytics
62+
teamSlug={props.teamSlug}
63+
projectSlug={props.projectSlug}
64+
from={props.from}
65+
to={props.to}
66+
interval={props.interval}
67+
projectId={props.projectId}
68+
teamId={props.teamId}
69+
authToken={props.authToken}
70+
/>
71+
</ResponsiveSuspense>
72+
);
73+
}
Lines changed: 122 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,122 @@
1+
import { EmptyStateCard } from "app/(app)/team/components/Analytics/EmptyStateCard";
2+
import { ResponsiveSuspense } from "responsive-rsc";
3+
import type { ThirdwebClient } from "thirdweb";
4+
import { getInAppWalletUsage, getUniversalBridgeUsage } from "@/api/analytics";
5+
import type { Project } from "@/api/project/projects";
6+
import type { Range } from "@/components/analytics/date-range-selector";
7+
import { LoadingChartState } from "@/components/analytics/empty-chart-state";
8+
import { ProjectHighlightsCard } from "./highlights-card-ui";
9+
import type { PageParams, PageSearchParams } from "./types";
10+
11+
export function ProjectHighlightCard(props: {
12+
authToken: string;
13+
client: ThirdwebClient;
14+
interval: "day" | "week";
15+
params: PageParams;
16+
range: Range;
17+
searchParams: PageSearchParams;
18+
project: Project;
19+
}) {
20+
return (
21+
<ResponsiveSuspense
22+
fallback={<LoadingChartState className="h-[458px] border" />}
23+
searchParamsUsed={["from", "to", "interval", "appHighlights"]}
24+
>
25+
<AsyncAppHighlightsCard
26+
authToken={props.authToken}
27+
client={props.client}
28+
interval={props.interval}
29+
params={props.params}
30+
project={props.project}
31+
range={props.range}
32+
selectedChart={
33+
typeof props.searchParams.appHighlights === "string"
34+
? props.searchParams.appHighlights
35+
: undefined
36+
}
37+
selectedChartQueryParam="appHighlights"
38+
/>
39+
</ResponsiveSuspense>
40+
);
41+
}
42+
43+
async function AsyncAppHighlightsCard(props: {
44+
project: Project;
45+
range: Range;
46+
interval: "day" | "week";
47+
selectedChartQueryParam: string;
48+
selectedChart: string | undefined;
49+
client: ThirdwebClient;
50+
params: PageParams;
51+
authToken: string;
52+
}) {
53+
const [aggregatedUserStats, walletUserStatsTimeSeries, universalBridgeUsage] =
54+
await Promise.allSettled([
55+
getInAppWalletUsage(
56+
{
57+
from: props.range.from,
58+
period: "all",
59+
projectId: props.project.id,
60+
teamId: props.project.teamId,
61+
to: props.range.to,
62+
},
63+
props.authToken,
64+
),
65+
getInAppWalletUsage(
66+
{
67+
from: props.range.from,
68+
period: props.interval,
69+
projectId: props.project.id,
70+
teamId: props.project.teamId,
71+
to: props.range.to,
72+
},
73+
props.authToken,
74+
),
75+
getUniversalBridgeUsage(
76+
{
77+
from: props.range.from,
78+
period: props.interval,
79+
projectId: props.project.id,
80+
teamId: props.project.teamId,
81+
to: props.range.to,
82+
},
83+
props.authToken,
84+
),
85+
]);
86+
87+
if (
88+
walletUserStatsTimeSeries.status === "fulfilled" &&
89+
universalBridgeUsage.status === "fulfilled"
90+
) {
91+
return (
92+
<ProjectHighlightsCard
93+
aggregatedUserStats={
94+
aggregatedUserStats.status === "fulfilled"
95+
? aggregatedUserStats.value
96+
: []
97+
}
98+
selectedChart={props.selectedChart}
99+
selectedChartQueryParam={props.selectedChartQueryParam}
100+
teamSlug={props.params.team_slug}
101+
projectSlug={props.params.project_slug}
102+
userStats={
103+
walletUserStatsTimeSeries.status === "fulfilled"
104+
? walletUserStatsTimeSeries.value
105+
: []
106+
}
107+
volumeStats={
108+
universalBridgeUsage.status === "fulfilled"
109+
? universalBridgeUsage.value
110+
: []
111+
}
112+
/>
113+
);
114+
}
115+
116+
return (
117+
<EmptyStateCard
118+
link="https://portal.thirdweb.com/wallets"
119+
metric="Wallets"
120+
/>
121+
);
122+
}
Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
import { ResponsiveSuspense } from "responsive-rsc";
2+
import { getInsightStatusCodeUsage } from "@/api/analytics";
3+
import { RequestsByStatusGraph } from "../gateway/indexer/components/RequestsByStatusGraph";
4+
5+
export function IndexerRequestsChartCard(props: {
6+
from: Date;
7+
to: Date;
8+
interval: "day" | "week";
9+
projectId: string;
10+
teamId: string;
11+
authToken: string;
12+
teamSlug: string;
13+
projectSlug: string;
14+
}) {
15+
return (
16+
<ResponsiveSuspense
17+
fallback={
18+
<RequestsByStatusGraph
19+
data={[]}
20+
isPending={true}
21+
viewMoreLink={`/team/${props.teamSlug}/${props.projectSlug}/gateway/indexer`}
22+
/>
23+
}
24+
searchParamsUsed={["from", "to", "interval"]}
25+
>
26+
<AsyncIndexerRequestsChartCard
27+
teamSlug={props.teamSlug}
28+
projectSlug={props.projectSlug}
29+
from={props.from}
30+
to={props.to}
31+
interval={props.interval}
32+
projectId={props.projectId}
33+
teamId={props.teamId}
34+
authToken={props.authToken}
35+
/>
36+
</ResponsiveSuspense>
37+
);
38+
}
39+
40+
async function AsyncIndexerRequestsChartCard(props: {
41+
from: Date;
42+
to: Date;
43+
interval: "day" | "week";
44+
projectId: string;
45+
teamId: string;
46+
authToken: string;
47+
teamSlug: string;
48+
projectSlug: string;
49+
}) {
50+
const requestsData = await getInsightStatusCodeUsage(
51+
{
52+
from: props.from,
53+
period: props.interval,
54+
projectId: props.projectId,
55+
teamId: props.teamId,
56+
to: props.to,
57+
},
58+
props.authToken,
59+
).catch(() => undefined);
60+
61+
return (
62+
<RequestsByStatusGraph
63+
data={requestsData && "data" in requestsData ? requestsData.data : []}
64+
isPending={false}
65+
viewMoreLink={`/team/${props.teamSlug}/${props.projectSlug}/gateway/indexer`}
66+
/>
67+
);
68+
}
Lines changed: 129 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,129 @@
1+
import { ResponsiveSuspense } from "responsive-rsc";
2+
import type { ThirdwebClient } from "thirdweb";
3+
import type { Project } from "@/api/project/projects";
4+
import type { Range } from "@/components/analytics/date-range-selector";
5+
import { LoadingChartState } from "@/components/analytics/empty-chart-state";
6+
import { TransactionsChartCardAsync } from "../components/Transactions";
7+
import { AIAnalyticsChartCard } from "./ai-card";
8+
import { AllWalletConnectionsChart } from "./all-wallet-connections-chart";
9+
import { ProjectHighlightCard } from "./highlights-card";
10+
import { IndexerRequestsChartCard } from "./indexer-card";
11+
import { RPCRequestsChartCard } from "./rpc-card";
12+
import type { PageParams, PageSearchParams } from "./types";
13+
import { X402RequestsChartCard } from "./x402-card";
14+
15+
export async function ProjectAnalytics(props: {
16+
project: Project;
17+
params: PageParams;
18+
range: Range;
19+
interval: "day" | "week";
20+
searchParams: PageSearchParams;
21+
client: ThirdwebClient;
22+
authToken: string;
23+
}) {
24+
const { project, params, range, interval, searchParams, client, authToken } =
25+
props;
26+
27+
return (
28+
<div className="flex grow flex-col gap-6">
29+
{/* highlights */}
30+
<ProjectHighlightCard
31+
authToken={authToken}
32+
client={client}
33+
interval={interval}
34+
params={params}
35+
project={project}
36+
range={range}
37+
searchParams={searchParams}
38+
/>
39+
40+
{/* wallets */}
41+
<AllWalletConnectionsChart
42+
teamSlug={params.team_slug}
43+
projectSlug={params.project_slug}
44+
authToken={authToken}
45+
projectId={project.id}
46+
from={range.from}
47+
to={range.to}
48+
interval={interval}
49+
teamId={project.teamId}
50+
/>
51+
52+
<div className="grid gap-6 md:grid-cols-2">
53+
{/* Indexer */}
54+
<IndexerRequestsChartCard
55+
teamSlug={params.team_slug}
56+
projectSlug={params.project_slug}
57+
from={range.from}
58+
to={range.to}
59+
interval={interval}
60+
projectId={project.id}
61+
teamId={project.teamId}
62+
authToken={authToken}
63+
/>
64+
65+
{/* RPC */}
66+
<RPCRequestsChartCard
67+
teamSlug={params.team_slug}
68+
projectSlug={params.project_slug}
69+
from={range.from}
70+
to={range.to}
71+
interval={interval}
72+
projectId={project.id}
73+
teamId={project.teamId}
74+
authToken={authToken}
75+
/>
76+
</div>
77+
78+
<div className="grid gap-6 md:grid-cols-2">
79+
{/* x402 */}
80+
<X402RequestsChartCard
81+
teamSlug={params.team_slug}
82+
projectSlug={params.project_slug}
83+
from={range.from}
84+
to={range.to}
85+
interval={interval}
86+
projectId={project.id}
87+
teamId={project.teamId}
88+
authToken={authToken}
89+
/>
90+
91+
{/* AI */}
92+
<AIAnalyticsChartCard
93+
teamSlug={params.team_slug}
94+
projectSlug={params.project_slug}
95+
from={range.from}
96+
to={range.to}
97+
interval={interval}
98+
projectId={project.id}
99+
teamId={project.teamId}
100+
authToken={authToken}
101+
/>
102+
</div>
103+
104+
{/* Transactions, Chains, Contracts */}
105+
<ResponsiveSuspense
106+
fallback={<LoadingChartState className="h-[458px] border" />}
107+
searchParamsUsed={["from", "to", "interval", "client_transactions"]}
108+
>
109+
<TransactionsChartCardAsync
110+
client={client}
111+
params={{
112+
from: range.from,
113+
period: interval,
114+
projectId: project.id,
115+
teamId: project.teamId,
116+
to: range.to,
117+
}}
118+
authToken={authToken}
119+
selectedChart={
120+
typeof searchParams.client_transactions === "string"
121+
? searchParams.client_transactions
122+
: undefined
123+
}
124+
selectedChartQueryParam="client_transactions"
125+
/>
126+
</ResponsiveSuspense>
127+
</div>
128+
);
129+
}

0 commit comments

Comments
 (0)