Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 8 additions & 10 deletions apps/dashboard/src/@/api/analytics.ts
Original file line number Diff line number Diff line change
Expand Up @@ -48,15 +48,15 @@ async function fetchAnalytics(
);
}
// client id DEBUG OVERRIDE
// ANALYTICS_SERVICE_URL.searchParams.delete("projectId");
// ANALYTICS_SERVICE_URL.searchParams.delete("teamId");
// ANALYTICS_SERVICE_URL.searchParams.append(
// analyticsServiceUrl.searchParams.delete("projectId");
// analyticsServiceUrl.searchParams.delete("teamId");
// analyticsServiceUrl.searchParams.append(
// "teamId",
// "team_clmb33q9w00gn1x0u2ri8z0k0",
// "team_cm0lde33r02344w129k5hm2xz",
// );
// ANALYTICS_SERVICE_URL.searchParams.append(
// analyticsServiceUrl.searchParams.append(
// "projectId",
// "prj_clyqwud5y00u1na7nzxnzlz7o",
// "prj_cm4rqwx9b002qrnsnr37wqpo6",
// );

return fetch(analyticsServiceUrl, {
Expand Down Expand Up @@ -377,7 +377,7 @@ export async function getEcosystemWalletUsage(args: {

export async function getUniversalBridgeUsage(args: {
teamId: string;
projectId: string;
projectId?: string;
from?: Date;
to?: Date;
period?: "day" | "week" | "month" | "year" | "all";
Expand All @@ -395,11 +395,10 @@ export async function getUniversalBridgeUsage(args: {
console.error(
`Failed to fetch universal bridge stats: ${res?.status} - ${res.statusText} - ${reason}`,
);
return null;
return [];
}

const json = await res.json();

return json.data as UniversalBridgeStats[];
}

Expand Down Expand Up @@ -430,6 +429,5 @@ export async function getUniversalBridgeWalletUsage(args: {
}

const json = await res.json();

return json.data as UniversalBridgeWalletStats[];
}
1 change: 1 addition & 0 deletions apps/dashboard/src/@/components/ui/chart.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ export type ChartConfig = {
[k in string]: {
label?: React.ReactNode;
icon?: React.ComponentType;
isCurrency?: boolean;
} & (
| { color?: string; theme?: never }
| { color?: never; theme: Record<keyof typeof THEMES, string> }
Expand Down
9 changes: 5 additions & 4 deletions apps/dashboard/src/@/lib/time.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,9 +39,10 @@ export function getFiltersFromSearchParams(params: {
};

const defaultInterval =
differenceInCalendarDays(range.to, range.from) > 30
? "week"
: ("day" as const);
params.interval ??
(differenceInCalendarDays(range.to, range.from) > 30
? ("week" as const)
: ("day" as const));

return {
range,
Expand All @@ -50,6 +51,6 @@ export function getFiltersFromSearchParams(params: {
? ("day" as const)
: params.interval === "week"
? ("week" as const)
: defaultInterval,
: (defaultInterval as "day" | "week"),
};
}
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import {
getClientTransactions,
getInAppWalletUsage,
getUniversalBridgeUsage,
getUserOpUsage,
getWalletConnections,
getWalletUsers,
Expand All @@ -14,6 +15,7 @@ import { redirect } from "next/navigation";
import { type WalletId, getWalletInfo } from "thirdweb/wallets";
import type {
InAppWalletStats,
UniversalBridgeStats,
WalletStats,
WalletUserStats,
} from "types/analytics";
Expand All @@ -24,7 +26,10 @@ import { PieChartCard } from "../../../../components/Analytics/PieChartCard";

import { getTeamBySlug } from "@/api/team";
import { GenericLoadingPage } from "@/components/blocks/skeletons/GenericLoadingPage";
import { EmptyStateCard } from "app/(app)/team/components/Analytics/EmptyStateCard";
import {
EmptyStateCard,
EmptyStateContent,
} from "app/(app)/team/components/Analytics/EmptyStateCard";
import { Suspense } from "react";
import { TotalSponsoredChartCardUI } from "../../_components/TotalSponsoredCard";
import { TransactionsChartCardUI } from "../../_components/TransactionsCard";
Expand Down Expand Up @@ -100,6 +105,7 @@ async function OverviewPageContent(props: {
userOpUsage,
clientTransactionsTimeSeries,
clientTransactions,
universalBridgeUsage,
] = await Promise.all([
// Aggregated wallet connections
getWalletConnections({
Expand Down Expand Up @@ -148,6 +154,13 @@ async function OverviewPageContent(props: {
to: range.to,
period: "all",
}),
// Universal Bridge
getUniversalBridgeUsage({
teamId: teamId,
from: range.from,
to: range.to,
period: interval,
}),
]);

const isEmpty =
Expand All @@ -164,8 +177,9 @@ async function OverviewPageContent(props: {
<div className="flex grow flex-col gap-6">
{walletUserStatsTimeSeries.some((w) => w.totalUsers !== 0) ? (
<div className="">
<UsersChartCard
<AppHighlightsCard
userStats={walletUserStatsTimeSeries}
volumeStats={universalBridgeUsage}
searchParams={searchParams}
/>
</div>
Expand Down Expand Up @@ -218,70 +232,96 @@ async function OverviewPageContent(props: {
);
}

type UserMetrics = {
totalUsers: number;
type AggregatedMetrics = {
activeUsers: number;
newUsers: number;
returningUsers: number;
totalVolume: number;
feesCollected: number;
};

type TimeSeriesMetrics = UserMetrics & {
type TimeSeriesMetrics = AggregatedMetrics & {
date: string;
};

function processTimeSeriesData(
userStats: WalletUserStats[],
volumeStats: UniversalBridgeStats[],
): TimeSeriesMetrics[] {
const metrics: TimeSeriesMetrics[] = [];

let cumulativeUsers = 0;
for (const stat of userStats) {
cumulativeUsers += stat.newUsers ?? 0;
const volume = volumeStats
.filter((v) => v.date === stat.date && v.status === "completed")
.reduce((acc, curr) => acc + curr.amountUsdCents / 100, 0);

const fees = volumeStats
.filter((v) => v.date === stat.date && v.status === "completed")
.reduce((acc, curr) => acc + curr.developerFeeUsdCents / 100, 0);

metrics.push({
date: stat.date,
activeUsers: stat.totalUsers ?? 0,
returningUsers: stat.returningUsers ?? 0,
newUsers: stat.newUsers ?? 0,
totalUsers: cumulativeUsers,
totalVolume: volume,
feesCollected: fees,
});
}

return metrics;
}

function UsersChartCard({
function AppHighlightsCard({
userStats,
volumeStats,
searchParams,
}: {
userStats: WalletUserStats[];
volumeStats: UniversalBridgeStats[];
searchParams?: { [key: string]: string | string[] | undefined };
}) {
const timeSeriesData = processTimeSeriesData(userStats);
const timeSeriesData = processTimeSeriesData(userStats, volumeStats);

const chartConfig = {
activeUsers: { label: "Active Users", color: "hsl(var(--chart-1))" },
totalUsers: { label: "Total Users", color: "hsl(var(--chart-2))" },
newUsers: { label: "New Users", color: "hsl(var(--chart-3))" },
returningUsers: {
label: "Returning Users",
totalVolume: {
label: "Total Volume",
color: "hsl(var(--chart-2))",
isCurrency: true,
emptyContent: (
<EmptyStateContent
metric="Payments"
description="Onramp, swap, and bridge with thirdweb's Universal Bridge."
link="https://portal.thirdweb.com/connect/pay/overview"
/>
),
},
feesCollected: {
label: "Fee Revenue",
color: "hsl(var(--chart-4))",
isCurrency: true,
emptyContent: (
<EmptyStateContent
metric="Fees"
description="Your apps haven't collected any fees yet."
link={"https://portal.thirdweb.com/connect/pay/fees"}
/>
),
},
activeUsers: { label: "Active Users", color: "hsl(var(--chart-1))" },
newUsers: { label: "New Users", color: "hsl(var(--chart-3))" },
} as const;

return (
<CombinedBarChartCard
className="max-md:rounded-none max-md:border-r-0 max-md:border-l-0"
title="Users"
title="App Highlights"
chartConfig={chartConfig}
activeChart={
(searchParams?.usersChart as keyof UserMetrics) ?? "activeUsers"
(searchParams?.appHighlights as keyof AggregatedMetrics) ??
"totalVolume"
}
data={timeSeriesData}
aggregateFn={(_data, key) =>
// If there is only one data point, use that one, otherwise use the previous
timeSeriesData.filter((d) => (d[key] as number) > 0).length >= 2
? timeSeriesData[timeSeriesData.length - 2]?.[key]
: timeSeriesData[timeSeriesData.length - 1]?.[key]
timeSeriesData.reduce((acc, curr) => acc + curr[key], 0)
}
// Get the trend from the last two COMPLETE periods
trendFn={(data, key) =>
Expand All @@ -291,7 +331,7 @@ function UsersChartCard({
1
: undefined
}
queryKey="usersChart"
queryKey="appHighlights"
existingQueryParams={searchParams}
/>
);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,11 +14,11 @@ export default async function Page(props: {
team_slug: string;
project_slug: string;
}>;
searchParams: {
searchParams: Promise<{
from?: string | undefined | string[];
to?: string | undefined | string[];
interval?: string | undefined | string[];
};
}>;
}) {
const params = await props.params;
const project = await getProject(params.team_slug, params.project_slug);
Expand All @@ -36,7 +36,7 @@ export default async function Page(props: {
});

return (
<ResponsiveSearchParamsProvider value={props.searchParams}>
<ResponsiveSearchParamsProvider value={searchParams}>
<div>
<div className="mb-4 flex justify-start">
<PayAnalyticsFilter />
Expand Down
Loading
Loading