|
| 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