Skip to content

Commit fd4e030

Browse files
committed
feat: add Engine Cloud analytics to project overview
1 parent e14a134 commit fd4e030

File tree

7 files changed

+259
-0
lines changed

7 files changed

+259
-0
lines changed

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ yalc.lock
1616
./build/
1717
playwright-report/
1818
.env/
19+
.pnpm-store/
1920

2021
# codecov binary
2122
codecov

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

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import "server-only";
33
import type {
44
AnalyticsQueryParams,
55
EcosystemWalletStats,
6+
EngineCloudStats,
67
InAppWalletStats,
78
RpcMethodStats,
89
TransactionStats,
@@ -431,3 +432,24 @@ export async function getUniversalBridgeWalletUsage(args: {
431432
const json = await res.json();
432433
return json.data as UniversalBridgeWalletStats[];
433434
}
435+
436+
export async function getEngineCloudMethodUsage(
437+
params: AnalyticsQueryParams,
438+
): Promise<EngineCloudStats[]> {
439+
const searchParams = buildSearchParams(params);
440+
const res = await fetchAnalytics(
441+
`v2/engine-cloud/requests?${searchParams.toString()}`,
442+
{
443+
method: "GET",
444+
headers: { "Content-Type": "application/json" },
445+
},
446+
);
447+
448+
if (res?.status !== 200) {
449+
console.error("Failed to fetch Engine Cloud method usage");
450+
return [];
451+
}
452+
453+
const json = await res.json();
454+
return json.data as EngineCloudStats[];
455+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
import type { Meta, StoryObj } from "@storybook/react";
2+
import { BadgeContainer } from "stories/utils";
3+
import type { EngineCloudStats } from "types/analytics";
4+
import { EngineCloudBarChartCardUI } from "./EngineCloudBarChartCardUI";
5+
6+
const meta = {
7+
title: "Analytics/EngineCloudBarChartCard",
8+
component: Component,
9+
} satisfies Meta<typeof Component>;
10+
11+
export default meta;
12+
type Story = StoryObj<typeof meta>;
13+
14+
export const Variants: Story = {
15+
parameters: {
16+
viewport: { defaultViewport: "desktop" },
17+
},
18+
};
19+
20+
const generateTimeSeriesData = (days: number, pathnames: string[]) => {
21+
const data: EngineCloudStats[] = [];
22+
const today = new Date();
23+
24+
for (let i = days - 1; i >= 0; i--) {
25+
const date = new Date(today);
26+
date.setDate(date.getDate() - i);
27+
const dateStr = date.toISOString().split("T")[0];
28+
for (const pathname of pathnames) {
29+
data.push({
30+
date: dateStr,
31+
chainId: "84532",
32+
pathname,
33+
totalRequests: Math.floor(Math.random() * 1000) + 100,
34+
});
35+
}
36+
}
37+
38+
return data;
39+
};
40+
41+
function Component() {
42+
return (
43+
<div className="container max-w-6xl space-y-10 py-10">
44+
<BadgeContainer label="Multiple Pathnames">
45+
<EngineCloudBarChartCardUI
46+
rawData={generateTimeSeriesData(30, [
47+
"/v1/write/transaction",
48+
"/v1/sign/transaction",
49+
"/v1/sign/message",
50+
])}
51+
/>
52+
</BadgeContainer>
53+
</div>
54+
);
55+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,141 @@
1+
"use client";
2+
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
3+
import {
4+
type ChartConfig,
5+
ChartContainer,
6+
ChartTooltip,
7+
ChartTooltipContent,
8+
} from "@/components/ui/chart";
9+
import { formatDate } from "date-fns";
10+
import { useMemo } from "react";
11+
import {
12+
Bar,
13+
CartesianGrid,
14+
BarChart as RechartsBarChart,
15+
XAxis,
16+
} from "recharts";
17+
import type { EngineCloudStats } from "types/analytics";
18+
import { EmptyStateCard } from "../../../../../components/Analytics/EmptyStateCard";
19+
20+
export function EngineCloudBarChartCardUI({
21+
rawData,
22+
}: { rawData: EngineCloudStats[] }) {
23+
const { data, pathnames, chartConfig, isAllEmpty } = useMemo(() => {
24+
// Dynamically collect all unique pathnames
25+
const pathnameSet = new Set<string>();
26+
for (const item of rawData) {
27+
// Ignore empty pathname ''.
28+
if (item.pathname) {
29+
pathnameSet.add(item.pathname);
30+
}
31+
}
32+
const pathnames = Array.from(pathnameSet);
33+
34+
// Group by date, then by pathname
35+
const dateMap = new Map<string, Record<string, number>>();
36+
for (const { date, pathname, totalRequests } of rawData) {
37+
const map = dateMap.get(date) ?? {};
38+
map[pathname] = Number(totalRequests) || 0;
39+
dateMap.set(date, map);
40+
}
41+
42+
// Build data array for recharts
43+
const data = Array.from(dateMap.entries()).map(([date, value]) => {
44+
let total = 0;
45+
for (const pathname of pathnames) {
46+
if (!value[pathname]) value[pathname] = 0;
47+
total += value[pathname];
48+
}
49+
return { date, ...value, total };
50+
});
51+
52+
// Chart config
53+
const chartConfig: ChartConfig = {};
54+
for (const pathname of pathnames) {
55+
chartConfig[pathname] = { label: pathname };
56+
}
57+
58+
return {
59+
data,
60+
pathnames,
61+
chartConfig,
62+
isAllEmpty: data.every((d) => d.total === 0),
63+
};
64+
}, [rawData]);
65+
66+
console.log("[DEBUG] data:", data);
67+
68+
if (data.length === 0 || isAllEmpty) {
69+
return <EmptyStateCard metric="RPC" link="https://portal.thirdweb.com/" />;
70+
}
71+
72+
return (
73+
<Card>
74+
<CardHeader className="flex flex-col items-stretch space-y-0 border-b p-0">
75+
<div className="flex flex-1 flex-col justify-center gap-1 p-6">
76+
<CardTitle className="font-semibold text-lg">
77+
Engine Cloud Requests
78+
</CardTitle>
79+
</div>
80+
</CardHeader>
81+
<CardContent className="px-2 sm:p-6 sm:pl-0">
82+
<ChartContainer
83+
config={chartConfig}
84+
className="aspect-auto h-[250px] w-full pt-6"
85+
>
86+
<RechartsBarChart
87+
accessibilityLayer
88+
data={data}
89+
margin={{
90+
left: 12,
91+
right: 12,
92+
}}
93+
>
94+
<CartesianGrid vertical={false} />
95+
<XAxis
96+
dataKey="date"
97+
tickLine={false}
98+
axisLine={false}
99+
tickMargin={8}
100+
minTickGap={32}
101+
tickFormatter={(value: string) => {
102+
const date = new Date(value);
103+
return date.toLocaleDateString("en-US", {
104+
month: "short",
105+
day: "numeric",
106+
});
107+
}}
108+
/>
109+
<ChartTooltip
110+
content={
111+
<ChartTooltipContent
112+
labelFormatter={(d) => formatDate(new Date(d), "MMM d")}
113+
valueFormatter={(_value, _item) => {
114+
const value = typeof _value === "number" ? _value : 0;
115+
return <span className="inline-flex gap-1.5">{value}</span>;
116+
}}
117+
/>
118+
}
119+
/>
120+
{pathnames.map((pathname, idx) => (
121+
<Bar
122+
key={pathname}
123+
stackId="a"
124+
dataKey={pathname}
125+
radius={[
126+
idx === pathnames.length - 1 ? 4 : 0,
127+
idx === pathnames.length - 1 ? 4 : 0,
128+
idx === 0 ? 4 : 0,
129+
idx === 0 ? 4 : 0,
130+
]}
131+
fill={`hsl(var(--chart-${idx + 1}))`}
132+
strokeWidth={1}
133+
className="stroke-background"
134+
/>
135+
))}
136+
</RechartsBarChart>
137+
</ChartContainer>
138+
</CardContent>
139+
</Card>
140+
);
141+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
import { getEngineCloudMethodUsage } from "@/api/analytics";
2+
import { LoadingChartState } from "components/analytics/empty-chart-state";
3+
import { Suspense } from "react";
4+
import type { AnalyticsQueryParams } from "types/analytics";
5+
import { EngineCloudBarChartCardUI } from "./EngineCloudBarChartCardUI";
6+
7+
export function EngineCloudChartCard(props: AnalyticsQueryParams) {
8+
return (
9+
<Suspense
10+
fallback={
11+
<div className="h-[400px]">
12+
<LoadingChartState />
13+
</div>
14+
}
15+
>
16+
<EngineCloudChartCardAsync {...props} />
17+
</Suspense>
18+
);
19+
}
20+
21+
async function EngineCloudChartCardAsync(props: AnalyticsQueryParams) {
22+
const rawData = await getEngineCloudMethodUsage(props);
23+
24+
return <EngineCloudBarChartCardUI rawData={rawData} />;
25+
}

apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/(sidebar)/page.tsx

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,7 @@ import { getAuthToken } from "../../../../api/lib/getAuthToken";
4141
import { loginRedirect } from "../../../../login/loginRedirect";
4242
import { CombinedBarChartCard } from "../../../components/Analytics/CombinedBarChartCard";
4343
import { PieChartCard } from "../../../components/Analytics/PieChartCard";
44+
import { EngineCloudChartCard } from "./components/EngineCloudChartCard";
4445
import { ProjectFTUX } from "./components/ProjectFTUX/ProjectFTUX";
4546
import { RpcMethodBarChartCard } from "./components/RpcMethodBarChartCard";
4647
import { TransactionsCharts } from "./components/Transactions";
@@ -279,6 +280,13 @@ async function ProjectAnalytics(props: {
279280
teamId={project.teamId}
280281
projectId={project.id}
281282
/>
283+
<EngineCloudChartCard
284+
from={range.from}
285+
to={range.to}
286+
period={interval}
287+
teamId={project.teamId}
288+
projectId={project.id}
289+
/>
282290
</div>
283291
);
284292
}

apps/dashboard/src/types/analytics.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,13 @@ export interface RpcMethodStats {
4545
count: number;
4646
}
4747

48+
export interface EngineCloudStats {
49+
date: string;
50+
chainId: string;
51+
pathname: string;
52+
totalRequests: number;
53+
}
54+
4855
export interface UniversalBridgeStats {
4956
date: string;
5057
chainId: number;

0 commit comments

Comments
 (0)