diff --git a/dapp/.env.example b/dapp/.env.example index 483729696..ce5dc91aa 100644 --- a/dapp/.env.example +++ b/dapp/.env.example @@ -21,6 +21,9 @@ VITE_SENTRY_DSN="" VITE_POSTHOG_API_HOST="https://us.i.posthog.com" VITE_POSTHOG_API_KEY="" +# Amplitude +VITE_AMPLITUDE_API_KEY="46f4e9953a416bbef532c29dd912700" + # Feature flags VITE_FEATURE_FLAG_WITHDRAWALS_ENABLED="true" VITE_FEATURE_FLAG_OKX_WALLET_ENABLED="true" diff --git a/dapp/package.json b/dapp/package.json index b5db82c64..2ac0d987d 100644 --- a/dapp/package.json +++ b/dapp/package.json @@ -18,6 +18,8 @@ }, "dependencies": { "@acre-btc/sdk": "workspace:*", + "@amplitude/analytics-browser": "^2.17.10", + "@amplitude/plugin-session-replay-browser": "^1.19.0", "@chakra-ui/react": "^2.8.2", "@emotion/react": "^11.11.1", "@emotion/styled": "^11.11.0", diff --git a/dapp/src/amplitude.ts b/dapp/src/amplitude.ts new file mode 100644 index 000000000..dd3389500 --- /dev/null +++ b/dapp/src/amplitude.ts @@ -0,0 +1,45 @@ +import * as amplitude from "@amplitude/analytics-browser" +import env from "#/constants/env" +import bitcoinAddressToUserId from "./utils/bitcoinAddressToUserId" + +type WalletType = "okx" | "unisat" | "xverse" + +export interface AnalyticsEventData { + wallet_connection_clicked: Record + wallet_connection_started: { wallet_type: WalletType; is_embed: boolean } + wallet_connection_completed: { wallet_type: WalletType } + + deposit_btc_started: Record + deposit_btc_completed: Record + deposit_btc_failed: Record + + withdraw_btc_started: Record + withdraw_btc_completed: Record + withdraw_btc_failed: Record + + mezo_mats_clicked: Record + footer_faq_clicked: Record + footer_docs_clicked: Record + footer_blog_clicked: Record +} + +export type AnalyticsEvent = keyof AnalyticsEventData + +export const initializeAmplitude = () => { + amplitude.init(env.AMPLITUDE_API_KEY, undefined, { + autocapture: true, + }) +} + +export const trackEvent = ( + eventName: T, + eventData?: AnalyticsEventData[T], +) => { + amplitude.track(eventName, eventData) +} + +export const identifyUser = (bitcoinAddress: string | undefined) => { + const id = new amplitude.Identify() + if (bitcoinAddress) id.set("user_id", bitcoinAddressToUserId(bitcoinAddress)) + amplitude.identify(id) +} diff --git a/dapp/src/components/ConnectWalletModal/ConnectWalletButton.tsx b/dapp/src/components/ConnectWalletModal/ConnectWalletButton.tsx index e102b384f..447f104bc 100644 --- a/dapp/src/components/ConnectWalletModal/ConnectWalletButton.tsx +++ b/dapp/src/components/ConnectWalletModal/ConnectWalletButton.tsx @@ -1,4 +1,4 @@ -import React, { useCallback, useEffect, useRef, useState } from "react" +import React, { useCallback, useEffect, useMemo, useRef, useState } from "react" import { time } from "#/constants" import { useAppDispatch, @@ -12,6 +12,7 @@ import { import { setIsSignedMessage } from "#/store/wallet" import { OrangeKitConnector, OrangeKitError, OnSuccessCallback } from "#/types" import { eip1193, logPromiseFailure, orangeKit } from "#/utils" +import { trackEvent } from "#/amplitude" import { Button, Card, @@ -82,14 +83,23 @@ export default function ConnectWalletButton({ const shouldShowStatuses = isSelected && !hasConnectionError const shouldShowRetryButton = address && hasSignMessageErrorStatus + const walletType = useMemo( + () => connector.id.replace("orangekit-", "") as "okx" | "unisat" | "xverse", + [connector.id], + ) + const onSuccessSignMessage = useCallback(() => { closeModal() dispatch(setIsSignedMessage(true)) + trackEvent("wallet_connection_completed", { + wallet_type: walletType, + }) + if (onSuccess) { onSuccess() } - }, [closeModal, dispatch, onSuccess]) + }, [closeModal, dispatch, onSuccess, walletType]) const handleSignMessageAndCreateSession = useCallback( async (connectedConnector: OrangeKitConnector, btcAddress: string) => { @@ -169,6 +179,11 @@ export default function ConnectWalletButton({ // Do not trigger action again when wallet connection is in progress if (shouldShowStatuses) return + trackEvent("wallet_connection_started", { + wallet_type: walletType, + is_embed: false, + }) + if (!isReconnecting) onDisconnect() resetConnectionAlert() resetMessageStatus() @@ -187,9 +202,13 @@ export default function ConnectWalletButton({ useEffect(() => { if (!isMounted.current && isEmbed && isSelected) { isMounted.current = true + trackEvent("wallet_connection_started", { + wallet_type: walletType, + is_embed: true, + }) handleConnection() } - }, [handleConnection, isEmbed, isSelected]) + }, [handleConnection, isEmbed, isSelected, walletType]) return ( { + trackEvent("wallet_connection_clicked") openModal(MODAL_TYPES.CONNECT_WALLET, { isReconnecting }) } @@ -118,7 +120,7 @@ export default function ConnectWallet() { leftIcon={} rightIcon={isOpen ? : } > - + {addressUtils.truncateAddress(address)} @@ -166,7 +168,7 @@ export default function ConnectWallet() { spacing={3} > - + {addressUtils.truncateAddress(address)} diff --git a/dapp/src/components/TransactionModal/ActiveStakingStep/DepositBTCModal.tsx b/dapp/src/components/TransactionModal/ActiveStakingStep/DepositBTCModal.tsx index 8587fa3b4..9dc398ec8 100644 --- a/dapp/src/components/TransactionModal/ActiveStakingStep/DepositBTCModal.tsx +++ b/dapp/src/components/TransactionModal/ActiveStakingStep/DepositBTCModal.tsx @@ -17,6 +17,7 @@ import { PROCESS_STATUSES } from "#/types" import { eip1193, logPromiseFailure } from "#/utils" import { useTimeout } from "@chakra-ui/react" import { useMutation } from "@tanstack/react-query" +import { trackEvent } from "#/amplitude" import WalletInteractionModal from "../WalletInteractionModal" export default function DepositBTCModal() { @@ -58,6 +59,7 @@ export default function DepositBTCModal() { (transactionHash: string) => { dispatch(setTxHash(transactionHash)) handleStake() + trackEvent("deposit_btc_completed", {}) handleCapture(PostHogEvent.DepositSuccess, { transactionHash, }) @@ -75,6 +77,7 @@ export default function DepositBTCModal() { onError(error) } + trackEvent("deposit_btc_failed", {}) handleCaptureWithCause(error, PostHogEvent.DepositFailure) }, [sessionIdToPromise, handlePause, onError, handleCaptureWithCause], diff --git a/dapp/src/components/TransactionModal/ActiveUnstakingStep/SignMessageModal.tsx b/dapp/src/components/TransactionModal/ActiveUnstakingStep/SignMessageModal.tsx index 01deb3fd3..a7cc5f3fc 100644 --- a/dapp/src/components/TransactionModal/ActiveUnstakingStep/SignMessageModal.tsx +++ b/dapp/src/components/TransactionModal/ActiveUnstakingStep/SignMessageModal.tsx @@ -17,6 +17,7 @@ import { useInitializeWithdraw } from "#/acre-react/hooks" import { time, queryKeysFactory } from "#/constants" import { useMutation, useQueryClient } from "@tanstack/react-query" import PostHogEvent from "#/posthog/events" +import { trackEvent } from "#/amplitude" import BuildTransactionModal from "./BuildTransactionModal" import WalletInteractionModal from "../WalletInteractionModal" @@ -58,6 +59,7 @@ export default function SignMessageModal() { const onSignMessageSuccess = useCallback(() => { logPromiseFailure(refetchBitcoinPosition()) dispatch(setStatus(PROCESS_STATUSES.SUCCEEDED)) + trackEvent("withdraw_btc_completed", {}) handleCapture(PostHogEvent.WithdrawalSuccess) }, [dispatch, refetchBitcoinPosition, handleCapture]) @@ -79,6 +81,7 @@ export default function SignMessageModal() { onSignMessageError(error) } + trackEvent("withdraw_btc_failed", {}) handleCaptureWithCause(error, PostHogEvent.WithdrawalFailure) }, [ diff --git a/dapp/src/constants/env.ts b/dapp/src/constants/env.ts index 4d03b302e..5527692f6 100644 --- a/dapp/src/constants/env.ts +++ b/dapp/src/constants/env.ts @@ -26,6 +26,8 @@ const POSTHOG_API_HOST = import.meta.env.VITE_POSTHOG_API_HOST const POSTHOG_API_KEY = import.meta.env.VITE_POSTHOG_API_KEY +const AMPLITUDE_API_KEY = import.meta.env.VITE_AMPLITUDE_API_KEY + export default { PROD, USE_TESTNET, @@ -41,4 +43,5 @@ export default { ACRE_API_ENDPOINT, POSTHOG_API_HOST, POSTHOG_API_KEY, + AMPLITUDE_API_KEY, } diff --git a/dapp/src/hooks/useAmplitude.ts b/dapp/src/hooks/useAmplitude.ts new file mode 100644 index 000000000..21c5dd27f --- /dev/null +++ b/dapp/src/hooks/useAmplitude.ts @@ -0,0 +1,32 @@ +import { env } from "#/constants" +import * as amplitude from "@amplitude/analytics-browser" +import { LogLevel } from "@amplitude/analytics-browser/lib/esm/types" +import { sessionReplayPlugin } from "@amplitude/plugin-session-replay-browser" +import { useEffect, useRef } from "react" + +const useAmplitude = () => { + const inited = useRef(false) + + useEffect(() => { + if (inited.current) return // Avoid re-initialization with React Strict Mode + inited.current = true + + amplitude.add( + sessionReplayPlugin({ + privacyConfig: { + maskSelector: ["[data-sensitive]"], + }, + }), + ) + + amplitude.init(env.AMPLITUDE_API_KEY, undefined, { + autocapture: true, + + // Disable tracking during development, but log events + optOut: !env.PROD, + logLevel: env.PROD ? undefined : LogLevel.Verbose, + }) + }, []) +} + +export default useAmplitude diff --git a/dapp/src/hooks/useInitApp.ts b/dapp/src/hooks/useInitApp.ts index bb0eb61e3..578060e1f 100644 --- a/dapp/src/hooks/useInitApp.ts +++ b/dapp/src/hooks/useInitApp.ts @@ -6,6 +6,7 @@ import useDetectEmbed from "./useDetectEmbed" import useFetchBTCPriceUSD from "./useFetchBTCPriceUSD" import useDisconnectWallet from "./useDisconnectWallet" import useSentry from "./useSentry" +import useAmplitude from "./useAmplitude" export default function useInitApp() { // TODO: Let's uncomment when dark mode is ready @@ -19,4 +20,5 @@ export default function useInitApp() { useAccountChangedOKX() useAccountsChangedOKX() useAccountsChangedUnisat() + useAmplitude() } diff --git a/dapp/src/hooks/useWallet.ts b/dapp/src/hooks/useWallet.ts index dcf25536d..bfd65a0b8 100644 --- a/dapp/src/hooks/useWallet.ts +++ b/dapp/src/hooks/useWallet.ts @@ -18,6 +18,7 @@ import { import { useMutation, useQueryClient } from "@tanstack/react-query" import { useDispatch } from "react-redux" import { setAddress } from "#/store/wallet" +import { identifyUser } from "#/amplitude" import useResetWalletState from "./useResetWalletState" import useLastUsedBtcAddress from "./useLastUsedBtcAddress" import useBitcoinBalance from "./useBitcoinBalance" @@ -78,6 +79,7 @@ export default function useWallet(): UseWalletReturn { dispatch(setAddress(bitcoinAddress)) setAddressInLocalStorage(bitcoinAddress) sentry.setUser(bitcoinAddress) + identifyUser(bitcoinAddress) }, }, }) @@ -90,6 +92,7 @@ export default function useWallet(): UseWalletReturn { removeAddressFromLocalStorage() resetWalletState() sentry.setUser(undefined) + identifyUser(undefined) }, }, }) @@ -129,6 +132,7 @@ export default function useWallet(): UseWalletReturn { dispatch(setAddress(bitcoinAddress)) setAddressInLocalStorage(bitcoinAddress) sentry.setUser(bitcoinAddress) + identifyUser(bitcoinAddress) }, }, queryClient, diff --git a/dapp/src/pages/DashboardPage/BeehiveCard.tsx b/dapp/src/pages/DashboardPage/BeehiveCard.tsx index 604242a0b..f22ce4365 100644 --- a/dapp/src/pages/DashboardPage/BeehiveCard.tsx +++ b/dapp/src/pages/DashboardPage/BeehiveCard.tsx @@ -6,6 +6,7 @@ import UserDataSkeleton from "#/components/shared/UserDataSkeleton" import { useMats, useModal } from "#/hooks" import { MODAL_TYPES } from "#/types" import { numbersUtils } from "#/utils" +import { trackEvent } from "#/amplitude" import { Box, Button, @@ -25,6 +26,7 @@ export default function BeehiveCard(props: CardProps) { const { data } = useMats() const handleOpenBeehiveModal = () => { + trackEvent("mezo_mats_clicked") openModal(MODAL_TYPES.MEZO_BEEHIVE) } diff --git a/dapp/src/pages/DashboardPage/PositionDetails.tsx b/dapp/src/pages/DashboardPage/PositionDetails.tsx index dc075d4ff..85339a251 100644 --- a/dapp/src/pages/DashboardPage/PositionDetails.tsx +++ b/dapp/src/pages/DashboardPage/PositionDetails.tsx @@ -1,4 +1,4 @@ -import React from "react" +import React, { useCallback } from "react" import CurrencyBalanceWithConversion from "#/components/shared/CurrencyBalanceWithConversion" import { useActivitiesCount, @@ -24,6 +24,7 @@ import { featureFlags } from "#/constants" import { IconClockHour5Filled } from "@tabler/icons-react" import TooltipIcon from "#/components/shared/TooltipIcon" import { activitiesUtils } from "#/utils" +import { trackEvent } from "#/amplitude" import AcreTVLMessage from "./AcreTVLMessage" const isWithdrawalFlowEnabled = featureFlags.WITHDRAWALS_ENABLED @@ -43,7 +44,17 @@ export default function PositionDetails() { const bitcoinAmount = bitcoinPosition?.estimatedBitcoinBalance ?? 0n const openDepositModal = useTransactionModal(ACTION_FLOW_TYPES.STAKE) + const handleDeposit = useCallback(() => { + openDepositModal() + trackEvent("deposit_btc_started") + }, [openDepositModal]) + const openWithdrawModal = useTransactionModal(ACTION_FLOW_TYPES.UNSTAKE) + const handleWithdraw = useCallback(() => { + openWithdrawModal() + trackEvent("withdraw_btc_started") + }, [openWithdrawModal]) + const activitiesCount = useActivitiesCount() const { data: activities } = useActivities() const isMobileMode = useMobileMode() @@ -97,7 +108,7 @@ export default function PositionDetails() { >