From 14cdb6d78bbcab095ec5f2e9ee279691660ad7c3 Mon Sep 17 00:00:00 2001 From: Joaquim Verges Date: Thu, 23 Jan 2025 11:59:15 +1300 Subject: [PATCH] [Dashboard] perf: Optimize chain and contract data processing --- .../(team)/_components/TransactionsCard.tsx | 10 +- .../[team_slug]/(team)/~/analytics/page.tsx | 2 +- .../Transactions/TransactionCharts.tsx | 119 +++++++++++++++ .../components/Transactions/index.tsx | 50 ++++++ .../team/[team_slug]/[project_slug]/page.tsx | 142 ++---------------- apps/dashboard/src/types/analytics.ts | 2 - 6 files changed, 186 insertions(+), 139 deletions(-) create mode 100644 apps/dashboard/src/app/team/[team_slug]/[project_slug]/components/Transactions/TransactionCharts.tsx create mode 100644 apps/dashboard/src/app/team/[team_slug]/[project_slug]/components/Transactions/index.tsx diff --git a/apps/dashboard/src/app/team/[team_slug]/(team)/_components/TransactionsCard.tsx b/apps/dashboard/src/app/team/[team_slug]/(team)/_components/TransactionsCard.tsx index 19c123c3a62..091c1e751b7 100644 --- a/apps/dashboard/src/app/team/[team_slug]/(team)/_components/TransactionsCard.tsx +++ b/apps/dashboard/src/app/team/[team_slug]/(team)/_components/TransactionsCard.tsx @@ -23,11 +23,13 @@ export async function TransactionsChartCardUI({ title?: string; description?: string; }) { + const uniqueChainIds = [ + ...new Set(data.map((item) => item.chainId).filter(Boolean)), + ]; const chains = await Promise.all( - data.map( - (item) => - // eslint-disable-next-line no-restricted-syntax - item.chainId && getChainMetadata(defineChain(Number(item.chainId))), + uniqueChainIds.map((chainId) => + // eslint-disable-next-line no-restricted-syntax + getChainMetadata(defineChain(Number(chainId))).catch(() => undefined), ), ).then((chains) => chains.filter((c) => c) as ChainMetadata[]); diff --git a/apps/dashboard/src/app/team/[team_slug]/(team)/~/analytics/page.tsx b/apps/dashboard/src/app/team/[team_slug]/(team)/~/analytics/page.tsx index 8b89ded9bbb..510a54ac7bf 100644 --- a/apps/dashboard/src/app/team/[team_slug]/(team)/~/analytics/page.tsx +++ b/apps/dashboard/src/app/team/[team_slug]/(team)/~/analytics/page.tsx @@ -35,7 +35,7 @@ import { TotalSponsoredChartCardUI } from "../../_components/TotalSponsoredCard" import { TransactionsChartCardUI } from "../../_components/TransactionsCard"; // revalidate every 5 minutes -export const revalidate = 300; +export const maxDuration = 300; type SearchParams = { usersChart?: string; diff --git a/apps/dashboard/src/app/team/[team_slug]/[project_slug]/components/Transactions/TransactionCharts.tsx b/apps/dashboard/src/app/team/[team_slug]/[project_slug]/components/Transactions/TransactionCharts.tsx new file mode 100644 index 00000000000..03772b6d9e6 --- /dev/null +++ b/apps/dashboard/src/app/team/[team_slug]/[project_slug]/components/Transactions/TransactionCharts.tsx @@ -0,0 +1,119 @@ +import { defineChain, getContract } from "thirdweb"; +import { getChainMetadata } from "thirdweb/chains"; +import { TransactionsChartCardUI } from "../../../(team)/_components/TransactionsCard"; +import { getThirdwebClient } from "../../../../../../@/constants/thirdweb.server"; +import { fetchDashboardContractMetadata } from "../../../../../../@3rdweb-sdk/react/hooks/useDashboardContractMetadata"; +import type { TransactionStats } from "../../../../../../types/analytics"; +import { PieChartCard } from "../../../../components/Analytics/PieChartCard"; + +export function TransactionsChartsUI({ + data, + aggregatedData, + searchParams, +}: { + data: TransactionStats[]; + aggregatedData: TransactionStats[]; + searchParams: { [key: string]: string | string[] | undefined }; +}) { + return ( + <> + +
+ + +
+ + ); +} + +async function ChainDistributionCard({ data }: { data: TransactionStats[] }) { + const reducedData = await Promise.all( + Object.entries( + data.reduce( + (acc, curr) => { + acc[curr.chainId] = (acc[curr.chainId] || 0) + curr.count; + return acc; + }, + {} as Record, + ), + ) + .sort((a, b) => b[1] - a[1]) + .slice(0, 10) // only top ten + .map(async ([key, value]) => { + // eslint-disable-next-line no-restricted-syntax + const chain = defineChain(Number(key)); + const chainMeta = await getChainMetadata(chain).catch(() => undefined); + return { + label: chainMeta?.slug || chain.id.toString(), + value, + link: `/${chain.id}`, + }; + }), + ); + + const aggregateFn = () => new Set(data.map((d) => `${d.chainId}`)).size; + + return ( + + ); +} + +async function ContractDistributionCard({ + data, +}: { data: TransactionStats[] }) { + const reducedData = await Promise.all( + Object.entries( + data + .filter((d) => d.contractAddress) + .reduce( + (acc, curr) => { + acc[`${curr.chainId}:${curr.contractAddress}`] = + (acc[`${curr.chainId}:${curr.contractAddress}`] || 0) + + curr.count; + return acc; + }, + {} as Record, + ), + ) + .sort((a, b) => b[1] - a[1]) + .slice(0, 10) // only top ten + .map(async ([key, value]) => { + const [chainId, contractAddress] = key.split(":"); + // eslint-disable-next-line no-restricted-syntax + const chain = defineChain(Number(chainId)); + const chainMeta = await getChainMetadata(chain).catch(() => undefined); + const contractData = await fetchDashboardContractMetadata( + getContract({ + chain, + address: contractAddress as string, // we filter above + client: getThirdwebClient(), + }), + ).catch(() => undefined); + return { + label: `${contractData?.name} (${chainMeta?.slug || chainId})`, + link: `/${chainId}/${contractAddress}`, + value, + }; + }), + ); + + const aggregateFn = () => + new Set( + data + .filter((d) => d.contractAddress) + .map((d) => `${d.chainId}:${d.contractAddress}`), + ).size; + + return ( + + ); +} diff --git a/apps/dashboard/src/app/team/[team_slug]/[project_slug]/components/Transactions/index.tsx b/apps/dashboard/src/app/team/[team_slug]/[project_slug]/components/Transactions/index.tsx new file mode 100644 index 00000000000..bd6d0379ecd --- /dev/null +++ b/apps/dashboard/src/app/team/[team_slug]/[project_slug]/components/Transactions/index.tsx @@ -0,0 +1,50 @@ +import { LoadingChartState } from "components/analytics/empty-chart-state"; +import { Suspense } from "react"; +import type { AnalyticsQueryParams } from "types/analytics"; +import { getClientTransactions } from "../../../../../../@/api/analytics"; +import { TransactionsChartsUI } from "./TransactionCharts"; + +export function TransactionsCharts( + props: AnalyticsQueryParams & { + searchParams: { [key: string]: string | string[] | undefined }; + }, +) { + return ( + // TODO: Add better LoadingChartState + + + + } + > + + + ); +} + +async function TransactionsChartCardAsync( + props: AnalyticsQueryParams & { + searchParams: { [key: string]: string | string[] | undefined }; + }, +) { + const [data, aggregatedData] = await Promise.all([ + getClientTransactions(props), + getClientTransactions({ + ...props, + period: "all", + }), + ]); + + if (!aggregatedData.length) { + return null; + } + + return ( + + ); +} diff --git a/apps/dashboard/src/app/team/[team_slug]/[project_slug]/page.tsx b/apps/dashboard/src/app/team/[team_slug]/[project_slug]/page.tsx index 432c1f15587..77614ed0c28 100644 --- a/apps/dashboard/src/app/team/[team_slug]/[project_slug]/page.tsx +++ b/apps/dashboard/src/app/team/[team_slug]/[project_slug]/page.tsx @@ -9,14 +9,12 @@ import { } from "components/analytics/date-range-selector"; import type { InAppWalletStats, - TransactionStats, UserOpStats, WalletStats, WalletUserStats, } from "types/analytics"; import { - getClientTransactions, getInAppWalletUsage, getUserOpUsage, getWalletConnections, @@ -25,22 +23,18 @@ import { } from "@/api/analytics"; import { EmptyStateCard } from "app/team/components/Analytics/EmptyStateCard"; import { Suspense } from "react"; -import { getContract } from "thirdweb"; import { type ChainMetadata, defineChain, getChainMetadata, } from "thirdweb/chains"; -import { shortenAddress } from "thirdweb/utils"; import { type WalletId, getWalletInfo } from "thirdweb/wallets"; -import { TransactionsChartCardUI } from "../(team)/_components/TransactionsCard"; -import { getThirdwebClient } from "../../../../@/constants/thirdweb.server"; -import { fetchDashboardContractMetadata } from "../../../../@3rdweb-sdk/react/hooks/useDashboardContractMetadata"; import { AnalyticsHeader } from "../../components/Analytics/AnalyticsHeader"; import { CombinedBarChartCard } from "../../components/Analytics/CombinedBarChartCard"; import { EmptyState } from "../../components/Analytics/EmptyState"; import { PieChartCard } from "../../components/Analytics/PieChartCard"; import { RpcMethodBarChartCard } from "./components/RpcMethodBarChartCard"; +import { TransactionsCharts } from "./components/Transactions"; interface PageParams { team_slug: string; @@ -56,6 +50,8 @@ interface PageProps { searchParams: Promise; } +export const maxDuration = 300; + export default async function ProjectOverviewPage(props: PageProps) { const [params, searchParams] = await Promise.all([ props.params, @@ -121,8 +117,6 @@ async function ProjectAnalytics(props: { inAppWalletUsage, userOpUsageTimeSeries, userOpUsage, - clientTransactionsTimeSeries, - clientTransactions, ] = await Promise.allSettled([ // Aggregated wallet connections getWalletConnections({ @@ -158,19 +152,6 @@ async function ProjectAnalytics(props: { to: range.to, period: "all", }), - // Client transactions - getClientTransactions({ - clientId: project.publishableKey, - from: range.from, - to: range.to, - period: interval, - }), - getClientTransactions({ - clientId: project.publishableKey, - from: range.from, - to: range.to, - period: "all", - }), ]); return ( @@ -216,22 +197,13 @@ async function ProjectAnalytics(props: { /> )} - {clientTransactionsTimeSeries.status === "fulfilled" && - clientTransactions.status === "fulfilled" && - clientTransactions.value.length > 0 && ( - <> - -
- - -
- - )} + {userOpUsageTimeSeries.status === "fulfilled" && userOpUsage.status === "fulfilled" && userOpUsage.value.length > 0 ? ( @@ -382,100 +354,6 @@ function AuthMethodDistributionCard({ data }: { data: InAppWalletStats[] }) { ); } -async function ChainDistributionCard({ data }: { data: TransactionStats[] }) { - const formattedData = await Promise.all( - data.map(async (w) => { - // eslint-disable-next-line no-restricted-syntax - const chain = await getChainMetadata(defineChain(Number(w.chainId))); - return { - chainId: w.chainId, - count: w.count, - chainName: chain?.slug, - }; - }), - ); - const reducedData = Object.entries( - formattedData.reduce( - (acc, curr) => { - acc[curr.chainName] = (acc[curr.chainName] || 0) + curr.count; - return acc; - }, - {} as Record, - ), - ).map(([key, value]) => ({ - label: key, - value, - })); - - const aggregateFn = () => reducedData.length; - - return ( - - ); -} - -async function ContractDistributionCard({ - data, -}: { data: TransactionStats[] }) { - const formattedData = ( - await Promise.all( - data.map(async (w) => { - // eslint-disable-next-line no-restricted-syntax - const chain = defineChain(Number(w.chainId)); - const chainMeta = await getChainMetadata(chain); - if (!w.contractAddress) { - return null; - } - const contractData = await fetchDashboardContractMetadata( - getContract({ - chain, - address: w.contractAddress, - client: getThirdwebClient(), - }), - ).catch(() => undefined); - return { - chainId: w.chainId, - count: w.count, - chainName: chainMeta?.slug || w.chainId.toString(), - contractAddress: w.contractAddress, - contractLabel: - contractData?.name || shortenAddress(w.contractAddress), - }; - }), - ) - ).filter((d) => d !== null); - - const reducedData = Object.entries( - formattedData.reduce( - (acc, curr) => { - acc[`${curr.chainName}:${curr.contractAddress}:${curr.contractLabel}`] = - (acc[ - `${curr.chainName}:${curr.contractAddress}:${curr.contractLabel}` - ] || 0) + curr.count; - return acc; - }, - {} as Record, - ), - ).map(([key, value]) => { - const [chainName, contractAddress, contractLabel] = key.split(":"); - return { - label: `${contractLabel} (${chainName})`, - link: `/${chainName}/${contractAddress}`, - value, - }; - }); - - const aggregateFn = () => reducedData.length; - - return ( - - ); -} - async function TotalSponsoredCard({ data, aggregatedData, diff --git a/apps/dashboard/src/types/analytics.ts b/apps/dashboard/src/types/analytics.ts index 772da2515bf..c198f910c5e 100644 --- a/apps/dashboard/src/types/analytics.ts +++ b/apps/dashboard/src/types/analytics.ts @@ -32,8 +32,6 @@ export interface TransactionStats { date: string; chainId: number; contractAddress?: string; - walletType?: string; - walletAddress?: string; count: number; }