Skip to content

Commit 0eff648

Browse files
gregfromstlClaude
and
Claude
authored
[Dashboard] Feature: Add Universal Bridge to analytics dashboard (#7051)
Co-authored-by: Claude <[email protected]>
1 parent ae7407d commit 0eff648

File tree

13 files changed

+225
-103
lines changed

13 files changed

+225
-103
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: 5 additions & 4 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
43-
? "week"
44-
: ("day" as const);
42+
params.interval ??
43+
(differenceInCalendarDays(range.to, range.from) > 30
44+
? ("week" as const)
45+
: ("day" as const));
4546

4647
return {
4748
range,
@@ -50,6 +51,6 @@ export function getFiltersFromSearchParams(params: {
5051
? ("day" as const)
5152
: params.interval === "week"
5253
? ("week" as const)
53-
: defaultInterval,
54+
: (defaultInterval as "day" | "week"),
5455
};
5556
}

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: 3 additions & 3 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 />

0 commit comments

Comments
 (0)