From f2e8b9ecabac270c4ffa9761e0ac6d9df885031a Mon Sep 17 00:00:00 2001 From: arcoraven Date: Fri, 30 May 2025 01:34:51 +0000 Subject: [PATCH] feat: add Engine Cloud analytics to project overview (#7195) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit # [Dashboard] Feature: Add Engine Cloud Analytics Charts Example screenshot: ![image.png](https://graphite-user-uploaded-assets-prod.s3.amazonaws.com/vKjRiOG6EUjD7XH4cppU/73d7e2b9-23bd-46b5-8cae-5564a3630770.png) ## Notes for the reviewer This PR adds Engine Cloud analytics to the dashboard, displaying request data in a bar chart visualization. The implementation includes: 1. A new API endpoint to fetch Engine Cloud method usage data 2. New UI components for displaying Engine Cloud analytics: - `EngineCloudBarChartCardUI` - Shows Engine Cloud requests by pathname - `EngineCloudChartCardUI` - Shows Engine Cloud methods usage with percentage breakdown 3. Integration of the new chart into the project analytics page ## How to test - View the Storybook component at "Analytics/EngineCloudBarChartCard" - Navigate to a project dashboard to see the Engine Cloud analytics chart - Verify data is properly fetched and displayed in the chart --- ## PR-Codex overview This PR introduces a new feature to track and visualize `EngineCloud` statistics in the dashboard, enhancing analytics capabilities with new components and API integrations. ### Detailed summary - Added `EngineCloudStats` interface in `analytics.ts`. - Implemented `getEngineCloudMethodUsage` API function. - Created `EngineCloudChartCard` component for displaying cloud stats. - Developed `EngineCloudBarChartCardUI` for rendering bar charts. - Added storybook entries for `EngineCloudBarChartCardUI`. - Enhanced existing analytics with new data visualizations. > ✨ Ask PR-Codex anything about this PR by commenting with `/codex {your question}` ## Summary by CodeRabbit - **New Features** - Introduced a stacked bar chart visualization for engine cloud request statistics in the analytics dashboard. - Added a chart card displaying engine cloud method usage data with loading state support. - Enhanced analytics with time series data for multiple API pathnames. - Included engine cloud method usage data in the project analytics view. - **Chores** - Updated `.gitignore` to exclude the `.pnpm-store/` directory from version control. --- .gitignore | 1 + apps/dashboard/src/@/api/analytics.ts | 22 +++ .../EngineCloudBarChartCard.stories.tsx | 56 +++++++ .../EngineCloudBarChartCardUI.tsx | 139 ++++++++++++++++++ .../components/EngineCloudChartCard/index.tsx | 25 ++++ .../[project_slug]/(sidebar)/page.tsx | 8 + apps/dashboard/src/types/analytics.ts | 7 + 7 files changed, 258 insertions(+) create mode 100644 apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/(sidebar)/components/EngineCloudChartCard/EngineCloudBarChartCard.stories.tsx create mode 100644 apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/(sidebar)/components/EngineCloudChartCard/EngineCloudBarChartCardUI.tsx create mode 100644 apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/(sidebar)/components/EngineCloudChartCard/index.tsx diff --git a/.gitignore b/.gitignore index 9706c4444dd..ec0919be192 100644 --- a/.gitignore +++ b/.gitignore @@ -16,6 +16,7 @@ yalc.lock ./build/ playwright-report/ .env/ +.pnpm-store/ # codecov binary codecov diff --git a/apps/dashboard/src/@/api/analytics.ts b/apps/dashboard/src/@/api/analytics.ts index 0bed8ec8eff..d7a80fab9bf 100644 --- a/apps/dashboard/src/@/api/analytics.ts +++ b/apps/dashboard/src/@/api/analytics.ts @@ -3,6 +3,7 @@ import "server-only"; import type { AnalyticsQueryParams, EcosystemWalletStats, + EngineCloudStats, InAppWalletStats, RpcMethodStats, TransactionStats, @@ -431,3 +432,24 @@ export async function getUniversalBridgeWalletUsage(args: { const json = await res.json(); return json.data as UniversalBridgeWalletStats[]; } + +export async function getEngineCloudMethodUsage( + params: AnalyticsQueryParams, +): Promise { + const searchParams = buildSearchParams(params); + const res = await fetchAnalytics( + `v2/engine-cloud/requests?${searchParams.toString()}`, + { + method: "GET", + headers: { "Content-Type": "application/json" }, + }, + ); + + if (res?.status !== 200) { + console.error("Failed to fetch Engine Cloud method usage"); + return []; + } + + const json = await res.json(); + return json.data as EngineCloudStats[]; +} diff --git a/apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/(sidebar)/components/EngineCloudChartCard/EngineCloudBarChartCard.stories.tsx b/apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/(sidebar)/components/EngineCloudChartCard/EngineCloudBarChartCard.stories.tsx new file mode 100644 index 00000000000..93aad2af143 --- /dev/null +++ b/apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/(sidebar)/components/EngineCloudChartCard/EngineCloudBarChartCard.stories.tsx @@ -0,0 +1,56 @@ +import type { Meta, StoryObj } from "@storybook/react"; +import { BadgeContainer } from "stories/utils"; +import type { EngineCloudStats } from "types/analytics"; +import { EngineCloudBarChartCardUI } from "./EngineCloudBarChartCardUI"; + +const meta = { + title: "Analytics/EngineCloudBarChartCard", + component: Component, +} satisfies Meta; + +export default meta; +type Story = StoryObj; + +export const Variants: Story = { + parameters: { + viewport: { defaultViewport: "desktop" }, + }, +}; + +const generateTimeSeriesData = (days: number, pathnames: string[]) => { + const data: EngineCloudStats[] = []; + const today = new Date(); + + for (let i = days - 1; i >= 0; i--) { + const date = new Date(today); + date.setDate(date.getDate() - i); + const dateStr = date.toISOString().split("T")[0]; + for (const pathname of pathnames) { + data.push({ + // biome-ignore lint/style/noNonNullAssertion: we know this is not null + date: dateStr!, + chainId: "84532", + pathname, + totalRequests: Math.floor(Math.random() * 1000) + 100, + }); + } + } + + return data; +}; + +function Component() { + return ( +
+ + + +
+ ); +} diff --git a/apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/(sidebar)/components/EngineCloudChartCard/EngineCloudBarChartCardUI.tsx b/apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/(sidebar)/components/EngineCloudChartCard/EngineCloudBarChartCardUI.tsx new file mode 100644 index 00000000000..a9fd7a0ae6a --- /dev/null +++ b/apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/(sidebar)/components/EngineCloudChartCard/EngineCloudBarChartCardUI.tsx @@ -0,0 +1,139 @@ +"use client"; +import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; +import { + type ChartConfig, + ChartContainer, + ChartTooltip, + ChartTooltipContent, +} from "@/components/ui/chart"; +import { formatDate } from "date-fns"; +import { useMemo } from "react"; +import { + Bar, + CartesianGrid, + BarChart as RechartsBarChart, + XAxis, +} from "recharts"; +import type { EngineCloudStats } from "types/analytics"; +import { EmptyStateCard } from "../../../../../components/Analytics/EmptyStateCard"; + +export function EngineCloudBarChartCardUI({ + rawData, +}: { rawData: EngineCloudStats[] }) { + const { data, pathnames, chartConfig, isAllEmpty } = useMemo(() => { + // Dynamically collect all unique pathnames + const pathnameSet = new Set(); + for (const item of rawData) { + // Ignore empty pathname ''. + if (item.pathname) { + pathnameSet.add(item.pathname); + } + } + const pathnames = Array.from(pathnameSet); + + // Group by date, then by pathname + const dateMap = new Map>(); + for (const { date, pathname, totalRequests } of rawData) { + const map = dateMap.get(date) ?? {}; + map[pathname] = Number(totalRequests) || 0; + dateMap.set(date, map); + } + + // Build data array for recharts + const data = Array.from(dateMap.entries()).map(([date, value]) => { + let total = 0; + for (const pathname of pathnames) { + if (!value[pathname]) value[pathname] = 0; + total += value[pathname]; + } + return { date, ...value, total }; + }); + + // Chart config + const chartConfig: ChartConfig = {}; + for (const pathname of pathnames) { + chartConfig[pathname] = { label: pathname }; + } + + return { + data, + pathnames, + chartConfig, + isAllEmpty: data.every((d) => d.total === 0), + }; + }, [rawData]); + + if (data.length === 0 || isAllEmpty) { + return ; + } + + return ( + + +
+ + Engine Cloud Requests + +
+
+ + + + + { + const date = new Date(value); + return date.toLocaleDateString("en-US", { + month: "short", + day: "numeric", + }); + }} + /> + formatDate(new Date(d), "MMM d")} + valueFormatter={(_value) => { + const value = typeof _value === "number" ? _value : 0; + return {value}; + }} + /> + } + /> + {pathnames.map((pathname, idx) => ( + + ))} + + + +
+ ); +} diff --git a/apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/(sidebar)/components/EngineCloudChartCard/index.tsx b/apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/(sidebar)/components/EngineCloudChartCard/index.tsx new file mode 100644 index 00000000000..3fef44b7637 --- /dev/null +++ b/apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/(sidebar)/components/EngineCloudChartCard/index.tsx @@ -0,0 +1,25 @@ +import { getEngineCloudMethodUsage } from "@/api/analytics"; +import { LoadingChartState } from "components/analytics/empty-chart-state"; +import { Suspense } from "react"; +import type { AnalyticsQueryParams } from "types/analytics"; +import { EngineCloudBarChartCardUI } from "./EngineCloudBarChartCardUI"; + +export function EngineCloudChartCard(props: AnalyticsQueryParams) { + return ( + + + + } + > + + + ); +} + +async function EngineCloudChartCardAsync(props: AnalyticsQueryParams) { + const rawData = await getEngineCloudMethodUsage(props); + + return ; +} diff --git a/apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/(sidebar)/page.tsx b/apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/(sidebar)/page.tsx index 9b2f8d86afc..d603dd53e70 100644 --- a/apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/(sidebar)/page.tsx +++ b/apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/(sidebar)/page.tsx @@ -41,6 +41,7 @@ import { getAuthToken } from "../../../../api/lib/getAuthToken"; import { loginRedirect } from "../../../../login/loginRedirect"; import { CombinedBarChartCard } from "../../../components/Analytics/CombinedBarChartCard"; import { PieChartCard } from "../../../components/Analytics/PieChartCard"; +import { EngineCloudChartCard } from "./components/EngineCloudChartCard"; import { ProjectFTUX } from "./components/ProjectFTUX/ProjectFTUX"; import { RpcMethodBarChartCard } from "./components/RpcMethodBarChartCard"; import { TransactionsCharts } from "./components/Transactions"; @@ -279,6 +280,13 @@ async function ProjectAnalytics(props: { teamId={project.teamId} projectId={project.id} /> + ); } diff --git a/apps/dashboard/src/types/analytics.ts b/apps/dashboard/src/types/analytics.ts index ba149a8a296..895da7b95df 100644 --- a/apps/dashboard/src/types/analytics.ts +++ b/apps/dashboard/src/types/analytics.ts @@ -45,6 +45,13 @@ export interface RpcMethodStats { count: number; } +export interface EngineCloudStats { + date: string; + chainId: string; + pathname: string; + totalRequests: number; +} + export interface UniversalBridgeStats { date: string; chainId: number;