From 0e630c455089080baf77d0fd9171b8e8f352df4e Mon Sep 17 00:00:00 2001 From: MananTank Date: Fri, 7 Nov 2025 20:13:34 +0000 Subject: [PATCH] [MNY-304] Move engine tx summary request to client side to fix page crash (#8379) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ## PR-Codex overview This PR focuses on refactoring the transaction analytics feature by updating data fetching methods, improving UI components, and enhancing error handling for transaction summaries and charts. ### Detailed summary - Removed outdated files: `utils.ts` and `filter.tsx`. - Updated import paths for `TransactionSummaryData`. - Changed UI text in `tx-chart-ui.tsx` for clarity. - Enhanced error handling in data fetching functions. - Introduced `getTransactionsChartData` and `getTransactionAnalyticsSummary` for improved data management. - Refactored `TransactionsAnalyticsPageContent` to streamline data flow and UI rendering. - Added new components for date and interval selection in transaction charts. > ✨ Ask PR-Codex anything about this PR by commenting with `/codex {your question}` ## Summary by CodeRabbit * **Refactor** * Analytics now load client-side with a spinner and a streamlined layout; analytics sections only show when transactions exist. * **New Features** * Direct date-range and interval controls in the analytics view. * Unified analytics summary and charts presented above the transactions table. * Chart header updated to "Transactions" with revised description. * **Chores** * Removed URL-synced filter behavior; filters are local. * Empty-chart messaging changed to "No transactions found." --- .../transactions/analytics/analytics-page.tsx | 112 +++++++++---- .../transactions/analytics/filter.tsx | 51 ------ .../transactions/analytics/summary.tsx | 2 +- .../transactions/analytics/tx-chart/data.ts | 75 +++++++++ .../analytics/tx-chart/tx-chart-ui.tsx | 12 +- .../analytics/tx-chart/tx-chart.tsx | 101 ++++++------ .../lib/analytics-summary.client.tsx | 75 +++++++++ .../(sidebar)/transactions/lib/analytics.ts | 147 ------------------ .../(sidebar)/transactions/lib/utils.ts | 14 -- .../(sidebar)/transactions/page.tsx | 50 ++---- 10 files changed, 298 insertions(+), 341 deletions(-) delete mode 100644 apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/(sidebar)/transactions/analytics/filter.tsx create mode 100644 apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/(sidebar)/transactions/analytics/tx-chart/data.ts create mode 100644 apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/(sidebar)/transactions/lib/analytics-summary.client.tsx delete mode 100644 apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/(sidebar)/transactions/lib/utils.ts diff --git a/apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/(sidebar)/transactions/analytics/analytics-page.tsx b/apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/(sidebar)/transactions/analytics/analytics-page.tsx index 4a558e9ff16..d239fc61dba 100644 --- a/apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/(sidebar)/transactions/analytics/analytics-page.tsx +++ b/apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/(sidebar)/transactions/analytics/analytics-page.tsx @@ -1,45 +1,95 @@ -import { ResponsiveSearchParamsProvider } from "responsive-rsc"; +"use client"; +import { useQuery } from "@tanstack/react-query"; +import { Spinner } from "@workspace/ui/components/spinner"; import type { ThirdwebClient } from "thirdweb"; import type { Project } from "@/api/project/projects"; import { UnifiedTransactionsTable } from "../components/transactions-table.client"; +import { getTransactionAnalyticsSummary } from "../lib/analytics-summary.client"; import type { Wallet } from "../server-wallets/wallet-table/types"; -import { TransactionAnalyticsFilter } from "./filter"; -import { TransactionsChartCard } from "./tx-chart/tx-chart"; +import type { SolanaWallet } from "../solana-wallets/wallet-table/types"; +import { EngineChecklist } from "./ftux.client"; +import { TransactionAnalyticsSummary } from "./summary"; +import { TransactionsAnalytics } from "./tx-chart/tx-chart"; export function TransactionsAnalyticsPageContent(props: { - searchParams: { - from?: string | undefined | string[]; - to?: string | undefined | string[]; - interval?: string | undefined | string[]; - }; project: Project; showAnalytics: boolean; - wallets?: Wallet[]; + wallets: Wallet[]; teamSlug: string; client: ThirdwebClient; + authToken: string; + teamId: string; + isManagedVault: boolean; + testTxWithWallet: string | undefined; + testSolanaTxWithWallet: string | undefined; + solanaWallets: SolanaWallet[]; }) { - return ( - -
- {props.showAnalytics && ( - <> -
- -
- - - )} - + const engineTxSummaryQuery = useQuery({ + queryKey: [ + "engine-tx-analytics-summary", + props.teamId, + props.project.publishableKey, + props.authToken, + ], + queryFn: async () => { + const data = await getTransactionAnalyticsSummary({ + clientId: props.project.publishableKey, + teamId: props.teamId, + authToken: props.authToken, + }); + return data; + }, + refetchOnWindowFocus: false, + refetchOnMount: false, + }); + + if (engineTxSummaryQuery.isPending) { + return ( +
+
- + ); + } + + const hasTransactions = engineTxSummaryQuery.data + ? engineTxSummaryQuery.data.totalCount > 0 + : false; + + return ( +
+ + + {props.showAnalytics && hasTransactions && ( +
+ + +
+ )} + + +
); } diff --git a/apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/(sidebar)/transactions/analytics/filter.tsx b/apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/(sidebar)/transactions/analytics/filter.tsx deleted file mode 100644 index 93f686df280..00000000000 --- a/apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/(sidebar)/transactions/analytics/filter.tsx +++ /dev/null @@ -1,51 +0,0 @@ -"use client"; - -import { - useResponsiveSearchParams, - useSetResponsiveSearchParams, -} from "responsive-rsc"; -import { DateRangeSelector } from "@/components/analytics/date-range-selector"; -import { IntervalSelector } from "@/components/analytics/interval-selector"; -import { normalizeTimeISOString } from "@/lib/time"; -import { getTxAnalyticsFiltersFromSearchParams } from "../lib/utils"; - -export function TransactionAnalyticsFilter() { - const responsiveSearchParams = useResponsiveSearchParams(); - const setResponsiveSearchParams = useSetResponsiveSearchParams(); - - const { range, interval } = getTxAnalyticsFiltersFromSearchParams({ - from: responsiveSearchParams.from, - interval: responsiveSearchParams.interval, - to: responsiveSearchParams.to, - }); - - return ( -
- { - setResponsiveSearchParams((v) => { - return { - ...v, - from: normalizeTimeISOString(newRange.from), - to: normalizeTimeISOString(newRange.to), - }; - }); - }} - /> - - { - setResponsiveSearchParams((v) => { - return { - ...v, - interval: newInterval, - }; - }); - }} - /> -
- ); -} diff --git a/apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/(sidebar)/transactions/analytics/summary.tsx b/apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/(sidebar)/transactions/analytics/summary.tsx index 811036ab06f..3ec15b077b7 100644 --- a/apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/(sidebar)/transactions/analytics/summary.tsx +++ b/apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/(sidebar)/transactions/analytics/summary.tsx @@ -1,7 +1,7 @@ import { ActivityIcon, CoinsIcon } from "lucide-react"; import { toEther } from "thirdweb/utils"; import { StatCard } from "@/components/analytics/stat"; // Assuming correct path -import type { TransactionSummaryData } from "../lib/analytics"; +import type { TransactionSummaryData } from "../lib/analytics-summary.client"; // Renders the UI based on fetched data or pending state function TransactionAnalyticsSummaryUI(props: { diff --git a/apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/(sidebar)/transactions/analytics/tx-chart/data.ts b/apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/(sidebar)/transactions/analytics/tx-chart/data.ts new file mode 100644 index 00000000000..fb534566df5 --- /dev/null +++ b/apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/(sidebar)/transactions/analytics/tx-chart/data.ts @@ -0,0 +1,75 @@ +"use client"; +import { NEXT_PUBLIC_ENGINE_CLOUD_URL } from "@/constants/public-envs"; +import type { TransactionStats } from "@/types/analytics"; + +export async function getTransactionsChartData({ + teamId, + clientId, + from, + to, + interval, + authToken, +}: { + teamId: string; + clientId: string; + from: string; + to: string; + interval: "day" | "week"; + authToken: string; +}): Promise { + const filters = { + endDate: to, + resolution: interval, + startDate: from, + }; + + const response = await fetch( + `${NEXT_PUBLIC_ENGINE_CLOUD_URL}/v1/transactions/analytics`, + { + body: JSON.stringify(filters), + headers: { + Authorization: `Bearer ${authToken}`, + "Content-Type": "application/json", + "x-client-id": clientId, + "x-team-id": teamId, + }, + method: "POST", + }, + ); + + if (!response.ok) { + if (response.status === 401 || response.status === 400) { + return []; + } + + // TODO - need to handle this error state, like we do with the connect charts + throw new Error( + `Error fetching transactions chart data: ${response.status} ${ + response.statusText + } - ${await response.text().catch(() => "Unknown error")}`, + ); + } + + type TransactionsChartResponse = { + result: { + analytics: Array<{ + timeBucket: string; + chainId: string; + count: number; + }>; + metadata: { + resolution: string; + startDate: string; + endDate: string; + }; + }; + }; + + const data = (await response.json()) as TransactionsChartResponse; + + return data.result.analytics.map((stat) => ({ + chainId: Number(stat.chainId), + count: stat.count, + date: stat.timeBucket, + })); +} diff --git a/apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/(sidebar)/transactions/analytics/tx-chart/tx-chart-ui.tsx b/apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/(sidebar)/transactions/analytics/tx-chart/tx-chart-ui.tsx index 5fd4b5e9951..1bacd8bddd0 100644 --- a/apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/(sidebar)/transactions/analytics/tx-chart/tx-chart-ui.tsx +++ b/apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/(sidebar)/transactions/analytics/tx-chart/tx-chart-ui.tsx @@ -108,10 +108,10 @@ export function TransactionsChartCardUI(props: { customHeader={

- Daily Transactions + Transactions

- Amount of daily transactions by chain. + transactions broken down by chain

@@ -194,13 +194,7 @@ function EmptyChartContent(props: {
) : ( -

- - - - - Waiting for transactions... -

+

No transactions found

)}
); diff --git a/apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/(sidebar)/transactions/analytics/tx-chart/tx-chart.tsx b/apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/(sidebar)/transactions/analytics/tx-chart/tx-chart.tsx index d4dd7b7931a..4237c3eed18 100644 --- a/apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/(sidebar)/transactions/analytics/tx-chart/tx-chart.tsx +++ b/apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/(sidebar)/transactions/analytics/tx-chart/tx-chart.tsx @@ -1,73 +1,68 @@ -import { ResponsiveSuspense } from "responsive-rsc"; +"use client"; +import { useQuery } from "@tanstack/react-query"; +import { useState } from "react"; import type { Project } from "@/api/project/projects"; -import { getTransactionsChart } from "../../lib/analytics"; -import { getTxAnalyticsFiltersFromSearchParams } from "../../lib/utils"; +import { + DateRangeSelector, + getLastNDaysRange, +} from "@/components/analytics/date-range-selector"; +import { IntervalSelector } from "@/components/analytics/interval-selector"; +import { normalizeTimeISOString } from "@/lib/time"; import type { Wallet } from "../../server-wallets/wallet-table/types"; +import { getTransactionsChartData } from "./data"; import { TransactionsChartCardUI } from "./tx-chart-ui"; -async function AsyncTransactionsChartCard(props: { - from: string; - to: string; - interval: "day" | "week"; +export function TransactionsAnalytics(props: { project: Project; wallets: Wallet[]; + authToken: string; teamSlug: string; }) { - const data = await getTransactionsChart({ + const [range, setRange] = useState(() => getLastNDaysRange("last-30")); + const [interval, setInterval] = useState<"day" | "week">("day"); + + const params = { clientId: props.project.publishableKey, - from: props.from, - interval: props.interval, + from: normalizeTimeISOString(range.from), + interval: interval, teamId: props.project.teamId, - to: props.to, + to: normalizeTimeISOString(range.to), + authToken: props.authToken, + }; + + const engineTxAnalytics = useQuery({ + queryKey: ["engine-tx-analytics", params], + queryFn: async () => { + const data = await getTransactionsChartData(params); + return data; + }, + refetchOnWindowFocus: false, + refetchOnMount: false, }); return ( - - ); -} +
+
+
+ -export function TransactionsChartCard(props: { - searchParams: { - from?: string | undefined | string[]; - to?: string | undefined | string[]; - interval?: string | undefined | string[]; - }; - project: Project; - wallets: Wallet[]; - teamSlug: string; -}) { - const { range, interval } = getTxAnalyticsFiltersFromSearchParams( - props.searchParams, - ); - - return ( - - } - searchParamsUsed={["from", "to", "interval"]} - > - +
+
+ - +
); } diff --git a/apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/(sidebar)/transactions/lib/analytics-summary.client.tsx b/apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/(sidebar)/transactions/lib/analytics-summary.client.tsx new file mode 100644 index 00000000000..b76243f7f41 --- /dev/null +++ b/apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/(sidebar)/transactions/lib/analytics-summary.client.tsx @@ -0,0 +1,75 @@ +"use client"; + +import { NEXT_PUBLIC_ENGINE_CLOUD_URL } from "@/constants/public-envs"; + +export type TransactionSummaryData = { + totalCount: number; + totalGasCostWei: string; + totalGasUnitsUsed: string; // Keep fetched data structure +}; + +type AnalyticsSummaryApiResponse = { + result: { + summary: { + totalCount: number; + totalGasCostWei: string; + totalGasUnitsUsed: string; + }; + metadata: { + startDate?: string; + endDate?: string; + }; + }; +}; + +// Fetches data from the /analytics-summary endpoint +export async function getTransactionAnalyticsSummary(props: { + teamId: string; + clientId: string; + authToken: string; +}): Promise { + const body = {}; + const defaultData: TransactionSummaryData = { + totalCount: 0, + totalGasCostWei: "0", + totalGasUnitsUsed: "0", + }; + + try { + const response = await fetch( + `${NEXT_PUBLIC_ENGINE_CLOUD_URL}/v1/transactions/analytics-summary`, + { + body: JSON.stringify(body), + headers: { + Authorization: `Bearer ${props.authToken}`, + "Content-Type": "application/json", + "x-client-id": props.clientId, + "x-team-id": props.teamId, + }, + method: "POST", + }, + ); + + if (!response.ok) { + if (response.status === 401 || response.status === 400) { + console.error("Unauthorized fetching transaction summary"); + return defaultData; + } + const errorText = await response.text(); + throw new Error( + `Error fetching transaction summary: ${response.status} ${response.statusText} - ${errorText}`, + ); + } + + const data = (await response.json()) as AnalyticsSummaryApiResponse; + + return { + totalCount: data.result.summary.totalCount ?? 0, + totalGasCostWei: data.result.summary.totalGasCostWei ?? "0", + totalGasUnitsUsed: data.result.summary.totalGasUnitsUsed ?? "0", + }; + } catch (error) { + console.error("Failed to fetch transaction summary:", error); + return defaultData; + } +} diff --git a/apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/(sidebar)/transactions/lib/analytics.ts b/apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/(sidebar)/transactions/lib/analytics.ts index c4618db2b6a..ef9104a2ef0 100644 --- a/apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/(sidebar)/transactions/lib/analytics.ts +++ b/apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/(sidebar)/transactions/lib/analytics.ts @@ -1,157 +1,10 @@ import { getAuthToken } from "@/api/auth-token"; import { NEXT_PUBLIC_ENGINE_CLOUD_URL } from "@/constants/public-envs"; -import type { TransactionStats } from "@/types/analytics"; import type { Transaction, TransactionsResponse, } from "../analytics/tx-table/types"; -// Define the structure of the data we expect back from our fetch function -export type TransactionSummaryData = { - totalCount: number; - totalGasCostWei: string; - totalGasUnitsUsed: string; // Keep fetched data structure -}; - -// Define the structure of the API response -type AnalyticsSummaryApiResponse = { - result: { - summary: { - totalCount: number; - totalGasCostWei: string; - totalGasUnitsUsed: string; - }; - metadata: { - startDate?: string; - endDate?: string; - }; - }; -}; - -// Fetches data from the /analytics-summary endpoint -export async function getTransactionAnalyticsSummary(props: { - teamId: string; - clientId: string; -}): Promise { - const authToken = await getAuthToken(); - const body = {}; - const defaultData: TransactionSummaryData = { - totalCount: 0, - totalGasCostWei: "0", - totalGasUnitsUsed: "0", - }; - - try { - const response = await fetch( - `${NEXT_PUBLIC_ENGINE_CLOUD_URL}/v1/transactions/analytics-summary`, - { - body: JSON.stringify(body), - headers: { - Authorization: `Bearer ${authToken}`, - "Content-Type": "application/json", - "x-client-id": props.clientId, - "x-team-id": props.teamId, - }, - method: "POST", - }, - ); - - if (!response.ok) { - if (response.status === 401 || response.status === 400) { - console.error("Unauthorized fetching transaction summary"); - return defaultData; - } - const errorText = await response.text(); - throw new Error( - `Error fetching transaction summary: ${response.status} ${response.statusText} - ${errorText}`, - ); - } - - const data = (await response.json()) as AnalyticsSummaryApiResponse; - - return { - totalCount: data.result.summary.totalCount ?? 0, - totalGasCostWei: data.result.summary.totalGasCostWei ?? "0", - totalGasUnitsUsed: data.result.summary.totalGasUnitsUsed ?? "0", - }; - } catch (error) { - console.error("Failed to fetch transaction summary:", error); - return defaultData; - } -} - -export async function getTransactionsChart({ - teamId, - clientId, - from, - to, - interval, -}: { - teamId: string; - clientId: string; - from: string; - to: string; - interval: "day" | "week"; -}): Promise { - const authToken = await getAuthToken(); - - const filters = { - endDate: to, - resolution: interval, - startDate: from, - }; - - const response = await fetch( - `${NEXT_PUBLIC_ENGINE_CLOUD_URL}/v1/transactions/analytics`, - { - body: JSON.stringify(filters), - headers: { - Authorization: `Bearer ${authToken}`, - "Content-Type": "application/json", - "x-client-id": clientId, - "x-team-id": teamId, - }, - method: "POST", - }, - ); - - if (!response.ok) { - if (response.status === 401 || response.status === 400) { - return []; - } - - // TODO - need to handle this error state, like we do with the connect charts - throw new Error( - `Error fetching transactions chart data: ${response.status} ${ - response.statusText - } - ${await response.text().catch(() => "Unknown error")}`, - ); - } - - type TransactionsChartResponse = { - result: { - analytics: Array<{ - timeBucket: string; - chainId: string; - count: number; - }>; - metadata: { - resolution: string; - startDate: string; - endDate: string; - }; - }; - }; - - const data = (await response.json()) as TransactionsChartResponse; - - return data.result.analytics.map((stat) => ({ - chainId: Number(stat.chainId), - count: stat.count, - date: stat.timeBucket, - })); -} - export async function getSingleTransaction({ teamId, clientId, diff --git a/apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/(sidebar)/transactions/lib/utils.ts b/apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/(sidebar)/transactions/lib/utils.ts deleted file mode 100644 index 5cdf97d7f4a..00000000000 --- a/apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/(sidebar)/transactions/lib/utils.ts +++ /dev/null @@ -1,14 +0,0 @@ -import { getFiltersFromSearchParams } from "@/lib/time"; - -export function getTxAnalyticsFiltersFromSearchParams(params: { - from?: string | undefined | string[]; - to?: string | undefined | string[]; - interval?: string | undefined | string[]; -}) { - return getFiltersFromSearchParams({ - defaultRange: "last-30", - from: params.from, - interval: params.interval, - to: params.to, - }); -} diff --git a/apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/(sidebar)/transactions/page.tsx b/apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/(sidebar)/transactions/page.tsx index e9a37e0525b..9e3deda8ddf 100644 --- a/apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/(sidebar)/transactions/page.tsx +++ b/apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/(sidebar)/transactions/page.tsx @@ -7,10 +7,7 @@ import { ProjectPage } from "@/components/blocks/project-page/project-page"; import { NEXT_PUBLIC_THIRDWEB_VAULT_URL } from "@/constants/public-envs"; import { getClientThirdwebClient } from "@/constants/thirdweb-client.client"; import { TransactionsAnalyticsPageContent } from "./analytics/analytics-page"; -import { EngineChecklist } from "./analytics/ftux.client"; -import { TransactionAnalyticsSummary } from "./analytics/summary"; import { ServerWalletsTable } from "./components/server-wallets-table.client"; -import { getTransactionAnalyticsSummary } from "./lib/analytics"; import type { Wallet } from "./server-wallets/wallet-table/types"; import { listSolanaAccounts } from "./solana-wallets/lib/vault.client"; import type { SolanaWallet } from "./solana-wallets/wallet-table/types"; @@ -110,12 +107,6 @@ export default async function TransactionsAnalyticsPage(props: { const isSolanaPermissionError = solanaAccounts.error?.message.includes("AUTH_INSUFFICIENT_SCOPE") ?? false; - const initialData = await getTransactionAnalyticsSummary({ - clientId: project.publishableKey, - teamId: project.teamId, - }).catch(() => undefined); - const hasTransactions = initialData ? initialData.totalCount > 0 : false; - const client = getClientThirdwebClient({ jwt: authToken, teamId: project.teamId, @@ -152,41 +143,30 @@ export default async function TransactionsAnalyticsPage(props: { }} >
- - {hasTransactions && - !searchParams.testTxWithWallet && - !searchParams.testSolanaTxWithWallet && ( - - )} - {/* transactions */} {/* Server Wallets (EVM + Solana) */}