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

track walletconnect failed requests #6304

Open
wants to merge 15 commits into
base: develop
Choose a base branch
from
49 changes: 32 additions & 17 deletions src/__swaps__/screens/Swap/hooks/useSearchCurrencyLists.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,13 +8,14 @@ import { useFavorites } from '@/resources/favorites';
import { useSwapsStore } from '@/state/swaps/swapsStore';
import { isAddress } from '@ethersproject/address';
import { rankings } from 'match-sorter';
import { useCallback, useMemo, useState } from 'react';
import { useCallback, useEffect, useMemo, useState } from 'react';
import { runOnJS, useAnimatedReaction } from 'react-native-reanimated';
import { useDebouncedCallback } from 'use-debounce';
import { TokenToBuyListItem } from '../components/TokenList/TokenToBuyList';
import { useSwapContext } from '../providers/swap-provider';
import { RecentSwap } from '@/__swaps__/types/swap';
import { useTokenDiscovery } from '../resources/search';
import { analyticsV2 } from '@/analytics';

export type AssetToBuySectionId = 'bridge' | 'recent' | 'favorites' | 'verified' | 'unverified' | 'other_networks' | 'popular';

Expand Down Expand Up @@ -422,28 +423,29 @@ export function useSearchCurrencyLists() {
}
);

return useMemo(() => {
const searchCurrencyLists = useMemo(() => {
const toChainId = selectedOutputChainId.value ?? ChainId.mainnet;
const bridgeResult = memoizedData.filteredBridgeAsset ?? undefined;
const crosschainMatches = query === '' ? undefined : verifiedAssets?.filter(asset => asset.chainId !== toChainId);
const verifiedResults = query === '' ? verifiedAssets : verifiedAssets?.filter(asset => asset.chainId === toChainId);
const unverifiedResults = memoizedData.enableUnverifiedSearch ? unverifiedAssets : undefined;

return {
results: buildListSectionsData({
combinedData: {
bridgeAsset: bridgeResult,
crosschainExactMatches: crosschainMatches,
unverifiedAssets: unverifiedResults,
verifiedAssets: verifiedResults,
recentSwaps: recentsForChain,
popularAssets: popularAssetsForChain,
},
favoritesList,
filteredBridgeAssetAddress: memoizedData.filteredBridgeAsset?.address,
}),
isLoading: isLoadingVerifiedAssets || isLoadingUnverifiedAssets || isLoadingPopularAssets,
};
const results = buildListSectionsData({
combinedData: {
bridgeAsset: bridgeResult,
crosschainExactMatches: crosschainMatches,
unverifiedAssets: unverifiedResults,
verifiedAssets: verifiedResults,
recentSwaps: recentsForChain,
popularAssets: popularAssetsForChain,
},
favoritesList,
filteredBridgeAssetAddress: memoizedData.filteredBridgeAsset?.address,
});

const isLoading = isLoadingVerifiedAssets || isLoadingUnverifiedAssets || isLoadingPopularAssets;

return { results, isLoading };
}, [
favoritesList,
isLoadingUnverifiedAssets,
Expand All @@ -458,4 +460,17 @@ export function useSearchCurrencyLists() {
recentsForChain,
popularAssetsForChain,
]);

useEffect(() => {
if (searchCurrencyLists.isLoading) return;
const params = { screen: 'swap' as const, total_tokens: 0, no_icon: 0, query };
for (const assetOrHeader of searchCurrencyLists.results) {
if (assetOrHeader.listItemType === 'header') continue;
if (!assetOrHeader.icon_url) params.no_icon += 1;
params.total_tokens += 1;
}
analyticsV2.track(analyticsV2.event.tokenList, params);
}, [searchCurrencyLists.results, searchCurrencyLists.isLoading, query]);

return searchCurrencyLists;
}
19 changes: 19 additions & 0 deletions src/analytics/event.ts
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,8 @@ export const event = {
wcNewSessionApproved: 'Approved new WalletConnect session',
wcShowingSigningRequest: 'Showing Walletconnect signing request',

wcRequestFailed: 'wc.failed_request',

nftOffersOpenedOffersSheet: 'Opened NFT Offers Sheet',
nftOffersOpenedSingleOfferSheet: 'Opened NFT Single Offer Sheet',
nftOffersViewedExternalOffer: 'Viewed external NFT Offer',
Expand Down Expand Up @@ -169,6 +171,9 @@ export const event = {
tokenDetailsErc20: 'token_details.erc20',
tokenDetailsNFT: 'token_details.nft',

// token lists (wallet, swap, send)
tokenList: 'token_list',

// trending tokens
viewTrendingToken: 'trending_tokens.view_trending_token',
viewRankedCategory: 'trending_tokens.view_ranked_category',
Expand Down Expand Up @@ -372,6 +377,12 @@ export type EventProperties = {
dappName: string;
dappUrl: string;
};
[event.wcRequestFailed]: {
type: 'session_proposal' | 'session_request' | 'read only wallet' | 'method not supported' | 'invalid namespaces' | 'dapp browser';
reason: string;
method?: string;
};

[event.nftOffersOpenedOffersSheet]: {
entryPoint: string;
};
Expand Down Expand Up @@ -717,6 +728,14 @@ export type EventProperties = {
available_data: { description: boolean; image_url: boolean; floorPrice: boolean };
};

[event.tokenList]: {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

wondering if the properties associated with this event should be distinct user properties instead. wdyt @DanielSinclair

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This approach is fine, I think. We generally just want to understand how often users are seeing assets missing metadata on various screens. We can group those back to uniques to understand number of users if we need to.

screen: 'wallet' | 'swap' | 'send' | 'discover';
total_tokens: number;
no_icon: number;
no_price?: number;
query?: string; // query is only sent for the swap screen
};

[event.viewTrendingToken]: {
address: TrendingToken['address'];
chainId: TrendingToken['chainId'];
Expand Down
7 changes: 3 additions & 4 deletions src/components/expanded-state/UniqueTokenExpandedState.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -417,18 +417,17 @@ const UniqueTokenExpandedState = ({ asset: passedAsset, external }: UniqueTokenE

const hideNftMarketplaceAction = isPoap || !slug;

const mountedAt = useRef(Date.now());
useTimeoutEffect(
() => {
({ elapsedTime }) => {
const { address, chainId } = getAddressAndChainIdFromUniqueId(uniqueId);
const { name, description, image_url } = asset;
analyticsV2.track(analyticsV2.event.tokenDetailsNFT, {
eventSentAfterMs: Date.now() - mountedAt.current,
eventSentAfterMs: elapsedTime,
token: { isPoap, isParty: !!isParty, isENS, address, chainId, name, image_url },
available_data: { description: !!description, image_url: !!image_url, floorPrice: !!offer?.floorPrice },
});
},
5 * 1000 // 5s
{ timeout: 5 * 1000 }
);
return (
<>
Expand Down
7 changes: 3 additions & 4 deletions src/components/expanded-state/asset/ChartExpandedState.js
Original file line number Diff line number Diff line change
Expand Up @@ -257,17 +257,16 @@ export default function ChartExpandedState({ asset }) {
[nativeCurrency]
);

const mountedAt = useRef(Date.now());
useTimeoutEffect(
() => {
({ elapsedTime }) => {
const { address, chainId, symbol, name, icon_url, price } = assetWithPrice;
analyticsV2.track(analyticsV2.event.tokenDetailsErc20, {
eventSentAfterMs: Date.now() - mountedAt.current,
eventSentAfterMs: elapsedTime,
token: { address, chainId, symbol, name, icon_url, price },
available_data: { chart: showChart, description: !!data?.description, iconUrl: !!icon_url },
});
},
5 * 1000 // 5s
{ timeout: 5 * 1000 }
);

return (
Expand Down
24 changes: 18 additions & 6 deletions src/hooks/useTimeout.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,20 +18,32 @@ export default function useTimeout(): [(func: () => void, ms?: number) => void,
return [start, stop, handle];
}

export function useTimeoutEffect(onTimeout: (cancelled: boolean) => void, delay: number) {
export function useTimeoutEffect(
onTimeout: (e: { cancelled: boolean; elapsedTime: number }) => void,
{ timeout, enabled = true }: { timeout: number; enabled?: boolean }
) {
const callback = useRef(onTimeout);
useLayoutEffect(() => {
callback.current = onTimeout;
}, [onTimeout]);

const timeoutRef = useRef<NodeJS.Timeout>();
useEffect(() => {
if (!enabled) return;
const startedAt = Date.now();
timeoutRef.current = setTimeout(() => callback.current(false), delay);
const timeout = timeoutRef.current;
timeoutRef.current = setTimeout(() => {
callback.current({
cancelled: false,
elapsedTime: Date.now() - startedAt,
});
}, timeout);
return () => {
clearTimeout(timeout);
if (Date.now() - startedAt < delay) callback.current(true);
if (!timeoutRef.current) return;
clearTimeout(timeoutRef.current);
const elapsedTime = Date.now() - startedAt;
if (elapsedTime < timeout) {
callback.current({ cancelled: true, elapsedTime });
}
};
}, [delay]);
}, [timeout, enabled]);
}
10 changes: 10 additions & 0 deletions src/hooks/useWalletSectionsData.ts
Original file line number Diff line number Diff line change
Expand Up @@ -123,6 +123,16 @@ export default function useWalletSectionsData({

const { isCoinListEdited } = useCoinListEdited();

useEffect(() => {
if (isLoadingUserAssets || type !== 'wallet') return;
const params = { screen: 'wallet' as const, no_icon: 0, no_price: 0, total_tokens: sortedAssets.length };
for (const asset of sortedAssets) {
if (!asset.icon_url) params.no_icon += 1;
if (!asset.price?.relative_change_24h) params.no_price += 1;
}
analyticsV2.track(analyticsV2.event.tokenList, params);
}, [isLoadingUserAssets, sortedAssets, type]);

const walletSections = useMemo(() => {
const accountInfo = {
hiddenAssets,
Expand Down
10 changes: 9 additions & 1 deletion src/screens/NoNeedWCSheet.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import * as lang from '@/languages';
import React, { useCallback } from 'react';
import React, { useCallback, useEffect } from 'react';
import { Centered } from '../components/layout';
import { Sheet, SheetActionButton } from '../components/sheet';
import { Text } from '../components/text';
Expand All @@ -9,6 +9,7 @@ import { useTheme } from '@/theme';
import { Colors } from '../styles/colors';
import { Box } from '@/design-system';
import { useRoute } from '@react-navigation/native';
import { analyticsV2 } from '@/analytics';

const BodyText = styled(Text).attrs(({ theme: { colors } }: { theme: { colors: Colors } }) => ({
align: 'center',
Expand All @@ -25,6 +26,13 @@ const WalletConnectRedirectSheet = () => {
const { goBack } = useNavigation();
const { params } = useRoute();

useEffect(() => {
analyticsV2.track(analyticsV2.event.wcRequestFailed, {
type: 'dapp browser',
reason: 'tried to connect with WalletConnect in the dapp browser',
});
}, []);

const handleOnPress = useCallback(() => {
(params as { cb?: () => void })?.cb?.();
goBack();
Expand Down
13 changes: 12 additions & 1 deletion src/screens/SendSheet.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ import { SendAssetForm, SendAssetList, SendContactList, SendHeader } from '../co
import { SheetActionButton } from '../components/sheet';
import { getDefaultCheckboxes } from './SendConfirmationSheet';
import { WrappedAlert as Alert } from '@/helpers/alert';
import { analytics } from '@/analytics';
import { analytics, analyticsV2 } from '@/analytics';
import { PROFILES, useExperimentalFlag } from '@/config';
import { AssetTypes, NewTransaction, ParsedAddressAsset, TransactionStatus, UniqueAsset } from '@/entities';
import { isNativeAsset } from '@/handlers/assets';
Expand Down Expand Up @@ -129,6 +129,7 @@ type OnSubmitProps = {
export default function SendSheet() {
const { goBack, navigate } = useNavigation();
const sortedAssets = useUserAssetsStore(state => state.legacyUserAssets);
const isLoadingUserAssets = useUserAssetsStore(state => state.isLoadingUserAssets);
const {
gasFeeParamsBySpeed,
gasLimit,
Expand Down Expand Up @@ -894,6 +895,16 @@ export default function SendSheet() {
isUniqueAsset,
]);

useEffect(() => {
if (isLoadingUserAssets || !sortedAssets) return;
const params = { screen: 'wallet' as const, no_icon: 0, no_price: 0, total_tokens: sortedAssets.length };
for (const asset of sortedAssets) {
if (!asset.icon_url) params.no_icon += 1;
if (!asset.price?.relative_change_24h) params.no_price += 1;
}
analyticsV2.track(analyticsV2.event.tokenList, params);
}, [isLoadingUserAssets, sortedAssets]);

const sendContactListDataKey = useMemo(() => `${ensSuggestions?.[0]?.address || '_'}`, [ensSuggestions]);

const isEmptyWallet = !sortedAssets?.length && !sendableUniqueTokens?.length;
Expand Down
22 changes: 21 additions & 1 deletion src/walletConnect/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ import { toUtf8String } from '@ethersproject/strings';
import { logger, RainbowError } from '@/logger';
import Navigation, { getActiveRoute } from '@/navigation/Navigation';
import Routes from '@/navigation/routesNames';
import { analyticsV2 as analytics } from '@/analytics';
import { analyticsV2 as analytics, analyticsV2 } from '@/analytics';
import { maybeSignUri } from '@/handlers/imgix';
import Alert from '@/components/alerts/Alert';
import * as lang from '@/languages';
Expand Down Expand Up @@ -541,6 +541,8 @@ export async function onSessionProposal(proposal: WalletKitTypes.SessionProposal
reason: 'INVALID_SESSION_SETTLE_REQUEST',
});

analyticsV2.track(analyticsV2.event.wcRequestFailed, { type: `invalid namespaces`, reason: namespaces.error.message });

showErrorSheet({
title: lang.t(T.errors.generic_title),
body: `${lang.t(T.errors.namespaces_invalid)} \n \n ${namespaces.error.message}`,
Expand Down Expand Up @@ -650,6 +652,11 @@ export async function onSessionRequest(event: SignClientTypes.EventArguments['se
message,
});

analyticsV2.track(analyticsV2.event.wcRequestFailed, {
type: 'session_request',
reason: 'session_request exited, signing request had no address and/or messsage',
});

await client.respondSessionRequest({
topic,
response: formatJsonRpcError(id, `Invalid RPC params`),
Expand Down Expand Up @@ -680,6 +687,11 @@ export async function onSessionRequest(event: SignClientTypes.EventArguments['se

const errorMessageBody = isReadOnly ? lang.t(T.errors.read_only_wallet_on_signing_method) : lang.t(T.errors.generic_error);

analyticsV2.track(analyticsV2.event.wcRequestFailed, {
type: 'read only wallet',
reason: 'session_request exited, selectedWallet was falsy or read only',
});

await client.respondSessionRequest({
topic,
response: formatJsonRpcError(id, `Wallet is read-only`),
Expand All @@ -703,6 +715,8 @@ export async function onSessionRequest(event: SignClientTypes.EventArguments['se
if (!session) {
logger.error(new RainbowError(`[walletConnect]: session_request topic was not found`));

analyticsV2.track(analyticsV2.event.wcRequestFailed, { type: 'session_request', reason: 'session_request topic was not found' });

await client.respondSessionRequest({
topic,
response: formatJsonRpcError(id, `Session not found`),
Expand Down Expand Up @@ -768,6 +782,12 @@ export async function onSessionRequest(event: SignClientTypes.EventArguments['se
method,
});

analyticsV2.track(analyticsV2.event.wcRequestFailed, {
type: `method not supported`,
reason: 'received unsupported session_request RPC method',
method: method,
});

try {
await client.respondSessionRequest({
topic,
Expand Down
Loading