Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions components/DelegatingWidget/Delegate.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -127,6 +127,7 @@ const Delegate = ({
const bondWithHintArgs = {
amount: amount?.toString() ? parseEther(amount) : BigInt(0),
to,
isTransferStake,
oldDelegateNewPosPrev,
oldDelegateNewPosNext,
currDelegateNewPosPrev,
Expand Down
18 changes: 17 additions & 1 deletion components/DelegatingWidget/index.tsx
Original file line number Diff line number Diff line change
@@ -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";
Expand All @@ -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";
Expand Down Expand Up @@ -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 (
<Box
css={{
Expand Down
1 change: 1 addition & 0 deletions components/Drawer/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,7 @@ const Index = ({
href={item.href}
passHref
variant="subtle"
onClick={item.onClick}
css={{
color:
asPath.split("?")[0] === item.as
Expand Down
1 change: 1 addition & 0 deletions hooks/useExplorerStore.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ export type InputData = {
delegate?: Address;
fees?: bigint;
isOrchestrator?: boolean;
isTransferStake?: boolean;
newDelegate?: Address;
newPosNext?: string;
newPosPrev?: string;
Expand Down
4 changes: 4 additions & 0 deletions hooks/useHandleTransaction.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
import { trackTransaction } from "@lib/analytics";
import { useAddRecentTransaction } from "@rainbow-me/rainbowkit";
import { capitalCase } from "change-case";
import { useEffect } from "react";
import { useConfig } from "wagmi";

import {
InputData,
Expand All @@ -24,6 +26,7 @@ export const useHandleTransaction = (
setLatestTransactionDetails,
} = useExplorerStore();
const addRecentTransaction = useAddRecentTransaction();
const config = useConfig();

useEffect(() => {
if (isLoading) {
Expand All @@ -45,6 +48,7 @@ export const useHandleTransaction = (
useEffect(() => {
if (data) {
setLatestTransactionDetails(data, id, args);
trackTransaction(config, id, args, data);

if (onSuccess) {
onSuccess(data);
Expand Down
21 changes: 21 additions & 0 deletions layouts/main.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -107,6 +112,7 @@ export type DrawerItem = {
as: string;
icon: React.ElementType;
className?: string;
onClick?: () => void;
};

const DesignSystemProviderTyped = DesignSystemProvider as React.FC<{
Expand Down Expand Up @@ -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",
Expand All @@ -279,6 +294,7 @@ const Layout = ({ children, title = "Livepeer Explorer" }) => {
as: "/orchestrators",
icon: DNS,
className: "orchestrators",
onClick: () => trackVercelAnalyticsEvent("orchestrators_nav_clicked"),
},
{
name: "Gateways",
Expand Down Expand Up @@ -548,6 +564,11 @@ const Layout = ({ children, title = "Livepeer Explorer" }) => {
<Link passHref href="/orchestrators">
<Button
size="3"
onClick={() =>
trackVercelAnalyticsEvent(
"orchestrators_nav_clicked"
)
}
css={{
marginLeft: "$1",
backgroundColor: isOrchestratorsNavActive
Expand Down
145 changes: 145 additions & 0 deletions lib/analytics.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,145 @@
import { type BeforeSend, track } from "@vercel/analytics";
import type { InputData, TransactionIdentifier } from "hooks/useExplorerStore";
import type { Config } from "wagmi";
import { waitForTransactionReceipt } from "wagmi/actions";

/**
* Replaces wallet addresses in tracked URLs (e.g. `/accounts/0x…`), so
* pageviews and events can't be tied to a visitor's wallet.
*/
export const redactAddresses: BeforeSend = (event) => ({
...event,
url: event.url.replace(/0x[0-9a-fA-F]{40}/g, "[address]"),
});

export type DelegationFunnelEvent =
| "orchestrators_nav_clicked"
| "orchestrators_page_viewed"
| "orchestrator_detail_viewed"
| "wallet_connected"
| "delegation_form_started"
| "delegation_transaction_submitted"
| "delegation_transaction_confirmed"
| "delegation_transaction_failed"
| "account_delegating_tab_viewed"
| "redelegation_started"
| "unbonding_or_exit_started";

export const trackVercelAnalyticsEvent = (
event: DelegationFunnelEvent,
properties?: Parameters<typeof track>[1]
) => {
if (typeof window === "undefined") {
return;
}

track(event, properties);
};

const NON_DELEGATION_PATHS = ["/migrate", "/voting", "/treasury"];

/** Coarse entry point a wallet was connected from; never identifying. */
const getWalletConnectSurface = (path: string) => {
if (path === "/") {
return "home";
}
if (path.startsWith("/orchestrators")) {
return "orchestrators_list";
}
if (path.startsWith("/accounts/")) {
return path.includes("/orchestrating") ? "orchestrator_detail" : "account";
}
return "other";
};

/**
* Tracks a wallet connect as delegation intent, tagged with the page it was
* made from. Migration and governance need a wallet for their own reasons,
* so connects there aren't tracked at all.
*/
export const trackWalletConnected = (asPath: string) => {
const path = asPath.split("?")[0];

if (
NON_DELEGATION_PATHS.some((nonDelegation) => path.startsWith(nonDelegation))
) {
return;
}

trackVercelAnalyticsEvent("wallet_connected", {
surface: getWalletConnectSurface(path),
});
};

const trackedOnce = new Set<string>();

/**
* Tracks `event` at most once per page session for `key`, e.g. once per
* orchestrator no matter how often its pages are revisited.
*/
export const trackVercelAnalyticsEventOnce = (
event: DelegationFunnelEvent,
key: string
) => {
const id = `${event}:${key.toLowerCase()}`;

if (!trackedOnce.has(id)) {
trackedOnce.add(id);
trackVercelAnalyticsEvent(event);
}
};

type TransactionEvents = {
submitted: DelegationFunnelEvent;
confirmed?: DelegationFunnelEvent;
failed?: DelegationFunnelEvent;
};

const REDELEGATION_EVENTS: TransactionEvents = {
submitted: "redelegation_started",
};

const DELEGATION_EVENTS: Partial<
Record<TransactionIdentifier, TransactionEvents>
> = {
bond: {
submitted: "delegation_transaction_submitted",
confirmed: "delegation_transaction_confirmed",
failed: "delegation_transaction_failed",
},
unbond: { submitted: "unbonding_or_exit_started" },
rebond: REDELEGATION_EVENTS,
rebondFromUnbonded: REDELEGATION_EVENTS,
};

/**
* Tracks the funnel events for a submitted contract interaction: submitted
* right away, then confirmed or failed once it is mined. The receipt is
* awaited outside React, so the result is still tracked if the sending
* component unmounts, e.g. when the mobile delegate sheet is closed.
* "Move Delegated Stake" is a `bond` but counts as a redelegation.
*/
export const trackTransaction = (
config: Config,
id: TransactionIdentifier,
args: InputData,
hash: `0x${string}`
Comment on lines +122 to +126
) => {
const isRedelegation = id === "bond" && args.isTransferStake;
const events = isRedelegation ? REDELEGATION_EVENTS : DELEGATION_EVENTS[id];

if (!events) {
return;
}

trackVercelAnalyticsEvent(events.submitted);

const { confirmed, failed } = events;
if (confirmed || failed) {
// Rejects when the transaction reverts on chain.
waitForTransactionReceipt(config, { hash }).then(
() => confirmed && trackVercelAnalyticsEvent(confirmed),
() => failed && trackVercelAnalyticsEvent(failed)
);
}
};
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,7 @@
"@rainbow-me/rainbowkit": "^2.2.11",
"@reach/tabs": "^0.17.0",
"@tanstack/react-query": "^5.102.8",
"@vercel/analytics": "^2.0.1",
"change-case": "^4.1.2",
"copy-to-clipboard": "^3.3.3",
"dayjs": "^1.11.23",
Expand Down
4 changes: 4 additions & 0 deletions pages/_app.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,10 @@ import "@rainbow-me/rainbowkit/styles.css";
import "../styles/globals.css";

import { ApolloProvider } from "@apollo/client";
import { redactAddresses } from "@lib/analytics";
import { fetcher } from "@lib/fetcher";
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
import { Analytics } from "@vercel/analytics/react";
import dynamic from "next/dynamic";
import Head from "next/head";
import { useRouter } from "next/router";
Expand Down Expand Up @@ -37,6 +39,8 @@ function App({ Component, pageProps, fallback = null }) {
<title>Livepeer Explorer</title>
</Head>

<Analytics beforeSend={redactAddresses} />

<ApolloProvider client={client}>
<Tooltip.Provider>
<QueryClientProvider client={queryClient}>
Expand Down
21 changes: 21 additions & 0 deletions pages/accounts/[account]/delegating.tsx
Original file line number Diff line number Diff line change
@@ -1,13 +1,17 @@
import ErrorComponent from "@components/Error";
import AccountLayout from "@layouts/account";
import { getLayout } from "@layouts/main";
import { trackVercelAnalyticsEventOnce } from "@lib/analytics";
import { getAccount, getSortedOrchestrators } from "@lib/api/ssr";
import { EnsIdentity } from "@lib/api/types/get-ens";
import {
AccountQueryResult,
getApollo,
OrchestratorsSortedQueryResult,
} from "apollo";
import { useAccountAddress } from "hooks";
import { useRouter } from "next/router";
import { useEffect } from "react";
import { isAddress } from "viem";

type PageProps = {
Expand All @@ -18,6 +22,23 @@ type PageProps = {
};

const Delegating = ({ hadError, account, sortedOrchestrators }: PageProps) => {
const { query } = useRouter();
const accountAddress = useAccountAddress();
const viewedAccount = String(query.account);
const isMyAccount =
!!accountAddress &&
accountAddress.toLowerCase() === viewedAccount.toLowerCase();

// Re-runs when the wallet reconnects after the page has mounted.
useEffect(() => {
if (!hadError && isMyAccount) {
trackVercelAnalyticsEventOnce(
"account_delegating_tab_viewed",
viewedAccount
);
}
}, [hadError, isMyAccount, viewedAccount]);

if (hadError) {
return <ErrorComponent statusCode={500} />;
}
Expand Down
15 changes: 15 additions & 0 deletions pages/accounts/[account]/orchestrating.tsx
Original file line number Diff line number Diff line change
@@ -1,13 +1,16 @@
import ErrorComponent from "@components/Error";
import AccountLayout from "@layouts/account";
import { getLayout } from "@layouts/main";
import { trackVercelAnalyticsEventOnce } from "@lib/analytics";
import { getAccount, getSortedOrchestrators } from "@lib/api/ssr";
import { EnsIdentity } from "@lib/api/types/get-ens";
import {
AccountQueryResult,
getApollo,
OrchestratorsSortedQueryResult,
} from "apollo";
import { useRouter } from "next/router";
import { useEffect } from "react";
import { isAddress } from "viem";

type PageProps = {
Expand All @@ -22,6 +25,18 @@ const Orchestrating = ({
account,
sortedOrchestrators,
}: PageProps) => {
const { query } = useRouter();
const viewedAccount = String(query.account);

useEffect(() => {
if (!hadError) {
trackVercelAnalyticsEventOnce(
"orchestrator_detail_viewed",
viewedAccount
);
}
}, [hadError, viewedAccount]);

if (hadError) {
return <ErrorComponent statusCode={500} />;
}
Expand Down
Loading
Loading