Skip to content

Commit 03449f2

Browse files
arcoravenjnsdls
authored andcommitted
feat: add Engine Cloud analytics to project overview
1 parent ac4ade6 commit 03449f2

File tree

7 files changed

+258
-0
lines changed

7 files changed

+258
-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,56 @@
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+
// biome-ignore lint/style/noNonNullAssertion: we know this is not null
31+
date: dateStr!,
32+
chainId: "84532",
33+
pathname,
34+
totalRequests: Math.floor(Math.random() * 1000) + 100,
35+
});
36+
}
37+
}
38+
39+
return data;
40+
};
41+
42+
function Component() {
43+
return (
44+
<div className="container max-w-6xl space-y-10 py-10">
45+
<BadgeContainer label="Multiple Pathnames">
46+
<EngineCloudBarChartCardUI
47+
rawData={generateTimeSeriesData(30, [
48+
"/v1/write/transaction",
49+
"/v1/sign/transaction",
50+
"/v1/sign/message",
51+
])}
52+
/>
53+
</BadgeContainer>
54+
</div>
55+
);
56+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,139 @@
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+
if (data.length === 0 || isAllEmpty) {
67+
return <EmptyStateCard metric="RPC" link="https://portal.thirdweb.com/" />;
68+
}
69+
70+
return (
71+
<Card>
72+
<CardHeader className="flex flex-col items-stretch space-y-0 border-b p-0">
73+
<div className="flex flex-1 flex-col justify-center gap-1 p-6">
74+
<CardTitle className="font-semibold text-lg">
75+
Engine Cloud Requests
76+
</CardTitle>
77+
</div>
78+
</CardHeader>
79+
<CardContent className="px-2 sm:p-6 sm:pl-0">
80+
<ChartContainer
81+
config={chartConfig}
82+
className="aspect-auto h-[250px] w-full pt-6"
83+
>
84+
<RechartsBarChart
85+
accessibilityLayer
86+
data={data}
87+
margin={{
88+
left: 12,
89+
right: 12,
90+
}}
91+
>
92+
<CartesianGrid vertical={false} />
93+
<XAxis
94+
dataKey="date"
95+
tickLine={false}
96+
axisLine={false}
97+
tickMargin={8}
98+
minTickGap={32}
99+
tickFormatter={(value: string) => {
100+
const date = new Date(value);
101+
return date.toLocaleDateString("en-US", {
102+
month: "short",
103+
day: "numeric",
104+
});
105+
}}
106+
/>
107+
<ChartTooltip
108+
content={
109+
<ChartTooltipContent
110+
labelFormatter={(d) => formatDate(new Date(d), "MMM d")}
111+
valueFormatter={(_value) => {
112+
const value = typeof _value === "number" ? _value : 0;
113+
return <span className="inline-flex gap-1.5">{value}</span>;
114+
}}
115+
/>
116+
}
117+
/>
118+
{pathnames.map((pathname, idx) => (
119+
<Bar
120+
key={pathname}
121+
stackId="a"
122+
dataKey={pathname}
123+
radius={[
124+
idx === pathnames.length - 1 ? 4 : 0,
125+
idx === pathnames.length - 1 ? 4 : 0,
126+
idx === 0 ? 4 : 0,
127+
idx === 0 ? 4 : 0,
128+
]}
129+
fill={`hsl(var(--chart-${idx + 1}))`}
130+
strokeWidth={1}
131+
className="stroke-background"
132+
/>
133+
))}
134+
</RechartsBarChart>
135+
</ChartContainer>
136+
</CardContent>
137+
</Card>
138+
);
139+
}
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)