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;
}
15 changes: 15 additions & 0 deletions src/analytics/event.ts
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,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 @@ -167,6 +169,9 @@ export const event = {
// token details
tokenDetailsErc20: 'token_details.erc20',
tokenDetailsNFT: 'token_details.nft',

// token lists (wallet, swap, send)
tokenList: 'token_list',
} as const;

type SwapEventParameters<T extends 'swap' | 'crosschainSwap'> = {
Expand Down Expand Up @@ -363,6 +368,8 @@ export type EventProperties = {
dappName: string;
dappUrl: string;
};
[event.wcRequestFailed]: { reason: string };
Copy link
Member

Choose a reason for hiding this comment

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

do we want more metadata associated with the failure here? like dapp url or something

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Copy link
Contributor

@DanielSinclair DanielSinclair Dec 19, 2024

Choose a reason for hiding this comment

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

@greg-schrammel Could we add a type here like session_proposal | sesion_request and then a method param where applicable in addition to the reason. That way we could more easily see that a certain request type or session proposal fails more than others. I tend to find the logger.error above many of these events are a bit easier to read than the reason; can we copy that text to be more detailed? We can pass any optional params or error messages from throws too (don't need to require for every type or method), but looks like the if conditions handle most of the error types that could be thrown


[event.nftOffersOpenedOffersSheet]: {
entryPoint: string;
};
Expand Down Expand Up @@ -694,4 +701,12 @@ export type EventProperties = {
eventSentAfterMs: number;
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
};
};
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 @@ -258,17 +258,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
23 changes: 17 additions & 6 deletions src/hooks/useTimeout.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,20 +18,31 @@ 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);
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 @@ -98,6 +98,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
3 changes: 2 additions & 1 deletion src/resources/assets/useSortedUserAssets.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { useAccountSettings } from '@/hooks';
import { selectSortedUserAssets } from '@/resources/assets/assetSelectors';
import { useUserAssets } from '@/resources/assets/UserAssetsQuery';
import { useConnectedToHardhatStore } from '@/state/connectedToHardhat';
import { useCallback } from 'react';

export function useSortedUserAssets() {
const { accountAddress, nativeCurrency } = useAccountSettings();
Expand All @@ -14,7 +15,7 @@ export function useSortedUserAssets() {
connectedToHardhat,
},
{
select: selectSortedUserAssets(nativeCurrency),
select: useCallback(selectSortedUserAssets(nativeCurrency), [nativeCurrency]),
}
);
}
7 changes: 6 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,10 @@ const WalletConnectRedirectSheet = () => {
const { goBack } = useNavigation();
const { params } = useRoute();

useEffect(() => {
analyticsV2.track(analyticsV2.event.wcRequestFailed, { reason: 'rainbow dapp browser' });
}, []);

const handleOnPress = useCallback(() => {
(params as { cb?: () => void })?.cb?.();
goBack();
Expand Down
14 changes: 12 additions & 2 deletions 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 @@ -117,7 +117,7 @@ type OnSubmitProps = {

export default function SendSheet() {
const { goBack, navigate } = useNavigation();
const { data: sortedAssets } = useSortedUserAssets();
const { isLoading: isLoadingUserAssets, data: sortedAssets } = useSortedUserAssets();
const {
gasFeeParamsBySpeed,
gasLimit,
Expand Down Expand Up @@ -883,6 +883,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
12 changes: 11 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 @@ -540,6 +540,8 @@ export async function onSessionProposal(proposal: WalletKitTypes.SessionProposal
reason: 'INVALID_SESSION_SETTLE_REQUEST',
});

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

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

analyticsV2.track(analyticsV2.event.wcRequestFailed, { reason: 'invalid signing request' });

await client.respondSessionRequest({
topic,
response: formatJsonRpcError(id, `Invalid RPC params`),
Expand Down Expand Up @@ -679,6 +683,8 @@ 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, { reason: 'read only wallet' });

await client.respondSessionRequest({
topic,
response: formatJsonRpcError(id, `Wallet is read-only`),
Expand All @@ -702,6 +708,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, { reason: 'session not found' });

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

analyticsV2.track(analyticsV2.event.wcRequestFailed, { reason: `method not supported: ${method}` });

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