Skip to content

Commit c011ee5

Browse files
committed
add analytics to insight page
1 parent c2ef5eb commit c011ee5

File tree

11 files changed

+925
-187
lines changed

11 files changed

+925
-187
lines changed

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

Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,10 @@ import type {
66
EcosystemWalletStats,
77
EngineCloudStats,
88
InAppWalletStats,
9+
InsightChainStats,
10+
InsightEndpointStats,
11+
InsightStatusCodeStats,
12+
InsightUsageStats,
913
RpcMethodStats,
1014
TransactionStats,
1115
UniversalBridgeStats,
@@ -424,3 +428,83 @@ export async function getEngineCloudMethodUsage(
424428
const json = await res.json();
425429
return json.data as EngineCloudStats[];
426430
}
431+
432+
export async function getInsightChainUsage(
433+
params: AnalyticsQueryParams,
434+
): Promise<InsightChainStats[]> {
435+
const searchParams = buildSearchParams(params);
436+
const res = await fetchAnalytics(
437+
`v2/insight/usage/by-chain?${searchParams.toString()}`,
438+
{
439+
method: "GET",
440+
},
441+
);
442+
443+
if (res?.status !== 200) {
444+
console.error("Failed to fetch Insight chain usage");
445+
return [];
446+
}
447+
448+
const json = await res.json();
449+
return json.data as InsightChainStats[];
450+
}
451+
452+
export async function getInsightStatusCodeUsage(
453+
params: AnalyticsQueryParams,
454+
): Promise<InsightStatusCodeStats[]> {
455+
const searchParams = buildSearchParams(params);
456+
const res = await fetchAnalytics(
457+
`v2/insight/usage/by-status-code?${searchParams.toString()}`,
458+
{
459+
method: "GET",
460+
},
461+
);
462+
463+
if (res?.status !== 200) {
464+
console.error("Failed to fetch Insight status code usage");
465+
return [];
466+
}
467+
468+
const json = await res.json();
469+
return json.data as InsightStatusCodeStats[];
470+
}
471+
472+
export async function getInsightEndpointUsage(
473+
params: AnalyticsQueryParams,
474+
): Promise<InsightEndpointStats[]> {
475+
const searchParams = buildSearchParams(params);
476+
const res = await fetchAnalytics(
477+
`v2/insight/usage/by-endpoint?${searchParams.toString()}`,
478+
{
479+
method: "GET",
480+
},
481+
);
482+
483+
if (res?.status !== 200) {
484+
console.error("Failed to fetch Insight endpoint usage");
485+
return [];
486+
}
487+
488+
const json = await res.json();
489+
return json.data as InsightEndpointStats[];
490+
}
491+
492+
export async function getInsightUsage(
493+
params: AnalyticsQueryParams,
494+
): Promise<InsightUsageStats[]> {
495+
const searchParams = buildSearchParams(params);
496+
const res = await fetchAnalytics(
497+
`v2/insight/usage?${searchParams.toString()}`,
498+
{
499+
method: "GET",
500+
},
501+
);
502+
503+
if (res?.status !== 200) {
504+
console.error("Failed to fetch Insight usage");
505+
return [];
506+
}
507+
508+
const json = await res.json();
509+
return json.data as InsightUsageStats[];
510+
}

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

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -78,4 +78,28 @@ export interface AnalyticsQueryParams {
7878
from?: Date;
7979
to?: Date;
8080
period?: "day" | "week" | "month" | "year" | "all";
81+
limit?: number;
82+
}
83+
84+
export interface InsightChainStats {
85+
date: string;
86+
chainId: string;
87+
totalRequests: number;
88+
}
89+
90+
export interface InsightStatusCodeStats {
91+
date: string;
92+
httpStatusCode: number;
93+
totalRequests: number;
94+
}
95+
96+
export interface InsightEndpointStats {
97+
date: string;
98+
endpoint: string;
99+
totalRequests: number;
100+
}
101+
102+
export interface InsightUsageStats {
103+
date: string;
104+
totalRequests: number;
81105
}

apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/(sidebar)/insight/blueprint-card.tsx

Lines changed: 0 additions & 97 deletions
This file was deleted.
Lines changed: 172 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,172 @@
1+
import { ActivityIcon, TrendingDownIcon } from "lucide-react";
2+
import { ResponsiveSuspense } from "responsive-rsc";
3+
import type { ThirdwebClient } from "thirdweb";
4+
import {
5+
getInsightChainUsage,
6+
getInsightEndpointUsage,
7+
getInsightStatusCodeUsage,
8+
getInsightUsage,
9+
} from "@/api/analytics";
10+
import type { Range } from "@/components/analytics/date-range-selector";
11+
import { StatCard } from "@/components/analytics/stat";
12+
import { Skeleton } from "@/components/ui/skeleton";
13+
import { InsightAnalyticsFilter } from "./InsightAnalyticsFilter";
14+
import { InsightFTUX } from "./insight-ftux";
15+
import { RequestsByStatusGraph } from "./RequestsByStatusGraph";
16+
import { TopInsightChainsTable } from "./TopChainsTable";
17+
import { TopInsightEndpointsTable } from "./TopEndpointsTable";
18+
19+
export async function InsightAnalytics(props: {
20+
projectClientId: string;
21+
client: ThirdwebClient;
22+
projectId: string;
23+
teamId: string;
24+
range: Range;
25+
interval: "day" | "week";
26+
}) {
27+
const { projectId, teamId, range, interval } = props;
28+
29+
const allTimeRequestsPromise = getInsightUsage({
30+
from: range.from,
31+
period: "all",
32+
projectId: projectId,
33+
teamId: teamId,
34+
to: range.to,
35+
});
36+
const chainsDataPromise = getInsightChainUsage({
37+
from: range.from,
38+
limit: 10,
39+
period: "all",
40+
projectId: projectId,
41+
teamId: teamId,
42+
to: range.to,
43+
}).catch((error) => {
44+
console.error(error);
45+
return [];
46+
});
47+
const statusCodesDataPromise = getInsightStatusCodeUsage({
48+
from: range.from,
49+
period: interval,
50+
projectId: projectId,
51+
teamId: teamId,
52+
to: range.to,
53+
}).catch((error) => {
54+
console.error(error);
55+
return [];
56+
});
57+
const endpointsDataPromise = getInsightEndpointUsage({
58+
from: range.from,
59+
limit: 10,
60+
period: "all",
61+
projectId: projectId,
62+
teamId: teamId,
63+
to: range.to,
64+
}).catch((error) => {
65+
console.error(error);
66+
return [];
67+
});
68+
69+
const [allTimeRequestsData, statusCodesData, endpointsData, chainsData] =
70+
await Promise.all([
71+
allTimeRequestsPromise,
72+
statusCodesDataPromise,
73+
endpointsDataPromise,
74+
chainsDataPromise,
75+
]);
76+
77+
const hasVolume = allTimeRequestsData.some((d) => d.totalRequests > 0);
78+
79+
const allTimeRequests =
80+
allTimeRequestsData?.reduce((acc, curr) => acc + curr.totalRequests, 0) ||
81+
0;
82+
let requestsInPeriod = 0;
83+
let errorsInPeriod = 0;
84+
for (const request of statusCodesData) {
85+
requestsInPeriod += request.totalRequests;
86+
if (request.httpStatusCode >= 400) {
87+
errorsInPeriod += request.totalRequests;
88+
}
89+
}
90+
const errorRate = Number(
91+
((errorsInPeriod / (requestsInPeriod || 1)) * 100).toFixed(2),
92+
);
93+
94+
if (!hasVolume) {
95+
return (
96+
<div className="container flex max-w-7xl grow flex-col">
97+
<InsightFTUX clientId={props.projectClientId} />
98+
</div>
99+
);
100+
}
101+
102+
return (
103+
<div>
104+
<div className="mb-4 flex justify-start">
105+
<InsightAnalyticsFilter />
106+
</div>
107+
<ResponsiveSuspense
108+
fallback={
109+
<div className="flex flex-col gap-6">
110+
<Skeleton className="h-[350px] border rounded-xl" />
111+
<div className="grid grid-cols-1 gap-6 xl:grid-cols-2">
112+
<Skeleton className="h-[350px] border rounded-xl" />
113+
<Skeleton className="h-[350px] border rounded-xl" />
114+
</div>
115+
<div className="grid grid-cols-1 gap-6 xl:grid-cols-2">
116+
<Skeleton className="h-[350px] border rounded-xl" />
117+
<Skeleton className="h-[350px] border rounded-xl" />
118+
</div>
119+
<Skeleton className="h-[500px] border rounded-xl" />
120+
</div>
121+
}
122+
searchParamsUsed={["from", "to", "interval"]}
123+
>
124+
<div className="flex flex-col gap-10 lg:gap-6">
125+
<div className="grid grid-cols-2 gap-4 lg:gap-6">
126+
<StatCard
127+
icon={ActivityIcon}
128+
isPending={false}
129+
label="Total Requests"
130+
value={allTimeRequests}
131+
/>
132+
<StatCard
133+
formatter={(value) => `${value}%`}
134+
icon={TrendingDownIcon}
135+
isPending={false}
136+
label="Error rate"
137+
value={errorRate}
138+
/>
139+
</div>
140+
<RequestsByStatusGraph
141+
data={statusCodesData}
142+
description="The number of requests by status code over time."
143+
isPending={false}
144+
title="Requests by Status Code"
145+
/>
146+
<GridWithSeparator>
147+
<div className="border-border border-b pb-6 xl:border-none xl:pb-0">
148+
<TopInsightEndpointsTable
149+
client={props.client}
150+
data={endpointsData || []}
151+
/>
152+
</div>
153+
<TopInsightChainsTable
154+
client={props.client}
155+
data={chainsData || []}
156+
/>
157+
</GridWithSeparator>
158+
</div>
159+
</ResponsiveSuspense>
160+
</div>
161+
);
162+
}
163+
164+
function GridWithSeparator(props: { children: React.ReactNode }) {
165+
return (
166+
<div className="relative grid grid-cols-1 gap-6 rounded-xl border border-border bg-card p-4 lg:gap-12 xl:grid-cols-2 xl:p-6">
167+
{props.children}
168+
{/* Desktop - horizontal middle */}
169+
<div className="absolute top-6 bottom-6 left-[50%] hidden w-[1px] bg-border xl:block" />
170+
</div>
171+
);
172+
}

0 commit comments

Comments
 (0)