Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

More events for mixpanel analytics #1160

Merged
merged 5 commits into from
Sep 19, 2023
Merged
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
2 changes: 1 addition & 1 deletion lib/mixpanel/getPageType.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import type { Route } from 'nextjs-routes';

const PAGE_TYPE_DICT: Record<Route['pathname'], string> = {
export const PAGE_TYPE_DICT: Record<Route['pathname'], string> = {
'/': 'Homepage',
'/txs': 'Transactions',
'/tx/[hash]': 'Transaction details',
Expand Down
5 changes: 4 additions & 1 deletion lib/mixpanel/useLogPageView.tsx
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { useColorMode } from '@chakra-ui/react';
import { usePathname } from 'next/navigation';
import { useRouter } from 'next/router';
import React from 'react';
Expand All @@ -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) {
Expand All @@ -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 ]);
}
48 changes: 48 additions & 0 deletions lib/mixpanel/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,15 @@ export enum EventTypes {
PAGE_VIEW = 'Page view',
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',
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 */
Expand All @@ -13,6 +22,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;
Expand All @@ -29,5 +39,43 @@ Type extends EventTypes.ADD_TO_WALLET ? (
'Token': string;
}
) :
Type extends EventTypes.ACCOUNT_ACCESS ? {
'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';
} :
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';
} :
Type extends EventTypes.QR_CODE ? {
'Page type': string;
} :
Type extends EventTypes.PAGE_WIDGET ? {
'Type': 'Tokens dropdown' | 'Tokens show all (icon)' | 'Add to watchlist' | 'Address actions (more button)';
} :
undefined;
/* eslint-enable @typescript-eslint/indent */
9 changes: 8 additions & 1 deletion ui/address/contract/ContractConnectWallet.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand All @@ -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 (
Expand Down
9 changes: 8 additions & 1 deletion ui/address/contract/ContractMethodCallable.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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';

Expand Down Expand Up @@ -105,8 +106,14 @@ const ContractMethodCallable = <T extends SmartContractMethod>({ 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 (
<Box>
Expand Down
2 changes: 2 additions & 0 deletions ui/address/details/AddressFavoriteButton.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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';

Expand All @@ -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() => {
Expand Down
10 changes: 9 additions & 1 deletion ui/address/details/AddressQrCode.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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) => {
Expand All @@ -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 <Skeleton className={ className } w="36px" h="32px" borderRadius="base"/>;
Expand Down
8 changes: 7 additions & 1 deletion ui/address/tokenSelect/TokenSelect.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -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();
Expand Down Expand Up @@ -97,7 +103,7 @@ const TokenSelect = ({ onClick }: Props) => {
pr="6px"
icon={ <Icon as={ walletIcon } boxSize={ 5 }/> }
as="a"
onClick={ onClick }
onClick={ handleIconButtonClick }
/>
</NextLink>
</Box>
Expand Down
3 changes: 3 additions & 0 deletions ui/address/tokenSelect/TokenSelectButton.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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';

Expand All @@ -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 ]);

Expand Down
25 changes: 21 additions & 4 deletions ui/addressVerification/AddressVerificationModal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand All @@ -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<StateData>({ 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);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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<SignMethod>(noWeb3Provider ? 'manual' : 'wallet');

const { open: openWeb3Modal } = useWeb3Modal();
const { isConnected } = useAccount();
Expand Down Expand Up @@ -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);

Expand Down Expand Up @@ -115,7 +117,7 @@ const AddressVerificationStepSignature = ({ address, signingMessage, contractCre
}, [ clearErrors, onSubmit ]);

const button = (() => {
if (signMethod === 'manually') {
if (signMethod === 'manual') {
return (
<Button
size="lg"
Expand Down Expand Up @@ -220,7 +222,7 @@ const AddressVerificationStepSignature = ({ address, signingMessage, contractCre
<Radio value="manually">Sign manually</Radio>
</RadioGroup>
) }
{ signMethod === 'manually' && <AddressVerificationFieldSignature formState={ formState } control={ control }/> }
{ signMethod === 'manual' && <AddressVerificationFieldSignature formState={ formState } control={ control }/> }
</Flex>
<Flex alignItems={{ base: 'flex-start', lg: 'center' }} mt={ 8 } columnGap={ 5 } rowGap={ 2 } flexDir={{ base: 'column', lg: 'row' }}>
{ button }
Expand Down
Loading