analytics overhaul (#4107)

<!-- start pr-codex -->

## PR-Codex overview
This PR enhances blockchain analytics in the dashboard. It introduces support for analytics on specific chains and updates chart components for better visualization.

### Detailed summary
- Added `CHAINSAW_API_KEY` environment variable for contract analytics
- Updated chart components for better visualization
- Improved analytics support for specific chains
- Refactored chart styling for better UI/UX

> The following files were skipped due to too many changes: `apps/dashboard/src/contract-ui/tabs/overview/components/Analytics.tsx`, `apps/dashboard/src/contract-ui/tabs/analytics/page.tsx`

> ✨ Ask PR-Codex anything about this PR by commenting with `/codex {your question}`

<!-- end pr-codex -->
This commit is contained in:
jnsdls
2024-08-15 15:29:50 +00:00
parent c9ac25fe3f
commit 997e1197e9
15 changed files with 580 additions and 411 deletions
+3
View File
@@ -40,6 +40,9 @@ NEXT_PUBLIC_STRIPE_KEY=
NEXT_PUBLIC_STRIPE_PAYMENT_METHOD_CFG_ID=
# Needed for contract analytics / blockchain data information
CHAINSAW_API_KEY=
#
# Private (server)
#
@@ -0,0 +1,109 @@
"use client";
import { Area, AreaChart, CartesianGrid, XAxis } from "recharts";
import {
Card,
CardContent,
CardDescription,
CardHeader,
CardTitle,
} from "@/components/ui/card";
import {
type ChartConfig,
ChartContainer,
ChartLegend,
ChartLegendContent,
ChartTooltip,
ChartTooltipContent,
} from "@/components/ui/chart";
import { useMemo } from "react";
export type ThirdwebAreaChartProps<TConfig extends ChartConfig> = {
// metadata
title: string;
description?: string;
// chart config
config: TConfig;
data: Array<Record<keyof TConfig, number> & { time: number | string | Date }>;
showLegend?: boolean;
// chart className
chartClassName?: string;
};
export function ThirdwebAreaChart<TConfig extends ChartConfig>(
props: ThirdwebAreaChartProps<TConfig>,
) {
const configKeys = useMemo(() => Object.keys(props.config), [props.config]);
return (
<Card>
<CardHeader>
<CardTitle>{props.title}</CardTitle>
{props.description && (
<CardDescription>{props.description}</CardDescription>
)}
</CardHeader>
<CardContent>
<ChartContainer config={props.config} className={props.chartClassName}>
<AreaChart
accessibilityLayer
data={props.data}
margin={{
left: 12,
right: 12,
}}
>
<CartesianGrid vertical={false} />
<XAxis
dataKey="time"
tickLine={false}
axisLine={false}
tickMargin={8}
tickFormatter={(value) => new Date(value).toLocaleDateString()}
/>
<ChartTooltip cursor={false} content={<ChartTooltipContent />} />
<defs>
{configKeys.map((key) => (
<linearGradient
key={key}
id={`fill_${key}`}
x1="0"
y1="0"
x2="0"
y2="1"
>
<stop
offset="5%"
stopColor={`var(--color-${key})`}
stopOpacity={0.8}
/>
<stop
offset="95%"
stopColor={`var(--color-${key})`}
stopOpacity={0.1}
/>
</linearGradient>
))}
</defs>
{configKeys.map((key) => (
<Area
key={key}
dataKey={key}
type="natural"
fill={`url(#fill_${key})`}
fillOpacity={0.4}
stroke={`var(--color-${key})`}
stackId="a"
/>
))}
{props.showLegend && (
<ChartLegend content={<ChartLegendContent />} />
)}
</AreaChart>
</ChartContainer>
</CardContent>
</Card>
);
}
@@ -0,0 +1,91 @@
"use client";
import { Bar, BarChart, CartesianGrid, XAxis } from "recharts";
import {
Card,
CardContent,
CardDescription,
CardHeader,
CardTitle,
} from "@/components/ui/card";
import {
type ChartConfig,
ChartContainer,
ChartLegend,
ChartLegendContent,
ChartTooltip,
ChartTooltipContent,
} from "@/components/ui/chart";
import { useMemo } from "react";
export type ThirdwebBarChartProps<TConfig extends ChartConfig> = {
// metadata
title: string;
description?: string;
// chart config
config: TConfig;
data: Array<Record<keyof TConfig, number> & { time: number | string | Date }>;
showLegend?: boolean;
variant?: "stacked" | "grouped";
// chart className
chartClassName?: string;
};
export function ThirdwebBarChart<TConfig extends ChartConfig>(
props: ThirdwebBarChartProps<TConfig>,
) {
const configKeys = useMemo(() => Object.keys(props.config), [props.config]);
// if there are more than 4 keys then we should stack them by default
const variant =
props.variant || configKeys.length > 4 ? "stacked" : "grouped";
return (
<Card>
<CardHeader>
<CardTitle>{props.title}</CardTitle>
{props.description && (
<CardDescription>{props.description}</CardDescription>
)}
</CardHeader>
<CardContent>
<ChartContainer config={props.config} className={props.chartClassName}>
<BarChart accessibilityLayer data={props.data}>
<CartesianGrid vertical={false} />
<XAxis
dataKey="time"
tickLine={false}
axisLine={false}
tickMargin={10}
tickFormatter={(value) => new Date(value).toLocaleDateString()}
/>
<ChartTooltip content={<ChartTooltipContent hideLabel />} />
{props.showLegend && (
<ChartLegend content={<ChartLegendContent />} />
)}
{configKeys.map((key, idx) => (
<Bar
key={key}
dataKey={key}
// if stacked then they should all be the same stackId
// if grouped then they should all be unique stackId (so the key works great)
stackId={variant === "stacked" ? "a" : key}
fill={`var(--color-${key})`}
// if stacked then we need to figure out the radius based on the index in the array
// if grouped then we can just use the same radius for all
radius={
variant === "stacked"
? idx === 0
? [0, 0, 4, 4]
: idx === configKeys.length - 1
? [4, 4, 0, 0]
: [0, 0, 0, 0]
: [4, 4, 4, 4]
}
/>
))}
</BarChart>
</ChartContainer>
</CardContent>
</Card>
);
}
+1 -1
View File
@@ -9,7 +9,7 @@ const Card = React.forwardRef<
<div
ref={ref}
className={cn(
"rounded-lg border bg-card text-card-foreground shadow-sm",
"rounded-lg border border-border bg-card text-card-foreground shadow-sm",
className,
)}
{...props}
+1 -1
View File
@@ -283,7 +283,7 @@ const ChartLegendContent = React.forwardRef<
<div
ref={ref}
className={cn(
"flex items-center justify-center gap-4",
"flex items-center justify-center gap-4 overflow-x-auto",
verticalAlign === "top" ? "pb-3" : "pt-3",
className,
)}
+11 -8
View File
@@ -94,19 +94,22 @@
/* shadcn charts */
@layer base {
:root {
--chart-1: 12 76% 61%;
--chart-2: 173 58% 39%;
--chart-3: 197 37% 24%;
--chart-4: 43 74% 66%;
--chart-5: 27 87% 67%;
}
.dark {
--chart-1: 220 70% 50%;
--chart-2: 160 60% 45%;
--chart-3: 30 80% 55%;
--chart-4: 280 65% 60%;
--chart-5: 340 75% 55%;
/* below is ai generated based on the initial */
--chart-6: 200 65% 50%;
--chart-7: 120 70% 45%;
--chart-8: 45 75% 55%;
--chart-9: 260 60% 60%;
--chart-10: 0 70% 50%;
--chart-11: 180 60% 50%;
--chart-12: 100 65% 55%;
--chart-13: 360 70% 55%;
--chart-14: 240 65% 50%;
--chart-15: 80 60% 50%;
}
}
@@ -129,13 +129,11 @@ export const BarChart = <
tickFormatter={(payload) =>
index.format
? index.format(payload)
: index.type === "date"
? new Date(payload).toLocaleDateString(undefined, {
day: "2-digit",
month: "2-digit",
year: "2-digit",
})
: payload
: new Date(payload).toLocaleDateString(undefined, {
day: "2-digit",
month: "2-digit",
year: "2-digit",
})
}
style={{
fontSize: "12px",
@@ -4,6 +4,7 @@ import { useEns } from "components/contract-components/hooks";
import { ContractOverviewPage } from "contract-ui/tabs/overview/page";
import type { EnhancedRoute } from "contract-ui/types/types";
import dynamic from "next/dynamic";
import { useAnalyticsSupportedForChain } from "../../data/analytics/hooks";
const LazyContractExplorerPage = dynamic(() =>
import("../tabs/explorer/page").then(
@@ -100,6 +101,10 @@ export function useContractRouteConfig(
const contractQuery = useContract(ensQuery.data?.address);
const contractTypeQuery = contractType.useQuery(contractAddress);
const analyticsSupported = useAnalyticsSupportedForChain(
contractQuery.contract?.chainId,
);
const claimconditionExtensionDetection = extensionDetectedState({
contractQuery,
feature: [
@@ -158,6 +163,11 @@ export function useContractRouteConfig(
component: LazyContractAnalyticsPage,
isDefault: true,
isBeta: true,
isEnabled: analyticsSupported.isLoading
? "loading"
: analyticsSupported.data
? "enabled"
: "disabled",
},
{
title: "NFTs",
@@ -9,23 +9,15 @@ import {
Flex,
SimpleGrid,
Skeleton,
Stack,
Stat,
StatLabel,
StatNumber,
} from "@chakra-ui/react";
import type { UseQueryResult } from "@tanstack/react-query";
import type {
AreaChartProps,
GenericDataType,
} from "components/analytics/area-chart";
import { AutoBarChart } from "components/analytics/auto-bar-chart";
import { BarChart } from "components/analytics/bar-chart";
import { ChartContainer } from "components/analytics/chart-container";
import {
type AnalyticsQueryParams,
type TotalQueryResult,
useAnalyticsSupportedChains,
useAnalyticsSupportedForChain,
useEventsAnalytics,
useFunctionsAnalytics,
useLogsAnalytics,
@@ -36,7 +28,8 @@ import {
useUniqueWalletsAnalytics,
} from "data/analytics/hooks";
import { Suspense, useMemo, useState } from "react";
import { Card, Heading, Text } from "tw-components";
import { Card, Heading } from "tw-components";
import { ThirdwebBarChart } from "../../../@/components/blocks/charts/bar-chart";
import { useIsomorphicLayoutEffect } from "../../../@/lib/useIsomorphicLayoutEffect";
interface ContractAnalyticsPageProps {
@@ -61,11 +54,32 @@ export const ContractAnalyticsPage: React.FC<ContractAnalyticsPageProps> = ({
const evmContractInfo = useEVMContractInfo();
if (!contractAddress) {
const analyticsSupported = useAnalyticsSupportedForChain(
evmContractInfo?.chain?.chainId,
);
if (
!contractAddress ||
!evmContractInfo?.chain?.chainId ||
analyticsSupported.isLoading
) {
// TODO build a skeleton for this
return <div>Loading...</div>;
}
if (!analyticsSupported.data) {
return (
<Alert status="warning" borderRadius="md" mb={4}>
<AlertIcon />
<AlertTitle>Analytics is not supported for this chain.</AlertTitle>
<AlertDescription>
Analytics support is rolling out to additional chains. Please check
back later.
</AlertDescription>
</Alert>
);
}
return (
<Flex direction="column" gap={6}>
{contractAddress && evmContractInfo?.chain && (
@@ -111,124 +125,40 @@ export const ContractAnalyticsPage: React.FC<ContractAnalyticsPageProps> = ({
</Flex>
</Flex>
<SimpleGrid columns={{ base: 1, md: 1 }} gap={4}>
<Flex flexDir="column" gap={4} as={Card} bg="backgroundHighlight">
<Stack spacing={0}>
<Heading as="h3" size="subtitle.sm">
Unique Wallets
</Heading>
<Text>
The number of unique wallet addresses that have sent a
transaction to this contract.
</Text>
</Stack>
<ChartContainer className="w-full" ratio={4.5 / 1}>
<AnalyticsChart
contractAddress={contractAddress}
chainId={evmContractInfo.chain.chainId}
startDate={startDate}
endDate={endDate}
index={"time"}
categories={[{ id: "wallets", label: "Unique Wallets" }]}
// FIXME
// eslint-disable-next-line react-compiler/react-compiler
useAnalytics={useUniqueWalletsAnalytics}
/>
</ChartContainer>
</Flex>
<Flex flexDir="column" gap={4} as={Card} bg="backgroundHighlight">
<Stack spacing={0}>
<Heading as="h3" size="subtitle.sm">
Total Transactions
</Heading>
<Text>
The number of transactions that have been sent to this
contract.
</Text>
</Stack>
<ChartContainer className="w-full" ratio={4.5 / 1}>
<AnalyticsChart
contractAddress={contractAddress}
chainId={evmContractInfo.chain.chainId}
startDate={startDate}
endDate={endDate}
index={"time"}
categories={[{ id: "count", label: "Transactions" }]}
// FIXME
// eslint-disable-next-line react-compiler/react-compiler
useAnalytics={useTransactionAnalytics}
/>
</ChartContainer>
</Flex>
<UniqueWalletsChart
chainId={evmContractInfo.chain.chainId}
contractAddress={contractAddress}
startDate={startDate}
endDate={endDate}
/>
<Flex flexDir="column" gap={4} as={Card} bg="backgroundHighlight">
<Stack spacing={0}>
<Heading as="h3" size="subtitle.sm">
Total Events
</Heading>
<Text>
The number of on-chain events that have been emitted from this
contract.
</Text>
</Stack>
<ChartContainer className="w-full" ratio={4.5 / 1}>
<AnalyticsChart
contractAddress={contractAddress}
chainId={evmContractInfo.chain.chainId}
startDate={startDate}
endDate={endDate}
index={"time"}
categories={[{ id: "count", label: "Events" }]}
// FIXME
// eslint-disable-next-line react-compiler/react-compiler
useAnalytics={useLogsAnalytics}
/>
</ChartContainer>
</Flex>
<Flex flexDir="column" gap={4} as={Card} bg="backgroundHighlight">
<Stack spacing={0}>
<Heading as="h3" size="subtitle.sm">
Function Breakdown
</Heading>
<Text>
The breakdown of calls to each write function from
transactions.
</Text>
</Stack>
<ChartContainer className="w-full" ratio={4.5 / 1}>
<AnalyticsChart
contractAddress={contractAddress}
chainId={evmContractInfo.chain.chainId}
startDate={startDate}
endDate={endDate}
index={"time"}
categories={"auto"}
// FIXME
// eslint-disable-next-line react-compiler/react-compiler
useAnalytics={useFunctionsAnalytics}
/>
</ChartContainer>
</Flex>
<Flex flexDir="column" gap={4} as={Card} bg="backgroundHighlight">
<Stack spacing={0}>
<Heading as="h3" size="subtitle.sm">
Event Breakdown
</Heading>
<Text>The breakdown of events emitted by this contract.</Text>
</Stack>
<ChartContainer className="w-full" ratio={4.5 / 1}>
<AnalyticsChart
contractAddress={contractAddress}
chainId={evmContractInfo.chain.chainId}
startDate={startDate}
endDate={endDate}
index={"time"}
categories={"auto"}
// FIXME
// eslint-disable-next-line react-compiler/react-compiler
useAnalytics={useEventsAnalytics}
/>
</ChartContainer>
</Flex>
<TotalTransactionsChart
chainId={evmContractInfo.chain.chainId}
contractAddress={contractAddress}
startDate={startDate}
endDate={endDate}
/>
<TotalEventsChart
chainId={evmContractInfo.chain.chainId}
contractAddress={contractAddress}
startDate={startDate}
endDate={endDate}
/>
<FunctionBreakdownChart
chainId={evmContractInfo.chain.chainId}
contractAddress={contractAddress}
startDate={startDate}
endDate={endDate}
/>
<EventBreakdownChart
chainId={evmContractInfo.chain.chainId}
contractAddress={contractAddress}
startDate={startDate}
endDate={endDate}
/>
</SimpleGrid>
</>
)}
@@ -236,87 +166,127 @@ export const ContractAnalyticsPage: React.FC<ContractAnalyticsPageProps> = ({
);
};
interface AnalyticsChartProps<
TAnalytics extends GenericDataType = GenericDataType,
> {
chainId: number;
type ChartProps = {
contractAddress: string;
chainId: number;
startDate: Date;
endDate: Date;
index: string;
categories: AreaChartProps<TAnalytics, "time">["categories"] | "auto";
useAnalytics: (params: AnalyticsQueryParams) => UseQueryResult<TAnalytics[]>;
showXAxis?: boolean;
showYAxis?: boolean;
}
export const AnalyticsChart: React.FC<AnalyticsChartProps> = ({
chainId,
contractAddress,
startDate,
endDate,
index,
categories,
useAnalytics,
showXAxis,
showYAxis,
}) => {
const supportedChainsQuery = useAnalyticsSupportedChains();
const analyticsQuery = useAnalytics({
contractAddress,
chainId,
startDate,
endDate,
});
const data = useMemo(() => {
if (!analyticsQuery.data) {
return [];
}
return analyticsQuery.data;
}, [analyticsQuery.data]);
if (data.length <= 1) {
const supportedChains = supportedChainsQuery.data ?? [];
return (
<Alert status="info" borderRadius="md" mb={4}>
<AlertIcon />
{supportedChains.includes(chainId) ? (
<AlertDescription>No recent activity.</AlertDescription>
) : (
<AlertDescription>
Analytics for this chain not currently supported.
</AlertDescription>
)}
</Alert>
);
}
if (categories === "auto") {
return (
<AutoBarChart
data={data}
index={{ id: index }}
showXAxis
showYAxis
stacked
/>
);
}
};
function UniqueWalletsChart(props: ChartProps) {
const { data } = useUniqueWalletsAnalytics(props);
return (
<BarChart
data={data}
index={{ id: index }}
categories={categories}
showXAxis={showXAxis !== undefined ? showXAxis : true}
showYAxis={showYAxis !== undefined ? showYAxis : true}
<ThirdwebBarChart
title="Unique Wallets"
description="The number of unique wallet addresses that have sent a transaction to this contract."
data={data || []}
config={{
wallets: {
label: "Unique Wallets",
color: "hsl(var(--chart-1))",
},
}}
chartClassName="aspect[2] lg:aspect-[4.5]"
/>
);
};
}
function TotalTransactionsChart(props: ChartProps) {
const { data } = useTransactionAnalytics({
chainId: props.chainId,
contractAddress: props.contractAddress,
endDate: props.endDate,
startDate: props.startDate,
});
return (
<ThirdwebBarChart
title="Total Transactions"
description="The number of transactions that have been sent to this contract."
data={data || []}
config={{
count: {
label: "Transactions",
color: "hsl(var(--chart-1))",
},
}}
chartClassName="aspect[2] lg:aspect-[4.5]"
/>
);
}
function TotalEventsChart(props: ChartProps) {
const { data } = useLogsAnalytics(props);
return (
<ThirdwebBarChart
title="Total Events"
description="The number of on-chain events that have been emitted from this contract."
data={data || []}
config={{
count: {
label: "Events",
color: "hsl(var(--chart-1))",
},
}}
chartClassName="aspect[2] lg:aspect-[4.5]"
/>
);
}
function FunctionBreakdownChart(props: ChartProps) {
const { data } = useFunctionsAnalytics(props);
return (
<ThirdwebBarChart
title="Function Breakdown"
description="The breakdown of calls to each write function from transactions."
data={data || []}
config={Object.keys(data?.[0] || {}).reduce(
(acc, key) => {
if (key === "time") {
return acc;
}
acc[key] = {
label: key,
color: `hsl(var(--chart-${(Object.keys(acc).length % 15) + 1}))`,
};
return acc;
},
{} as Record<string, { label: string; color: string }>,
)}
chartClassName="aspect[2] lg:aspect-[4.5]"
showLegend
/>
);
}
function EventBreakdownChart(props: ChartProps) {
const { data } = useEventsAnalytics(props);
return (
<ThirdwebBarChart
title="Event Breakdown"
description="The breakdown of events emitted by this contract."
data={data || []}
config={Object.keys(data?.[0] || {}).reduce(
(acc, key) => {
if (key === "time") {
return acc;
}
acc[key] = {
label: key,
color: `hsl(var(--chart-${(Object.keys(acc).length % 15) + 1}))`,
};
return acc;
},
{} as Record<string, { label: string; color: string }>,
)}
chartClassName="aspect[2] lg:aspect-[4.5]"
showLegend
/>
);
}
interface AnalyticsStatProps {
label: string;
@@ -1,25 +1,21 @@
import { Flex, GridItem, Icon, SimpleGrid, Tooltip } from "@chakra-ui/react";
import { ChartContainer } from "components/analytics/chart-container";
import { AnalyticsChart } from "contract-ui/tabs/analytics/page";
import { ThirdwebAreaChart } from "@/components/blocks/charts/area-chart";
import { Button } from "@/components/ui/button";
import { useTabHref } from "contract-ui/utils";
import {
useAnalyticsSupportedForChain,
useLogsAnalytics,
useTransactionAnalytics,
useUniqueWalletsAnalytics,
} from "data/analytics/hooks";
import { useState } from "react";
import { FiInfo } from "react-icons/fi";
import {
Card,
Heading,
TrackedLink,
type TrackedLinkProps,
} from "tw-components";
import { useTrack } from "hooks/analytics/useTrack";
import { ArrowRightIcon } from "lucide-react";
import Link from "next/link";
import { useMemo, useState } from "react";
interface AnalyticsOverviewProps {
chainId: number;
contractAddress: string;
trackingCategory: TrackedLinkProps["category"];
trackingCategory: string;
}
export const AnalyticsOverview: React.FC<AnalyticsOverviewProps> = ({
@@ -27,10 +23,13 @@ export const AnalyticsOverview: React.FC<AnalyticsOverviewProps> = ({
contractAddress,
trackingCategory,
}) => {
const analyticsSupported = useAnalyticsSupportedForChain(chainId);
const trackEvent = useTrack();
const [startDate] = useState(
(() => {
const date = new Date();
date.setDate(date.getDate() - 11);
date.setDate(date.getDate() - 14);
return date;
})(),
);
@@ -38,152 +37,94 @@ export const AnalyticsOverview: React.FC<AnalyticsOverviewProps> = ({
const analyticsHref = useTabHref("analytics");
return (
<Flex direction="column" gap={{ base: 3, md: 6 }}>
<Flex align="center" justify="space-between" w="full">
<Heading size="title.sm">Analytics</Heading>
<TrackedLink
category={trackingCategory}
label="view_all_nfts"
color="blue.400"
_light={{
color: "blue.600",
}}
gap={4}
href={analyticsHref}
>
View all -&gt;
</TrackedLink>
</Flex>
if (!analyticsSupported.data) {
return null;
}
<SimpleGrid gap={{ base: 3, md: 6 }} columns={{ base: 1, md: 3 }}>
<GridItem>
<Flex
flexDir="column"
gap={4}
as={Card}
bg="backgroundHighlight"
h="full"
>
<Flex align="center" justify="space-between">
<Heading size="label.md">Unique Wallets</Heading>
<Tooltip
p={0}
label={
<Flex p={2} fontSize="small" color="white">
The number of unique wallet addresses that have sent a
transaction to this contract.
</Flex>
}
bgColor="backgroundCardHighlight"
borderRadius="lg"
placement="right"
shouldWrapChildren
>
<Icon as={FiInfo} boxSize={4} color="gray.700" />
</Tooltip>
</Flex>
<ChartContainer className="w-full" ratio={1.7}>
<AnalyticsChart
contractAddress={contractAddress}
chainId={chainId}
startDate={startDate}
endDate={endDate}
index={"time"}
categories={[{ id: "wallets", label: "Unique Wallets" }]}
// FIXME
// eslint-disable-next-line react-compiler/react-compiler
useAnalytics={useUniqueWalletsAnalytics}
showYAxis={false}
/>
</ChartContainer>
</Flex>
</GridItem>
<GridItem>
<Flex
flexDir="column"
gap={4}
as={Card}
bg="backgroundHighlight"
h="full"
>
<Flex align="center" justify="space-between">
<Heading size="label.md">Total Transactions</Heading>
<Tooltip
p={0}
label={
<Flex p={2} fontSize="small" color="white">
The number of transactions that have been sent to this
contract.
</Flex>
}
bgColor="backgroundCardHighlight"
borderRadius="lg"
placement="right"
shouldWrapChildren
>
<Icon as={FiInfo} boxSize={4} color="gray.700" />
</Tooltip>
</Flex>
<ChartContainer className="w-full" ratio={1.7}>
<AnalyticsChart
contractAddress={contractAddress}
chainId={chainId}
startDate={startDate}
endDate={endDate}
index={"time"}
categories={[{ id: "count", label: "Transactions" }]}
// FIXME
// eslint-disable-next-line react-compiler/react-compiler
useAnalytics={useTransactionAnalytics}
showYAxis={false}
/>
</ChartContainer>
</Flex>
</GridItem>
<GridItem>
<Flex
flexDir="column"
gap={4}
as={Card}
bg="backgroundHighlight"
h="full"
>
<Flex align="center" justify="space-between">
<Heading size="label.md">Total Events</Heading>
<Tooltip
p={0}
label={
<Flex p={2} fontSize="small" color="white">
The number of on-chain events that have been emitted from
this contract.
</Flex>
}
bgColor="backgroundCardHighlight"
borderRadius="lg"
placement="right"
shouldWrapChildren
>
<Icon as={FiInfo} boxSize={4} color="gray.700" />
</Tooltip>
</Flex>
<ChartContainer className="w-full" ratio={1.7}>
<AnalyticsChart
contractAddress={contractAddress}
chainId={chainId}
startDate={startDate}
endDate={endDate}
index={"time"}
categories={[{ id: "count", label: "Events" }]}
// FIXME
// eslint-disable-next-line react-compiler/react-compiler
useAnalytics={useLogsAnalytics}
showYAxis={false}
/>
</ChartContainer>
</Flex>
</GridItem>
</SimpleGrid>
</Flex>
return (
<div className="relative">
<Button
asChild
className="absolute top-4 right-6 flex flex-row gap-1 items-center"
size="sm"
variant="outline"
onClick={() => {
trackEvent({
category: trackingCategory,
action: "click",
label: "view_all_analytics",
});
}}
>
<Link href={analyticsHref}>
<span>View All</span>
<ArrowRightIcon className="size-4" />
</Link>
</Button>
<OverviewAnalytics
chainId={chainId}
contractAddress={contractAddress}
endDate={endDate}
startDate={startDate}
/>
</div>
);
};
type ChartProps = {
contractAddress: string;
chainId: number;
startDate: Date;
endDate: Date;
};
function OverviewAnalytics(props: ChartProps) {
const wallets = useUniqueWalletsAnalytics(props);
const transactions = useTransactionAnalytics(props);
const events = useLogsAnalytics(props);
const mergedData = useMemo(() => {
const time = (wallets.data || transactions.data || events.data || []).map(
(wallet) => wallet.time,
);
return time.map((time) => {
const wallet = wallets.data?.find((wallet) => wallet.time === time);
const transaction = transactions.data?.find(
(transaction) => transaction.time === time,
);
const event = events.data?.find((event) => event.time === time);
return {
time,
wallets: wallet?.wallets || 0,
transactions: transaction?.count || 0,
events: event?.count || 0,
};
});
}, [wallets.data, transactions.data, events.data]);
return (
<ThirdwebAreaChart
title="Analytics"
config={{
wallets: {
label: "Unique Wallets",
color: "hsl(var(--chart-1))",
},
transactions: {
label: "Total Transactions",
color: "hsl(var(--chart-2))",
},
events: {
label: "Total Events",
color: "hsl(var(--chart-3))",
},
}}
data={mergedData}
showLegend
chartClassName="aspect[2] lg:aspect-[4.5]"
/>
);
}
@@ -65,6 +65,7 @@ export const ContractSidebar: React.FC<ContractSidebarProps> = ({
href: `/${r.path.replace("overview", "")}`,
isBeta: r.isBeta,
isDeprecated: r.isDeprecated,
extensionDetectedState: r.isEnabled,
onClick: () => {
openState.onClose();
},
@@ -131,25 +132,25 @@ const NavLinkSection: React.FC<NavLinkSectionProps> = ({
</Text>
</Flex>
{filteredLinks.map((link) => (
<Flex gap={2} key={link.href}>
<DetailNavLink key={link.href} {...link}>
<DetailNavLink key={link.href} {...link}>
<Flex gap={2} as="span">
{link.title}
</DetailNavLink>
{link.isBeta && (
<Box>
<Badge colorScheme="green" variant="subtle">
Beta
</Badge>
</Box>
)}
{link.isDeprecated && (
<Box>
<Badge colorScheme="orange" variant="subtle">
Deprecated
</Badge>
</Box>
)}
</Flex>
{link.isBeta && (
<Box>
<Badge colorScheme="green" variant="subtle">
Beta
</Badge>
</Box>
)}
{link.isDeprecated && (
<Box>
<Badge colorScheme="orange" variant="subtle">
Deprecated
</Badge>
</Box>
)}
</Flex>
</DetailNavLink>
))}
</Flex>
);
@@ -1 +0,0 @@
export const THIRDWEB_ANALYTICS_API_HOSTNAME = "https://chainsaw.thirdweb.com";
+19 -13
View File
@@ -1,5 +1,4 @@
import { useQuery } from "@tanstack/react-query";
import { THIRDWEB_ANALYTICS_API_HOSTNAME } from "./constants";
export type AnalyticsQueryParams = {
chainId: number;
@@ -17,7 +16,7 @@ async function makeQuery(
.filter(([, value]) => !!value)
.map(([key, value]) => `${key}=${value}`)
.join("&")}`;
return fetch(`${THIRDWEB_ANALYTICS_API_HOSTNAME}${path}${queryString}`, {
return fetch(`/api/server-proxy/chainsaw/${path}${queryString}`, {
method: "GET",
});
}
@@ -374,17 +373,24 @@ export function useTotalWalletsAnalytics(params: AnalyticsQueryParams) {
});
}
export function useAnalyticsSupportedChains() {
return useQuery(
["analytics-supported-chains"] as const,
async (): Promise<number[]> => {
const res = await makeQuery("/api/v1/supported-chains", {});
if (!res.ok) {
throw new Error(`Unexpected status ${res.status}`);
}
export function useAnalyticsSupportedForChain(chainId: number | undefined) {
return useQuery({
queryKey: ["analytics-supported-chains", chainId] as const,
queryFn: async (): Promise<boolean> => {
try {
const res = await makeQuery(`/service/chains/${chainId}`, {});
if (!res.ok) {
// assume not supported if we get a non-200 response
return false;
}
const { results } = await res.json();
return results;
const { data } = await res.json();
return data;
} catch (e) {
console.error("Error checking if analytics is supported for chain", e);
return false;
}
},
);
enabled: !!chainId,
});
}
@@ -0,0 +1,39 @@
import type { NextRequest } from "next/server";
export const config = {
runtime: "edge",
};
const handler = async (req: NextRequest) => {
if (!process.env.CHAINSAW_API_KEY) {
return new Response("CHAINSAW_API_KEY not set", { status: 401 });
}
// get the full path from the request including query params
let pathname = req.nextUrl.pathname;
// remove the /api/server-proxy prefix
pathname = pathname.replace(/^\/api\/server-proxy\/chainsaw/, "");
const searchParams = req.nextUrl.searchParams;
searchParams.delete("paths");
// create a new URL object for the chainsaw service
// TODO: this should really point at dev vs prod based on the environment - but for now prod only will have to suffice
const CHAINSAW_URL = new URL("https://chainsaw.thirdweb.com");
CHAINSAW_URL.pathname = pathname;
searchParams.forEach((value, key) => {
CHAINSAW_URL.searchParams.append(key, value);
});
return fetch(CHAINSAW_URL, {
method: req.method,
headers: {
"content-type": "application/json",
// pass the shared secret
"x-service-api-key": process.env.CHAINSAW_API_KEY,
},
body: req.body,
});
};
export default handler;
Submodule apps/hardhat-boilerplate deleted from 638fd58ec8