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 token lists #6303

Merged
merged 14 commits into from
Dec 20, 2024
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;
Comment on lines +463 to +475
Copy link
Contributor

Choose a reason for hiding this comment

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

does having a useEffect in here have any negative side effects to re-rendering the list?

If it does, we could mount a separate component that listens to react-query subscriptions and doesn't touch the list.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

no, it doesn't trigger a rerender ever, just sends track events

}
11 changes: 11 additions & 0 deletions src/analytics/event.ts
Original file line number Diff line number Diff line change
Expand Up @@ -167,6 +167,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 @@ -694,4 +697,12 @@ export type EventProperties = {
eventSentAfterMs: number;
available_data: { description: boolean; image_url: boolean; floorPrice: boolean };
};

[event.tokenList]: {
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
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 @@ -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]);

greg-schrammel marked this conversation as resolved.
Show resolved Hide resolved
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]),
}
);
}
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 };
greg-schrammel marked this conversation as resolved.
Show resolved Hide resolved
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
Loading