From 342de4007a63f5e452caa0924a65d98f962d014d Mon Sep 17 00:00:00 2001 From: jipstavenuiter Date: Wed, 16 Sep 2026 13:02:21 +0700 Subject: [PATCH 1/2] feat: instrument the delegation funnel with Vercel Web Analytics Adds custom events for the delegation funnel (nav click, page views, wallet connect, form start, transaction submit/confirm/fail) per the instrumentation plan in #722, so delegator UX changes can be measured against a baseline instead of guessed at. Co-Authored-By: Claude Sonnet 5 --- components/DelegatingWidget/Delegate.tsx | 1 + components/DelegatingWidget/index.tsx | 18 ++- components/Drawer/index.tsx | 1 + hooks/useExplorerStore.tsx | 1 + hooks/useHandleTransaction.tsx | 4 + layouts/main.tsx | 21 +++ lib/analytics.ts | 148 +++++++++++++++++++++ package.json | 1 + pages/_app.tsx | 4 + pages/accounts/[account]/delegating.tsx | 21 +++ pages/accounts/[account]/orchestrating.tsx | 15 +++ pages/orchestrators.tsx | 7 + pnpm-lock.yaml | 45 ++++++- 13 files changed, 282 insertions(+), 5 deletions(-) create mode 100644 lib/analytics.ts diff --git a/components/DelegatingWidget/Delegate.tsx b/components/DelegatingWidget/Delegate.tsx index 272f3b44..536d22ee 100644 --- a/components/DelegatingWidget/Delegate.tsx +++ b/components/DelegatingWidget/Delegate.tsx @@ -127,6 +127,7 @@ const Delegate = ({ const bondWithHintArgs = { amount: amount?.toString() ? parseEther(amount) : BigInt(0), to, + isTransferStake, oldDelegateNewPosPrev, oldDelegateNewPosNext, currDelegateNewPosPrev, diff --git a/components/DelegatingWidget/index.tsx b/components/DelegatingWidget/index.tsx index a2988ed9..02896afc 100644 --- a/components/DelegatingWidget/index.tsx +++ b/components/DelegatingWidget/index.tsx @@ -1,3 +1,4 @@ +import { trackVercelAnalyticsEvent } from "@lib/analytics"; import { EnsIdentity } from "@lib/api/types/get-ens"; import { Box, Card, Flex, Text } from "@livepeer/design-system"; import { formatLPT } from "@utils/numberFormatters"; @@ -9,7 +10,7 @@ import { useIsWrongRouteChain, usePendingFeesAndStakeData, } from "hooks"; -import { useMemo, useState } from "react"; +import { useEffect, useMemo, useRef, useState } from "react"; import ArrowDown from "../../public/img/arrow-down.svg"; import Footer from "./Footer"; @@ -72,6 +73,21 @@ const Index = ({ fromWei(pendingFeesAndStake?.pendingStake ?? "0") ); + // Fire once, the first time the form goes from clean to dirty - not again + // on every subsequent clear/refill of the amount. Covers both typed + // amounts and the "max" shortcut. + const hasTrackedFormStart = useRef(false); + useEffect(() => { + if ( + !hasTrackedFormStart.current && + selectedStakingAction === "delegate" && + parseFloat(amount) > 0 + ) { + hasTrackedFormStart.current = true; + trackVercelAnalyticsEvent("delegation_form_started"); + } + }, [amount, selectedStakingAction]); + return ( { if (data) { setLatestTransactionDetails(data, id, args); + trackTransactionEvent(id, "submitted", args); if (onSuccess) { onSuccess(data); @@ -56,6 +58,7 @@ export const useHandleTransaction = ( useEffect(() => { if (isSuccess) { setLatestTransactionConfirmed(); + trackTransactionEvent(id, "confirmed", args); } // eslint-disable-next-line react-hooks/exhaustive-deps }, [isSuccess]); @@ -64,6 +67,7 @@ export const useHandleTransaction = ( if (error) { console.error(error); setLatestTransactionError(error.message.replace("GraphQL error: ", "")); + trackTransactionEvent(id, "failed", args, error); } // eslint-disable-next-line react-hooks/exhaustive-deps }, [error]); diff --git a/layouts/main.tsx b/layouts/main.tsx index 2e404ad4..7b4ad726 100644 --- a/layouts/main.tsx +++ b/layouts/main.tsx @@ -10,6 +10,10 @@ import { SnackbarProvider } from "@components/Snackbar"; import TxStartedDialog from "@components/TxStartedDialog"; import TxSummaryDialog from "@components/TxSummaryDialog"; import URLVerificationBanner from "@components/URLVerificationBanner"; +import { + trackVercelAnalyticsEvent, + trackWalletConnected, +} from "@lib/analytics"; import { IS_L2 } from "@lib/chains"; import { globalStyles } from "@lib/globalStyles"; import { @@ -63,6 +67,7 @@ import { FiInfo } from "react-icons/fi"; import { LuRadioTower } from "react-icons/lu"; import { useWindowSize } from "react-use"; import { Chain } from "viem"; +import { useAccountEffect } from "wagmi"; import { useAccountAddress, @@ -107,6 +112,7 @@ export type DrawerItem = { as: string; icon: React.ElementType; className?: string; + onClick?: () => void; }; const DesignSystemProviderTyped = DesignSystemProvider as React.FC<{ @@ -265,6 +271,15 @@ const Layout = ({ children, title = "Livepeer Explorer" }) => { ReactGA.pageview(window.location.pathname + window.location.search); }, []); + useAccountEffect({ + onConnect: ({ isReconnected }) => { + // Reconnects restore a previous session, so they don't count. + if (!isReconnected) { + trackWalletConnected(asPath); + } + }, + }); + const items: DrawerItem[] = [ { name: "Overview", @@ -279,6 +294,7 @@ const Layout = ({ children, title = "Livepeer Explorer" }) => { as: "/orchestrators", icon: DNS, className: "orchestrators", + onClick: () => trackVercelAnalyticsEvent("orchestrators_nav_clicked"), }, { name: "Gateways", @@ -548,6 +564,11 @@ const Layout = ({ children, title = "Livepeer Explorer" }) => {