From 22ce546a1d6d516de338306b9adbca72e1ed4379 Mon Sep 17 00:00:00 2001 From: tom Date: Fri, 8 Sep 2023 12:07:05 -0300 Subject: [PATCH 1/5] color mode property for PageView and AccountAccess events --- lib/mixpanel/useLogPageView.tsx | 5 ++++- lib/mixpanel/utils.ts | 5 +++++ ui/pages/UnverifiedEmail.tsx | 7 +++++++ ui/snippets/profileMenu/ProfileMenuContent.tsx | 13 ++++++++++++- ui/snippets/profileMenu/ProfileMenuDesktop.tsx | 10 ++++++++++ ui/snippets/profileMenu/ProfileMenuMobile.tsx | 10 ++++++++++ 6 files changed, 48 insertions(+), 2 deletions(-) diff --git a/lib/mixpanel/useLogPageView.tsx b/lib/mixpanel/useLogPageView.tsx index 04c25de7ec..9c3f6a929a 100644 --- a/lib/mixpanel/useLogPageView.tsx +++ b/lib/mixpanel/useLogPageView.tsx @@ -1,3 +1,4 @@ +import { useColorMode } from '@chakra-ui/react'; import { usePathname } from 'next/navigation'; import { useRouter } from 'next/router'; import React from 'react'; @@ -16,6 +17,7 @@ export default function useLogPageView(isInited: boolean) { const tab = getQueryParamString(router.query.tab); const page = getQueryParamString(router.query.page); + const { colorMode } = useColorMode(); React.useEffect(() => { if (!config.features.mixpanel.isEnabled || !isInited) { @@ -26,11 +28,12 @@ export default function useLogPageView(isInited: boolean) { 'Page type': getPageType(router.pathname), Tab: getTabName(tab), Page: page || undefined, + 'Color mode': colorMode, }); // these are only deps that should trigger the effect // in some scenarios page type is not changing (e.g navigation from one address page to another), // but we still want to log page view // so we use pathname from 'next/navigation' instead of router.pathname from 'next/router' as deps // eslint-disable-next-line react-hooks/exhaustive-deps - }, [ isInited, page, pathname, tab ]); + }, [ isInited, page, pathname, tab, colorMode ]); } diff --git a/lib/mixpanel/utils.ts b/lib/mixpanel/utils.ts index 6e88ec10b7..9301081143 100644 --- a/lib/mixpanel/utils.ts +++ b/lib/mixpanel/utils.ts @@ -4,6 +4,7 @@ export enum EventTypes { PAGE_VIEW = 'Page view', SEARCH_QUERY = 'Search query', ADD_TO_WALLET = 'Add to wallet', + ACCOUNT_ACCESS = 'Account access', } /* eslint-disable @typescript-eslint/indent */ @@ -13,6 +14,7 @@ Type extends EventTypes.PAGE_VIEW ? 'Page type': string; 'Tab': string; 'Page'?: string; + 'Color mode': 'light' | 'dark'; } : Type extends EventTypes.SEARCH_QUERY ? { 'Search query': string; @@ -29,5 +31,8 @@ Type extends EventTypes.ADD_TO_WALLET ? ( 'Token': string; } ) : +Type extends EventTypes.ACCOUNT_ACCESS ? { + 'Action': 'auth0_init' | 'verification_email_resent' | 'logged_out'; +} : undefined; /* eslint-enable @typescript-eslint/indent */ diff --git a/ui/pages/UnverifiedEmail.tsx b/ui/pages/UnverifiedEmail.tsx index 5149fd729a..aa04b0c771 100644 --- a/ui/pages/UnverifiedEmail.tsx +++ b/ui/pages/UnverifiedEmail.tsx @@ -7,6 +7,7 @@ import dayjs from 'lib/date/dayjs'; import getErrorObjPayload from 'lib/errors/getErrorObjPayload'; import getErrorObjStatusCode from 'lib/errors/getErrorObjStatusCode'; import useToast from 'lib/hooks/useToast'; +import * as mixpanel from 'lib/mixpanel/index'; interface Props { email?: string; // TODO: obtain email from API @@ -22,8 +23,14 @@ const UnverifiedEmail = ({ email }: Props) => { setIsLoading(true); + mixpanel.logEvent( + mixpanel.EventTypes.ACCOUNT_ACCESS, + { Action: 'verification_email_resent' }, + ); + try { await apiFetch('email_resend'); + toast({ id: toastId, position: 'top-right', diff --git a/ui/snippets/profileMenu/ProfileMenuContent.tsx b/ui/snippets/profileMenu/ProfileMenuContent.tsx index bfa6685d55..b9782c7f36 100644 --- a/ui/snippets/profileMenu/ProfileMenuContent.tsx +++ b/ui/snippets/profileMenu/ProfileMenuContent.tsx @@ -5,6 +5,7 @@ import type { UserInfo } from 'types/api/account'; import config from 'configs/app'; import useNavItems from 'lib/hooks/useNavItems'; +import * as mixpanel from 'lib/mixpanel/index'; import getDefaultTransitionProps from 'theme/utils/getDefaultTransitionProps'; import NavLink from 'ui/snippets/navigation/NavLink'; @@ -18,6 +19,14 @@ const ProfileMenuContent = ({ data }: Props) => { const { accountNavItems, profileItem } = useNavItems(); const primaryTextColor = useColorModeValue('blackAlpha.800', 'whiteAlpha.800'); + const handleSingOutClick = React.useCallback(() => { + mixpanel.logEvent( + mixpanel.EventTypes.ACCOUNT_ACCESS, + { Action: 'logged_out' }, + { send_immediately: true }, + ); + }, []); + if (!feature.isEnabled) { return null; } @@ -52,7 +61,9 @@ const ProfileMenuContent = ({ data }: Props) => { - + ); diff --git a/ui/snippets/profileMenu/ProfileMenuDesktop.tsx b/ui/snippets/profileMenu/ProfileMenuDesktop.tsx index feed722dc7..33d39cd0d3 100644 --- a/ui/snippets/profileMenu/ProfileMenuDesktop.tsx +++ b/ui/snippets/profileMenu/ProfileMenuDesktop.tsx @@ -4,6 +4,7 @@ import React from 'react'; import useFetchProfileInfo from 'lib/hooks/useFetchProfileInfo'; import useLoginUrl from 'lib/hooks/useLoginUrl'; +import * as mixpanel from 'lib/mixpanel/index'; import UserAvatar from 'ui/shared/UserAvatar'; import ProfileMenuContent from 'ui/snippets/profileMenu/ProfileMenuContent'; @@ -18,6 +19,14 @@ const ProfileMenuDesktop = () => { } }, [ data, error?.status, isLoading ]); + const handleSignInClick = React.useCallback(() => { + mixpanel.logEvent( + mixpanel.EventTypes.ACCOUNT_ACCESS, + { Action: 'auth0_init' }, + { send_immediately: true }, + ); + }, []); + const buttonProps: Partial = (() => { if (hasMenu || !loginUrl) { return {}; @@ -26,6 +35,7 @@ const ProfileMenuDesktop = () => { return { as: 'a', href: loginUrl, + onClick: handleSignInClick, }; })(); diff --git a/ui/snippets/profileMenu/ProfileMenuMobile.tsx b/ui/snippets/profileMenu/ProfileMenuMobile.tsx index c21552f323..4f6ccf64ef 100644 --- a/ui/snippets/profileMenu/ProfileMenuMobile.tsx +++ b/ui/snippets/profileMenu/ProfileMenuMobile.tsx @@ -4,6 +4,7 @@ import React from 'react'; import useFetchProfileInfo from 'lib/hooks/useFetchProfileInfo'; import useLoginUrl from 'lib/hooks/useLoginUrl'; +import * as mixpanel from 'lib/mixpanel/index'; import UserAvatar from 'ui/shared/UserAvatar'; import ProfileMenuContent from 'ui/snippets/profileMenu/ProfileMenuContent'; @@ -14,6 +15,14 @@ const ProfileMenuMobile = () => { const loginUrl = useLoginUrl(); const [ hasMenu, setHasMenu ] = React.useState(false); + const handleSignInClick = React.useCallback(() => { + mixpanel.logEvent( + mixpanel.EventTypes.ACCOUNT_ACCESS, + { Action: 'auth0_init' }, + { send_immediately: true }, + ); + }, []); + React.useEffect(() => { if (!isLoading) { setHasMenu(Boolean(data)); @@ -28,6 +37,7 @@ const ProfileMenuMobile = () => { return { as: 'a', href: loginUrl, + onClick: handleSignInClick, }; })(); From 172dee0835aa9925d9629bdaf40cad9a95a04de7 Mon Sep 17 00:00:00 2001 From: tom Date: Fri, 8 Sep 2023 15:39:04 -0300 Subject: [PATCH 2/5] account events --- lib/mixpanel/getPageType.ts | 2 +- lib/mixpanel/utils.ts | 23 ++++++++++++++++- .../AddressVerificationModal.tsx | 25 ++++++++++++++++--- .../AddressVerificationStepSignature.tsx | 14 ++++++----- ui/pages/UnverifiedEmail.tsx | 2 +- ui/pages/VerifiedAddresses.tsx | 2 ++ ui/privateTags/AddressModal/AddressModal.tsx | 25 ++++++++++++++++--- ui/privateTags/PrivateAddressTags.tsx | 9 ++++++- .../TransactionModal/TransactionForm.tsx | 13 +++++----- .../TransactionModal/TransactionModal.tsx | 22 ++++++++++++++-- .../AddressActions/PrivateTagMenuItem.tsx | 13 +++++++++- .../AddressActions/TokenInfoMenuItem.tsx | 2 ++ .../profileMenu/ProfileMenuContent.tsx | 2 +- .../profileMenu/ProfileMenuDesktop.tsx | 2 +- ui/snippets/profileMenu/ProfileMenuMobile.tsx | 2 +- ui/tokenInfo/TokenInfoForm.tsx | 14 +++++++++++ 16 files changed, 143 insertions(+), 29 deletions(-) diff --git a/lib/mixpanel/getPageType.ts b/lib/mixpanel/getPageType.ts index 2e1b555d68..6d21095222 100644 --- a/lib/mixpanel/getPageType.ts +++ b/lib/mixpanel/getPageType.ts @@ -1,6 +1,6 @@ import type { Route } from 'nextjs-routes'; -const PAGE_TYPE_DICT: Record = { +export const PAGE_TYPE_DICT: Record = { '/': 'Homepage', '/txs': 'Transactions', '/tx/[hash]': 'Transaction details', diff --git a/lib/mixpanel/utils.ts b/lib/mixpanel/utils.ts index 9301081143..fd4fb2149a 100644 --- a/lib/mixpanel/utils.ts +++ b/lib/mixpanel/utils.ts @@ -5,6 +5,9 @@ export enum EventTypes { SEARCH_QUERY = 'Search query', ADD_TO_WALLET = 'Add to wallet', ACCOUNT_ACCESS = 'Account access', + PRIVATE_TAG = 'Private tag', + VERIFY_ADDRESS = 'Verify address', + VERIFY_TOKEN = 'Verify token', } /* eslint-disable @typescript-eslint/indent */ @@ -32,7 +35,25 @@ Type extends EventTypes.ADD_TO_WALLET ? ( } ) : Type extends EventTypes.ACCOUNT_ACCESS ? { - 'Action': 'auth0_init' | 'verification_email_resent' | 'logged_out'; + 'Action': 'Auth0 init' | 'Verification email resent' | 'Logged out'; +} : +Type extends EventTypes.PRIVATE_TAG ? { + 'Action': 'Form opened' | 'Submit'; + 'Page type': string; + 'Tag type': 'Address' | 'Tx'; +} : +Type extends EventTypes.VERIFY_ADDRESS ? ( + { + 'Action': 'Form opened' | 'Address entered'; + 'Page type': string; + } | { + 'Action': 'Sign ownership'; + 'Page type': string; + 'Sign method': 'wallet' | 'manual'; + } +) : +Type extends EventTypes.VERIFY_TOKEN ? { + 'Action': 'Form opened' | 'Submit'; } : undefined; /* eslint-enable @typescript-eslint/indent */ diff --git a/ui/addressVerification/AddressVerificationModal.tsx b/ui/addressVerification/AddressVerificationModal.tsx index d9bba69ace..601ec982e9 100644 --- a/ui/addressVerification/AddressVerificationModal.tsx +++ b/ui/addressVerification/AddressVerificationModal.tsx @@ -5,6 +5,7 @@ import type { AddressVerificationFormFirstStepFields, AddressCheckStatusSuccess import type { VerifiedAddress } from 'types/api/account'; import eastArrowIcon from 'icons/arrows/east.svg'; +import * as mixpanel from 'lib/mixpanel/index'; import Web3ModalProvider from 'ui/shared/Web3ModalProvider'; import AddressVerificationStepAddress from './steps/AddressVerificationStepAddress'; @@ -20,22 +21,38 @@ interface Props { onAddTokenInfoClick: (address: string) => void; onShowListClick: () => void; defaultAddress?: string; + pageType: string; } -const AddressVerificationModal = ({ defaultAddress, isOpen, onClose, onSubmit, onAddTokenInfoClick, onShowListClick }: Props) => { +const AddressVerificationModal = ({ defaultAddress, isOpen, onClose, onSubmit, onAddTokenInfoClick, onShowListClick, pageType }: Props) => { const [ stepIndex, setStepIndex ] = React.useState(0); const [ data, setData ] = React.useState({ address: '', signingMessage: '' }); + React.useEffect(() => { + isOpen && mixpanel.logEvent( + mixpanel.EventTypes.VERIFY_ADDRESS, + { Action: 'Form opened', 'Page type': pageType }, + ); + }, [ isOpen, pageType ]); + const handleGoToSecondStep = React.useCallback((firstStepResult: typeof data) => { setData(firstStepResult); setStepIndex((prev) => prev + 1); - }, []); + mixpanel.logEvent( + mixpanel.EventTypes.VERIFY_ADDRESS, + { Action: 'Address entered', 'Page type': pageType }, + ); + }, [ pageType ]); - const handleGoToThirdStep = React.useCallback((address: VerifiedAddress) => { + const handleGoToThirdStep = React.useCallback((address: VerifiedAddress, signMethod: 'wallet' | 'manual') => { onSubmit(address); setStepIndex((prev) => prev + 1); setData((prev) => ({ ...prev, isToken: Boolean(address.metadata.tokenName) })); - }, [ onSubmit ]); + mixpanel.logEvent( + mixpanel.EventTypes.VERIFY_ADDRESS, + { Action: 'Sign ownership', 'Page type': pageType, 'Sign method': signMethod }, + ); + }, [ onSubmit, pageType ]); const handleGoToPrevStep = React.useCallback(() => { setStepIndex((prev) => prev - 1); diff --git a/ui/addressVerification/steps/AddressVerificationStepSignature.tsx b/ui/addressVerification/steps/AddressVerificationStepSignature.tsx index bb4c0bf9f4..af77a4bda7 100644 --- a/ui/addressVerification/steps/AddressVerificationStepSignature.tsx +++ b/ui/addressVerification/steps/AddressVerificationStepSignature.tsx @@ -26,13 +26,15 @@ import AddressVerificationFieldSignature from '../fields/AddressVerificationFiel type Fields = RootFields & AddressVerificationFormSecondStepFields; +type SignMethod = 'wallet' | 'manual'; + interface Props extends AddressVerificationFormFirstStepFields, AddressCheckStatusSuccess{ - onContinue: (newItem: VerifiedAddress) => void; + onContinue: (newItem: VerifiedAddress, signMethod: SignMethod) => void; noWeb3Provider?: boolean; } const AddressVerificationStepSignature = ({ address, signingMessage, contractCreator, contractOwner, onContinue, noWeb3Provider }: Props) => { - const [ signMethod, setSignMethod ] = React.useState<'wallet' | 'manually'>(noWeb3Provider ? 'manually' : 'wallet'); + const [ signMethod, setSignMethod ] = React.useState(noWeb3Provider ? 'manual' : 'wallet'); const { open: openWeb3Modal } = useWeb3Modal(); const { isConnected } = useAccount(); @@ -70,11 +72,11 @@ const AddressVerificationStepSignature = ({ address, signingMessage, contractCre return setError('root', { type, message: response.status === 'INVALID_SIGNER_ERROR' ? response.invalidSigner.signer : undefined }); } - onContinue(response.result.verifiedAddress); + onContinue(response.result.verifiedAddress, signMethod); } catch (error) { setError('root', { type: 'UNKNOWN_STATUS' }); } - }, [ address, apiFetch, onContinue, setError ]); + }, [ address, apiFetch, onContinue, setError, signMethod ]); const onSubmit = handleSubmit(onFormSubmit); @@ -115,7 +117,7 @@ const AddressVerificationStepSignature = ({ address, signingMessage, contractCre }, [ clearErrors, onSubmit ]); const button = (() => { - if (signMethod === 'manually') { + if (signMethod === 'manual') { return ( - + { deleteModalData && ( void; + onSuccess: () => Promise; setAlertVisible: (isAlertVisible: boolean) => void; } @@ -31,7 +32,7 @@ type Inputs = { tag: string; } -const TransactionForm: React.FC = ({ data, onClose, setAlertVisible }) => { +const TransactionForm: React.FC = ({ data, onClose, onSuccess, setAlertVisible }) => { const [ pending, setPending ] = useState(false); const formBackgroundColor = useColorModeValue('white', 'gray.900'); @@ -74,11 +75,11 @@ const TransactionForm: React.FC = ({ data, onClose, setAlertVisible }) => setAlertVisible(true); } }, - onSuccess: () => { - queryClient.refetchQueries([ resourceKey('private_tags_tx') ]).then(() => { - onClose(); - setPending(false); - }); + onSuccess: async() => { + await queryClient.refetchQueries([ resourceKey('private_tags_tx') ]); + await onSuccess(); + onClose(); + setPending(false); }, }); diff --git a/ui/privateTags/TransactionModal/TransactionModal.tsx b/ui/privateTags/TransactionModal/TransactionModal.tsx index acb8ad2387..eefdefef2a 100644 --- a/ui/privateTags/TransactionModal/TransactionModal.tsx +++ b/ui/privateTags/TransactionModal/TransactionModal.tsx @@ -2,6 +2,8 @@ import React, { useCallback, useState } from 'react'; import type { TransactionTag } from 'types/api/account'; +import { PAGE_TYPE_DICT } from 'lib/mixpanel/getPageType'; +import * as mixpanel from 'lib/mixpanel/index'; import FormModal from 'ui/shared/FormModal'; import TransactionForm from './TransactionForm'; @@ -18,9 +20,25 @@ const AddressModal: React.FC = ({ isOpen, onClose, data }) => { const [ isAlertVisible, setAlertVisible ] = useState(false); + React.useEffect(() => { + isOpen && !data?.id && mixpanel.logEvent( + mixpanel.EventTypes.PRIVATE_TAG, + { Action: 'Form opened', 'Page type': PAGE_TYPE_DICT['/account/tag-address'], 'Tag type': 'Tx' }, + ); + }, [ data?.id, isOpen ]); + + const handleSuccess = React.useCallback(async() => { + if (!data?.id) { + mixpanel.logEvent( + mixpanel.EventTypes.PRIVATE_TAG, + { Action: 'Submit', 'Page type': PAGE_TYPE_DICT['/account/tag-address'], 'Tag type': 'Tx' }, + ); + } + }, [ data?.id ]); + const renderForm = useCallback(() => { - return ; - }, [ data, onClose ]); + return ; + }, [ data, handleSuccess, onClose ]); return ( isOpen={ isOpen } diff --git a/ui/shared/AddressActions/PrivateTagMenuItem.tsx b/ui/shared/AddressActions/PrivateTagMenuItem.tsx index 452a2d2599..95352dcf0a 100644 --- a/ui/shared/AddressActions/PrivateTagMenuItem.tsx +++ b/ui/shared/AddressActions/PrivateTagMenuItem.tsx @@ -1,11 +1,13 @@ import { MenuItem, Icon, chakra, useDisclosure } from '@chakra-ui/react'; import { useQueryClient } from '@tanstack/react-query'; +import { useRouter } from 'next/router'; import React from 'react'; import type { Address } from 'types/api/address'; import iconPrivateTags from 'icons/privattags.svg'; import { getResourceKey } from 'lib/api/useApiQuery'; +import getPageType from 'lib/mixpanel/getPageType'; import PrivateTagModal from 'ui/privateTags/AddressModal/AddressModal'; interface Props { @@ -17,6 +19,7 @@ interface Props { const PrivateTagMenuItem = ({ className, hash, onBeforeClick }: Props) => { const modal = useDisclosure(); const queryClient = useQueryClient(); + const router = useRouter(); const queryKey = getResourceKey('address', { pathParams: { hash } }); const addressData = queryClient.getQueryData
(queryKey); @@ -44,13 +47,21 @@ const PrivateTagMenuItem = ({ className, hash, onBeforeClick }: Props) => { return null; } + const pageType = getPageType(router.pathname); + return ( <> Add private tag - + ); }; diff --git a/ui/shared/AddressActions/TokenInfoMenuItem.tsx b/ui/shared/AddressActions/TokenInfoMenuItem.tsx index ad01af5243..06950e9e82 100644 --- a/ui/shared/AddressActions/TokenInfoMenuItem.tsx +++ b/ui/shared/AddressActions/TokenInfoMenuItem.tsx @@ -8,6 +8,7 @@ import config from 'configs/app'; import iconEdit from 'icons/edit.svg'; import useApiQuery from 'lib/api/useApiQuery'; import useHasAccount from 'lib/hooks/useHasAccount'; +import { PAGE_TYPE_DICT } from 'lib/mixpanel/getPageType'; import AddressVerificationModal from 'ui/addressVerification/AddressVerificationModal'; interface Props { @@ -93,6 +94,7 @@ const TokenInfoMenuItem = ({ className, hash, onBeforeClick }: Props) => { { content } { const handleSingOutClick = React.useCallback(() => { mixpanel.logEvent( mixpanel.EventTypes.ACCOUNT_ACCESS, - { Action: 'logged_out' }, + { Action: 'Logged out' }, { send_immediately: true }, ); }, []); diff --git a/ui/snippets/profileMenu/ProfileMenuDesktop.tsx b/ui/snippets/profileMenu/ProfileMenuDesktop.tsx index 33d39cd0d3..30f8e633a2 100644 --- a/ui/snippets/profileMenu/ProfileMenuDesktop.tsx +++ b/ui/snippets/profileMenu/ProfileMenuDesktop.tsx @@ -22,7 +22,7 @@ const ProfileMenuDesktop = () => { const handleSignInClick = React.useCallback(() => { mixpanel.logEvent( mixpanel.EventTypes.ACCOUNT_ACCESS, - { Action: 'auth0_init' }, + { Action: 'Auth0 init' }, { send_immediately: true }, ); }, []); diff --git a/ui/snippets/profileMenu/ProfileMenuMobile.tsx b/ui/snippets/profileMenu/ProfileMenuMobile.tsx index 4f6ccf64ef..7c4c6ed3c1 100644 --- a/ui/snippets/profileMenu/ProfileMenuMobile.tsx +++ b/ui/snippets/profileMenu/ProfileMenuMobile.tsx @@ -18,7 +18,7 @@ const ProfileMenuMobile = () => { const handleSignInClick = React.useCallback(() => { mixpanel.logEvent( mixpanel.EventTypes.ACCOUNT_ACCESS, - { Action: 'auth0_init' }, + { Action: 'Auth0 init' }, { send_immediately: true }, ); }, []); diff --git a/ui/tokenInfo/TokenInfoForm.tsx b/ui/tokenInfo/TokenInfoForm.tsx index 9d7f33615a..30da8f0e42 100644 --- a/ui/tokenInfo/TokenInfoForm.tsx +++ b/ui/tokenInfo/TokenInfoForm.tsx @@ -12,6 +12,7 @@ import useApiFetch from 'lib/api/useApiFetch'; import useApiQuery from 'lib/api/useApiQuery'; import useToast from 'lib/hooks/useToast'; import useUpdateEffect from 'lib/hooks/useUpdateEffect'; +import * as mixpanel from 'lib/mixpanel/index'; import ContentLoader from 'ui/shared/ContentLoader'; import DataFetchAlert from 'ui/shared/DataFetchAlert'; @@ -44,6 +45,7 @@ interface Props { const TokenInfoForm = ({ address, tokenName, application, onSubmit }: Props) => { const containerRef = React.useRef(null); + const openEventSent = React.useRef(false); const apiFetch = useApiFetch(); const toast = useToast(); @@ -58,6 +60,13 @@ const TokenInfoForm = ({ address, tokenName, application, onSubmit }: Props) => }); const { handleSubmit, formState, control, trigger } = formApi; + React.useEffect(() => { + if (!application?.id && !openEventSent.current) { + mixpanel.logEvent(mixpanel.EventTypes.VERIFY_TOKEN, { Action: 'Form opened' }); + openEventSent.current = true; + } + }, [ application?.id ]); + const onFormSubmit: SubmitHandler = React.useCallback(async(data) => { try { const submission = prepareRequestBody(data); @@ -73,6 +82,11 @@ const TokenInfoForm = ({ address, tokenName, application, onSubmit }: Props) => if ('id' in result) { onSubmit(result); + + if (!application?.id) { + mixpanel.logEvent(mixpanel.EventTypes.VERIFY_TOKEN, { Action: 'Submit' }); + } + } else { throw result; } From 256f5758564c652a0ac91f42c3915286dd9dd6ac Mon Sep 17 00:00:00 2001 From: tom Date: Fri, 8 Sep 2023 16:25:00 -0300 Subject: [PATCH 3/5] contract events --- lib/mixpanel/utils.ts | 14 ++++++++++++++ ui/address/contract/ContractConnectWallet.tsx | 9 ++++++++- ui/address/contract/ContractMethodCallable.tsx | 9 ++++++++- .../ContractVerificationForm.tsx | 14 +++++++++++++- 4 files changed, 43 insertions(+), 3 deletions(-) diff --git a/lib/mixpanel/utils.ts b/lib/mixpanel/utils.ts index fd4fb2149a..f3f2146a73 100644 --- a/lib/mixpanel/utils.ts +++ b/lib/mixpanel/utils.ts @@ -8,6 +8,9 @@ export enum EventTypes { PRIVATE_TAG = 'Private tag', VERIFY_ADDRESS = 'Verify address', VERIFY_TOKEN = 'Verify token', + WALLET_CONNECT = 'Wallet connect', + CONTRACT_INTERACTION = 'Contract interaction', + CONTRACT_VERIFICATION = 'Contract verification', } /* eslint-disable @typescript-eslint/indent */ @@ -55,5 +58,16 @@ Type extends EventTypes.VERIFY_ADDRESS ? ( Type extends EventTypes.VERIFY_TOKEN ? { 'Action': 'Form opened' | 'Submit'; } : +Type extends EventTypes.WALLET_CONNECT ? { + 'Status': 'Started' | 'Connected'; +} : +Type extends EventTypes.CONTRACT_INTERACTION ? { + 'Method type': 'Read' | 'Write'; + 'Method name': string; +} : +Type extends EventTypes.CONTRACT_VERIFICATION ? { + 'Method': string; + 'Status': 'Method selected' | 'Finished'; +} : undefined; /* eslint-enable @typescript-eslint/indent */ diff --git a/ui/address/contract/ContractConnectWallet.tsx b/ui/address/contract/ContractConnectWallet.tsx index 5832fa566a..eab09c0eaf 100644 --- a/ui/address/contract/ContractConnectWallet.tsx +++ b/ui/address/contract/ContractConnectWallet.tsx @@ -4,11 +4,11 @@ import React from 'react'; import { useAccount, useDisconnect } from 'wagmi'; import useIsMobile from 'lib/hooks/useIsMobile'; +import * as mixpanel from 'lib/mixpanel/index'; import AddressEntity from 'ui/shared/entities/address/AddressEntity'; const ContractConnectWallet = () => { const { open, isOpen } = useWeb3Modal(); - const { address, isDisconnected } = useAccount(); const { disconnect } = useDisconnect(); const isMobile = useIsMobile(); const [ isModalOpening, setIsModalOpening ] = React.useState(false); @@ -17,12 +17,19 @@ const ContractConnectWallet = () => { setIsModalOpening(true); await open(); setIsModalOpening(false); + mixpanel.logEvent(mixpanel.EventTypes.WALLET_CONNECT, { Status: 'Started' }); }, [ open ]); + const handleAccountConnected = React.useCallback(({ isReconnected }: { isReconnected: boolean }) => { + !isReconnected && mixpanel.logEvent(mixpanel.EventTypes.WALLET_CONNECT, { Status: 'Connected' }); + }, []); + const handleDisconnect = React.useCallback(() => { disconnect(); }, [ disconnect ]); + const { address, isDisconnected } = useAccount({ onConnect: handleAccountConnected }); + const content = (() => { if (isDisconnected || !address) { return ( diff --git a/ui/address/contract/ContractMethodCallable.tsx b/ui/address/contract/ContractMethodCallable.tsx index b2b91afaeb..f08a908088 100644 --- a/ui/address/contract/ContractMethodCallable.tsx +++ b/ui/address/contract/ContractMethodCallable.tsx @@ -8,6 +8,7 @@ import type { MethodFormFields, ContractMethodCallResult } from './types'; import type { SmartContractMethodInput, SmartContractMethod } from 'types/api/contract'; import arrowIcon from 'icons/arrows/down-right.svg'; +import * as mixpanel from 'lib/mixpanel/index'; import ContractMethodField from './ContractMethodField'; @@ -105,8 +106,14 @@ const ContractMethodCallable = ({ data, onSubmit, .catch((error) => { setResult(error?.error || error?.data || (error?.reason && { message: error.reason }) || error); setLoading(false); + }) + .finally(() => { + mixpanel.logEvent(mixpanel.EventTypes.CONTRACT_INTERACTION, { + 'Method type': isWrite ? 'Write' : 'Read', + 'Method name': 'name' in data ? data.name : 'Fallback', + }); }); - }, [ onSubmit, data, inputs ]); + }, [ inputs, onSubmit, data, isWrite ]); return ( diff --git a/ui/contractVerification/ContractVerificationForm.tsx b/ui/contractVerification/ContractVerificationForm.tsx index 6ae8f42aef..e6392edfc1 100644 --- a/ui/contractVerification/ContractVerificationForm.tsx +++ b/ui/contractVerification/ContractVerificationForm.tsx @@ -12,6 +12,7 @@ import { route } from 'nextjs-routes'; import useApiFetch from 'lib/api/useApiFetch'; import delay from 'lib/delay'; import useToast from 'lib/hooks/useToast'; +import * as mixpanel from 'lib/mixpanel/index'; import useSocketChannel from 'lib/socket/useSocketChannel'; import useSocketMessage from 'lib/socket/useSocketMessage'; @@ -23,7 +24,7 @@ import ContractVerificationStandardInput from './methods/ContractVerificationSta import ContractVerificationVyperContract from './methods/ContractVerificationVyperContract'; import ContractVerificationVyperMultiPartFile from './methods/ContractVerificationVyperMultiPartFile'; import ContractVerificationVyperStandardInput from './methods/ContractVerificationVyperStandardInput'; -import { prepareRequestBody, formatSocketErrors, getDefaultValues } from './utils'; +import { prepareRequestBody, formatSocketErrors, getDefaultValues, METHOD_LABELS } from './utils'; interface Props { method?: SmartContractVerificationMethod; @@ -38,6 +39,7 @@ const ContractVerificationForm = ({ method: methodFromQuery, config, hash }: Pro }); const { control, handleSubmit, watch, formState, setError, reset } = formApi; const submitPromiseResolver = React.useRef<(value: unknown) => void>(); + const methodNameRef = React.useRef(); const apiFetch = useApiFetch(); const toast = useToast(); @@ -80,6 +82,12 @@ const ContractVerificationForm = ({ method: methodFromQuery, config, hash }: Pro isClosable: true, }); + mixpanel.logEvent( + mixpanel.EventTypes.CONTRACT_VERIFICATION, + { Status: 'Finished', Method: methodNameRef.current || '' }, + { send_immediately: true }, + ); + window.location.assign(route({ pathname: '/address/[hash]', query: { hash, tab: 'contract' } })); }, [ hash, setError, toast ]); @@ -135,6 +143,10 @@ const ContractVerificationForm = ({ method: methodFromQuery, config, hash }: Pro useUpdateEffect(() => { if (methodValue) { reset(getDefaultValues(methodValue, config)); + + const methodName = METHOD_LABELS[methodValue]; + mixpanel.logEvent(mixpanel.EventTypes.CONTRACT_VERIFICATION, { Status: 'Method selected', Method: methodName }); + methodNameRef.current = methodName; } // !!! should run only when method is changed }, [ methodValue ]); From 18893d057a066ed800cbcaa7a2a6d6724e1c5b7b Mon Sep 17 00:00:00 2001 From: tom Date: Fri, 8 Sep 2023 17:58:40 -0300 Subject: [PATCH 4/5] address events --- lib/mixpanel/utils.ts | 8 ++++++++ ui/address/details/AddressFavoriteButton.tsx | 2 ++ ui/address/details/AddressQrCode.tsx | 10 +++++++++- ui/address/tokenSelect/TokenSelect.tsx | 8 +++++++- ui/address/tokenSelect/TokenSelectButton.tsx | 3 +++ 5 files changed, 29 insertions(+), 2 deletions(-) diff --git a/lib/mixpanel/utils.ts b/lib/mixpanel/utils.ts index f3f2146a73..f644e57037 100644 --- a/lib/mixpanel/utils.ts +++ b/lib/mixpanel/utils.ts @@ -11,6 +11,8 @@ export enum EventTypes { WALLET_CONNECT = 'Wallet connect', CONTRACT_INTERACTION = 'Contract interaction', CONTRACT_VERIFICATION = 'Contract verification', + QR_CODE = 'QR code', + PAGE_WIDGET = 'Page widget', } /* eslint-disable @typescript-eslint/indent */ @@ -69,5 +71,11 @@ Type extends EventTypes.CONTRACT_VERIFICATION ? { 'Method': string; 'Status': 'Method selected' | 'Finished'; } : +Type extends EventTypes.QR_CODE ? { + 'Page type': string; +} : +Type extends EventTypes.PAGE_WIDGET ? { + 'Type': 'Tokens dropdown' | 'Tokens show all (icon)' | 'Add to watchlist'; +} : undefined; /* eslint-enable @typescript-eslint/indent */ diff --git a/ui/address/details/AddressFavoriteButton.tsx b/ui/address/details/AddressFavoriteButton.tsx index d36feff27d..b9d0453389 100644 --- a/ui/address/details/AddressFavoriteButton.tsx +++ b/ui/address/details/AddressFavoriteButton.tsx @@ -8,6 +8,7 @@ import starOutlineIcon from 'icons/star_outline.svg'; import { getResourceKey } from 'lib/api/useApiQuery'; import useIsAccountActionAllowed from 'lib/hooks/useIsAccountActionAllowed'; import usePreventFocusAfterModalClosing from 'lib/hooks/usePreventFocusAfterModalClosing'; +import * as mixpanel from 'lib/mixpanel/index'; import WatchlistAddModal from 'ui/watchlist/AddressModal/AddressModal'; import DeleteAddressModal from 'ui/watchlist/DeleteAddressModal'; @@ -29,6 +30,7 @@ const AddressFavoriteButton = ({ className, hash, watchListId }: Props) => { return; } watchListId ? deleteModalProps.onOpen() : addModalProps.onOpen(); + !watchListId && mixpanel.logEvent(mixpanel.EventTypes.PAGE_WIDGET, { Type: 'Add to watchlist' }); }, [ isAccountActionAllowed, watchListId, deleteModalProps, addModalProps ]); const handleAddOrDeleteSuccess = React.useCallback(async() => { diff --git a/ui/address/details/AddressQrCode.tsx b/ui/address/details/AddressQrCode.tsx index 8583d11e2d..0257303bf1 100644 --- a/ui/address/details/AddressQrCode.tsx +++ b/ui/address/details/AddressQrCode.tsx @@ -14,11 +14,14 @@ import { Skeleton, } from '@chakra-ui/react'; import * as Sentry from '@sentry/react'; +import { useRouter } from 'next/router'; import QRCode from 'qrcode'; import React from 'react'; import qrCodeIcon from 'icons/qr_code.svg'; import useIsMobile from 'lib/hooks/useIsMobile'; +import getPageType from 'lib/mixpanel/getPageType'; +import * as mixpanel from 'lib/mixpanel/index'; const SVG_OPTIONS = { margin: 0, @@ -33,9 +36,13 @@ interface Props { const AddressQrCode = ({ hash, className, isLoading }: Props) => { const { isOpen, onOpen, onClose } = useDisclosure(); const isMobile = useIsMobile(); + const router = useRouter(); + const [ qr, setQr ] = React.useState(''); const [ error, setError ] = React.useState(''); + const pageType = getPageType(router.pathname); + React.useEffect(() => { if (isOpen) { QRCode.toString(hash, SVG_OPTIONS, (error: Error | null | undefined, svg: string) => { @@ -47,9 +54,10 @@ const AddressQrCode = ({ hash, className, isLoading }: Props) => { setError(''); setQr(svg); + mixpanel.logEvent(mixpanel.EventTypes.QR_CODE, { 'Page type': pageType }); }); } - }, [ hash, isOpen, onClose ]); + }, [ hash, isOpen, onClose, pageType ]); if (isLoading) { return ; diff --git a/ui/address/tokenSelect/TokenSelect.tsx b/ui/address/tokenSelect/TokenSelect.tsx index 314bb74918..9c54d9096e 100644 --- a/ui/address/tokenSelect/TokenSelect.tsx +++ b/ui/address/tokenSelect/TokenSelect.tsx @@ -11,6 +11,7 @@ import type { Address } from 'types/api/address'; import walletIcon from 'icons/wallet.svg'; import { getResourceKey } from 'lib/api/useApiQuery'; import useIsMobile from 'lib/hooks/useIsMobile'; +import * as mixpanel from 'lib/mixpanel/index'; import getQueryParamString from 'lib/router/getQueryParamString'; import useSocketChannel from 'lib/socket/useSocketChannel'; import useSocketMessage from 'lib/socket/useSocketMessage'; @@ -38,6 +39,11 @@ const TokenSelect = ({ onClick }: Props) => { const tokensResourceKey = getResourceKey('address_tokens', { pathParams: { hash: addressQueryData?.hash }, queryParams: { type: 'ERC-20' } }); const tokensIsFetching = useIsFetching({ queryKey: tokensResourceKey }); + const handleIconButtonClick = React.useCallback(() => { + mixpanel.logEvent(mixpanel.EventTypes.PAGE_WIDGET, { Type: 'Tokens show all (icon)' }); + onClick?.(); + }, [ onClick ]); + const handleTokenBalanceMessage: SocketMessage.AddressTokenBalance['handler'] = React.useCallback((payload) => { if (payload.block_number !== blockNumber) { refetch(); @@ -97,7 +103,7 @@ const TokenSelect = ({ onClick }: Props) => { pr="6px" icon={ } as="a" - onClick={ onClick } + onClick={ handleIconButtonClick } /> diff --git a/ui/address/tokenSelect/TokenSelectButton.tsx b/ui/address/tokenSelect/TokenSelectButton.tsx index c175326270..03976be637 100644 --- a/ui/address/tokenSelect/TokenSelectButton.tsx +++ b/ui/address/tokenSelect/TokenSelectButton.tsx @@ -5,6 +5,7 @@ import type { FormattedData } from './types'; import arrowIcon from 'icons/arrows/east-mini.svg'; import tokensIcon from 'icons/tokens.svg'; +import * as mixpanel from 'lib/mixpanel/index'; import { getTokensTotalInfo } from '../utils/tokenUtils'; @@ -25,6 +26,8 @@ const TokenSelectButton = ({ isOpen, isLoading, onClick, data }: Props, ref: Rea if (isLoading && !isOpen) { return; } + + mixpanel.logEvent(mixpanel.EventTypes.PAGE_WIDGET, { Type: 'Tokens dropdown' }); onClick(); }, [ isLoading, isOpen, onClick ]); From 955448b22042722b850128fb6448313f77c7b61f Mon Sep 17 00:00:00 2001 From: tom Date: Mon, 11 Sep 2023 12:18:06 -0300 Subject: [PATCH 5/5] event for more button --- lib/mixpanel/utils.ts | 2 +- ui/shared/AddressActions/Menu.tsx | 6 ++++++ 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/lib/mixpanel/utils.ts b/lib/mixpanel/utils.ts index f644e57037..f2dff64716 100644 --- a/lib/mixpanel/utils.ts +++ b/lib/mixpanel/utils.ts @@ -75,7 +75,7 @@ Type extends EventTypes.QR_CODE ? { 'Page type': string; } : Type extends EventTypes.PAGE_WIDGET ? { - 'Type': 'Tokens dropdown' | 'Tokens show all (icon)' | 'Add to watchlist'; + 'Type': 'Tokens dropdown' | 'Tokens show all (icon)' | 'Add to watchlist' | 'Address actions (more button)'; } : undefined; /* eslint-enable @typescript-eslint/indent */ diff --git a/ui/shared/AddressActions/Menu.tsx b/ui/shared/AddressActions/Menu.tsx index 3303dbd3a9..0d4e74746d 100644 --- a/ui/shared/AddressActions/Menu.tsx +++ b/ui/shared/AddressActions/Menu.tsx @@ -5,6 +5,7 @@ import React from 'react'; import config from 'configs/app'; import iconArrow from 'icons/arrows/east-mini.svg'; import useIsAccountActionAllowed from 'lib/hooks/useIsAccountActionAllowed'; +import * as mixpanel from 'lib/mixpanel/index'; import getQueryParamString from 'lib/router/getQueryParamString'; import PrivateTagMenuItem from './PrivateTagMenuItem'; @@ -22,6 +23,10 @@ const AddressActions = ({ isLoading }: Props) => { const isTokenPage = router.pathname === '/token/[hash]'; const isAccountActionAllowed = useIsAccountActionAllowed(); + const handleButtonClick = React.useCallback(() => { + mixpanel.logEvent(mixpanel.EventTypes.PAGE_WIDGET, { Type: 'Address actions (more button)' }); + }, []); + return ( @@ -29,6 +34,7 @@ const AddressActions = ({ isLoading }: Props) => { as={ Button } size="sm" variant="outline" + onClick={ handleButtonClick } > More