Skip to content

Commit 49204b5

Browse files
gregfromstlclaude
andcommitted
feat: add Universal Bridge to analytics dashboard
This PR adds Universal Bridge payment data to the main analytics dashboard, enhancing visibility of payments volume and fee revenue alongside user metrics. Key changes include: - Added volume and fee collection metrics to team and project analytics pages - Renamed "Users" chart card to "App Highlights" to better represent the combined metrics - Improved empty state handling with specific call-to-actions - Fixed interval parameter handling in analytics API - Made Universal Bridge projectId parameter optional for team-wide stats 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <[email protected]>
1 parent ae7407d commit 49204b5

File tree

12 files changed

+214
-90
lines changed

12 files changed

+214
-90
lines changed

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

Lines changed: 8 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -48,15 +48,15 @@ async function fetchAnalytics(
4848
);
4949
}
5050
// client id DEBUG OVERRIDE
51-
// ANALYTICS_SERVICE_URL.searchParams.delete("projectId");
52-
// ANALYTICS_SERVICE_URL.searchParams.delete("teamId");
53-
// ANALYTICS_SERVICE_URL.searchParams.append(
51+
// analyticsServiceUrl.searchParams.delete("projectId");
52+
// analyticsServiceUrl.searchParams.delete("teamId");
53+
// analyticsServiceUrl.searchParams.append(
5454
// "teamId",
55-
// "team_clmb33q9w00gn1x0u2ri8z0k0",
55+
// "team_cm0lde33r02344w129k5hm2xz",
5656
// );
57-
// ANALYTICS_SERVICE_URL.searchParams.append(
57+
// analyticsServiceUrl.searchParams.append(
5858
// "projectId",
59-
// "prj_clyqwud5y00u1na7nzxnzlz7o",
59+
// "prj_cm4rqwx9b002qrnsnr37wqpo6",
6060
// );
6161

6262
return fetch(analyticsServiceUrl, {
@@ -377,7 +377,7 @@ export async function getEcosystemWalletUsage(args: {
377377

378378
export async function getUniversalBridgeUsage(args: {
379379
teamId: string;
380-
projectId: string;
380+
projectId?: string;
381381
from?: Date;
382382
to?: Date;
383383
period?: "day" | "week" | "month" | "year" | "all";
@@ -395,11 +395,10 @@ export async function getUniversalBridgeUsage(args: {
395395
console.error(
396396
`Failed to fetch universal bridge stats: ${res?.status} - ${res.statusText} - ${reason}`,
397397
);
398-
return null;
398+
return [];
399399
}
400400

401401
const json = await res.json();
402-
403402
return json.data as UniversalBridgeStats[];
404403
}
405404

@@ -430,6 +429,5 @@ export async function getUniversalBridgeWalletUsage(args: {
430429
}
431430

432431
const json = await res.json();
433-
434432
return json.data as UniversalBridgeWalletStats[];
435433
}

apps/dashboard/src/@/components/ui/chart.tsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ export type ChartConfig = {
1212
[k in string]: {
1313
label?: React.ReactNode;
1414
icon?: React.ComponentType;
15+
isCurrency?: boolean;
1516
} & (
1617
| { color?: string; theme?: never }
1718
| { color?: never; theme: Record<keyof typeof THEMES, string> }

apps/dashboard/src/@/lib/time.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -39,9 +39,10 @@ export function getFiltersFromSearchParams(params: {
3939
};
4040

4141
const defaultInterval =
42-
differenceInCalendarDays(range.to, range.from) > 30
42+
params.interval ??
43+
(differenceInCalendarDays(range.to, range.from) > 30
4344
? "week"
44-
: ("day" as const);
45+
: ("day" as const));
4546

4647
return {
4748
range,

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

Lines changed: 64 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import {
22
getClientTransactions,
33
getInAppWalletUsage,
4+
getUniversalBridgeUsage,
45
getUserOpUsage,
56
getWalletConnections,
67
getWalletUsers,
@@ -14,6 +15,7 @@ import { redirect } from "next/navigation";
1415
import { type WalletId, getWalletInfo } from "thirdweb/wallets";
1516
import type {
1617
InAppWalletStats,
18+
UniversalBridgeStats,
1719
WalletStats,
1820
WalletUserStats,
1921
} from "types/analytics";
@@ -24,7 +26,10 @@ import { PieChartCard } from "../../../../components/Analytics/PieChartCard";
2426

2527
import { getTeamBySlug } from "@/api/team";
2628
import { GenericLoadingPage } from "@/components/blocks/skeletons/GenericLoadingPage";
27-
import { EmptyStateCard } from "app/(app)/team/components/Analytics/EmptyStateCard";
29+
import {
30+
EmptyStateCard,
31+
EmptyStateContent,
32+
} from "app/(app)/team/components/Analytics/EmptyStateCard";
2833
import { Suspense } from "react";
2934
import { TotalSponsoredChartCardUI } from "../../_components/TotalSponsoredCard";
3035
import { TransactionsChartCardUI } from "../../_components/TransactionsCard";
@@ -100,6 +105,7 @@ async function OverviewPageContent(props: {
100105
userOpUsage,
101106
clientTransactionsTimeSeries,
102107
clientTransactions,
108+
universalBridgeUsage,
103109
] = await Promise.all([
104110
// Aggregated wallet connections
105111
getWalletConnections({
@@ -148,6 +154,13 @@ async function OverviewPageContent(props: {
148154
to: range.to,
149155
period: "all",
150156
}),
157+
// Universal Bridge
158+
getUniversalBridgeUsage({
159+
teamId: teamId,
160+
from: range.from,
161+
to: range.to,
162+
period: interval,
163+
}),
151164
]);
152165

153166
const isEmpty =
@@ -164,8 +177,9 @@ async function OverviewPageContent(props: {
164177
<div className="flex grow flex-col gap-6">
165178
{walletUserStatsTimeSeries.some((w) => w.totalUsers !== 0) ? (
166179
<div className="">
167-
<UsersChartCard
180+
<AppHighlightsCard
168181
userStats={walletUserStatsTimeSeries}
182+
volumeStats={universalBridgeUsage}
169183
searchParams={searchParams}
170184
/>
171185
</div>
@@ -218,70 +232,96 @@ async function OverviewPageContent(props: {
218232
);
219233
}
220234

221-
type UserMetrics = {
222-
totalUsers: number;
235+
type AggregatedMetrics = {
223236
activeUsers: number;
224237
newUsers: number;
225-
returningUsers: number;
238+
totalVolume: number;
239+
feesCollected: number;
226240
};
227241

228-
type TimeSeriesMetrics = UserMetrics & {
242+
type TimeSeriesMetrics = AggregatedMetrics & {
229243
date: string;
230244
};
231245

232246
function processTimeSeriesData(
233247
userStats: WalletUserStats[],
248+
volumeStats: UniversalBridgeStats[],
234249
): TimeSeriesMetrics[] {
235250
const metrics: TimeSeriesMetrics[] = [];
236251

237-
let cumulativeUsers = 0;
238252
for (const stat of userStats) {
239-
cumulativeUsers += stat.newUsers ?? 0;
253+
const volume = volumeStats
254+
.filter((v) => v.date === stat.date && v.status === "completed")
255+
.reduce((acc, curr) => acc + curr.amountUsdCents / 100, 0);
256+
257+
const fees = volumeStats
258+
.filter((v) => v.date === stat.date && v.status === "completed")
259+
.reduce((acc, curr) => acc + curr.developerFeeUsdCents / 100, 0);
260+
240261
metrics.push({
241262
date: stat.date,
242263
activeUsers: stat.totalUsers ?? 0,
243-
returningUsers: stat.returningUsers ?? 0,
244264
newUsers: stat.newUsers ?? 0,
245-
totalUsers: cumulativeUsers,
265+
totalVolume: volume,
266+
feesCollected: fees,
246267
});
247268
}
248269

249270
return metrics;
250271
}
251272

252-
function UsersChartCard({
273+
function AppHighlightsCard({
253274
userStats,
275+
volumeStats,
254276
searchParams,
255277
}: {
256278
userStats: WalletUserStats[];
279+
volumeStats: UniversalBridgeStats[];
257280
searchParams?: { [key: string]: string | string[] | undefined };
258281
}) {
259-
const timeSeriesData = processTimeSeriesData(userStats);
282+
const timeSeriesData = processTimeSeriesData(userStats, volumeStats);
260283

261284
const chartConfig = {
262-
activeUsers: { label: "Active Users", color: "hsl(var(--chart-1))" },
263-
totalUsers: { label: "Total Users", color: "hsl(var(--chart-2))" },
264-
newUsers: { label: "New Users", color: "hsl(var(--chart-3))" },
265-
returningUsers: {
266-
label: "Returning Users",
285+
totalVolume: {
286+
label: "Total Volume",
287+
color: "hsl(var(--chart-2))",
288+
isCurrency: true,
289+
emptyContent: (
290+
<EmptyStateContent
291+
metric="Payments"
292+
description="Onramp, swap, and bridge with thirdweb's Universal Bridge."
293+
link="https://portal.thirdweb.com/connect/pay/overview"
294+
/>
295+
),
296+
},
297+
feesCollected: {
298+
label: "Fee Revenue",
267299
color: "hsl(var(--chart-4))",
300+
isCurrency: true,
301+
emptyContent: (
302+
<EmptyStateContent
303+
metric="Fees"
304+
description="Your apps haven't collected any fees yet."
305+
link={"https://portal.thirdweb.com/connect/pay/fees"}
306+
/>
307+
),
268308
},
309+
activeUsers: { label: "Active Users", color: "hsl(var(--chart-1))" },
310+
newUsers: { label: "New Users", color: "hsl(var(--chart-3))" },
269311
} as const;
270312

271313
return (
272314
<CombinedBarChartCard
273315
className="max-md:rounded-none max-md:border-r-0 max-md:border-l-0"
274-
title="Users"
316+
title="App Highlights"
275317
chartConfig={chartConfig}
276318
activeChart={
277-
(searchParams?.usersChart as keyof UserMetrics) ?? "activeUsers"
319+
(searchParams?.appHighlights as keyof AggregatedMetrics) ??
320+
"totalVolume"
278321
}
279322
data={timeSeriesData}
280323
aggregateFn={(_data, key) =>
281-
// If there is only one data point, use that one, otherwise use the previous
282-
timeSeriesData.filter((d) => (d[key] as number) > 0).length >= 2
283-
? timeSeriesData[timeSeriesData.length - 2]?.[key]
284-
: timeSeriesData[timeSeriesData.length - 1]?.[key]
324+
timeSeriesData.reduce((acc, curr) => acc + curr[key], 0)
285325
}
286326
// Get the trend from the last two COMPLETE periods
287327
trendFn={(data, key) =>
@@ -291,7 +331,7 @@ function UsersChartCard({
291331
1
292332
: undefined
293333
}
294-
queryKey="usersChart"
334+
queryKey="appHighlights"
295335
existingQueryParams={searchParams}
296336
/>
297337
);

apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/connect/universal-bridge/page.tsx

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -14,11 +14,11 @@ export default async function Page(props: {
1414
team_slug: string;
1515
project_slug: string;
1616
}>;
17-
searchParams: {
17+
searchParams: Promise<{
1818
from?: string | undefined | string[];
1919
to?: string | undefined | string[];
2020
interval?: string | undefined | string[];
21-
};
21+
}>;
2222
}) {
2323
const params = await props.params;
2424
const project = await getProject(params.team_slug, params.project_slug);
@@ -36,7 +36,7 @@ export default async function Page(props: {
3636
});
3737

3838
return (
39-
<ResponsiveSearchParamsProvider value={props.searchParams}>
39+
<ResponsiveSearchParamsProvider value={searchParams}>
4040
<div>
4141
<div className="mb-4 flex justify-start">
4242
<PayAnalyticsFilter />
@@ -54,7 +54,7 @@ export default async function Page(props: {
5454
projectId={project.id}
5555
teamId={project.teamId}
5656
range={range}
57-
interval={interval}
57+
interval={interval as "day" | "week"}
5858
/>
5959
</ResponsiveSuspense>
6060
</div>

0 commit comments

Comments
 (0)