diff --git a/src/__swaps__/screens/Swap/providers/swap-provider.tsx b/src/__swaps__/screens/Swap/providers/swap-provider.tsx index abefed50f50..9575fb0e54e 100644 --- a/src/__swaps__/screens/Swap/providers/swap-provider.tsx +++ b/src/__swaps__/screens/Swap/providers/swap-provider.tsx @@ -207,7 +207,6 @@ export const SwapProvider = ({ children }: SwapProviderProps) => { const isBridge = swapsStore.getState().inputAsset?.mainnetAddress === swapsStore.getState().outputAsset?.mainnetAddress; const isDegenModeEnabled = swapsStore.getState().degenMode; - const slippage = swapsStore.getState().slippage; const isSwappingToPopularAsset = swapsStore.getState().outputAsset?.sectionId === 'popular'; const selectedGas = getSelectedGas(parameters.chainId); @@ -244,7 +243,6 @@ export const SwapProvider = ({ children }: SwapProviderProps) => { } const gasFeeParamsBySpeed = getGasSettingsBySpeed(parameters.chainId); - const selectedGasSpeed = getSelectedGasSpeed(parameters.chainId); let gasParams: TransactionGasParamAmounts | LegacyTransactionGasParamAmounts = {} as | TransactionGasParamAmounts @@ -282,18 +280,26 @@ export const SwapProvider = ({ children }: SwapProviderProps) => { SwapInputController.quoteFetchingInterval.start(); analyticsV2.track(analyticsV2.event.swapsFailed, { - createdAt: Date.now(), type, - parameters, - selectedGas, - selectedGasSpeed, - slippage, - bridge: isBridge, - errorMessage, - inputNativeValue: SwapInputController.inputValues.value.inputNativeValue, - outputNativeValue: SwapInputController.inputValues.value.outputNativeValue, + isBridge: isBridge, + inputAssetSymbol: internalSelectedInputAsset.value?.symbol || '', + inputAssetName: internalSelectedInputAsset.value?.name || '', + inputAssetAddress: internalSelectedInputAsset.value?.address as AddressOrEth, + inputAssetChainId: internalSelectedInputAsset.value?.chainId || ChainId.mainnet, + inputAssetAmount: parameters.quote.sellAmount as number, + outputAssetSymbol: internalSelectedOutputAsset.value?.symbol || '', + outputAssetName: internalSelectedOutputAsset.value?.name || '', + outputAssetAddress: internalSelectedOutputAsset.value?.address as AddressOrEth, + outputAssetChainId: internalSelectedOutputAsset.value?.chainId || ChainId.mainnet, + outputAssetAmount: parameters.quote.buyAmount as number, + mainnetAddress: (parameters.assetToBuy.chainId === ChainId.mainnet + ? parameters.assetToBuy.address + : parameters.assetToSell.mainnetAddress) as AddressOrEth, + flashbots: parameters.flashbots ?? false, + tradeAmountUSD: parameters.quote.tradeAmountUSD, degenMode: isDegenModeEnabled, isSwappingToPopularAsset, + errorMessage, }); if (errorMessage !== 'handled') { @@ -333,15 +339,23 @@ export const SwapProvider = ({ children }: SwapProviderProps) => { })(Routes.PROFILE_SCREEN, {}); analyticsV2.track(analyticsV2.event.swapsSubmitted, { - createdAt: Date.now(), type, - parameters, - selectedGas, - selectedGasSpeed, - slippage, - bridge: isBridge, - inputNativeValue: SwapInputController.inputValues.value.inputNativeValue, - outputNativeValue: SwapInputController.inputValues.value.outputNativeValue, + isBridge: isBridge, + inputAssetSymbol: internalSelectedInputAsset.value?.symbol || '', + inputAssetName: internalSelectedInputAsset.value?.name || '', + inputAssetAddress: internalSelectedInputAsset.value?.address as AddressOrEth, + inputAssetChainId: internalSelectedInputAsset.value?.chainId || ChainId.mainnet, + inputAssetAmount: parameters.quote.sellAmount as number, + outputAssetSymbol: internalSelectedOutputAsset.value?.symbol || '', + outputAssetName: internalSelectedOutputAsset.value?.name || '', + outputAssetAddress: internalSelectedOutputAsset.value?.address as AddressOrEth, + outputAssetChainId: internalSelectedOutputAsset.value?.chainId || ChainId.mainnet, + outputAssetAmount: parameters.quote.buyAmount as number, + mainnetAddress: (parameters.assetToBuy.chainId === ChainId.mainnet + ? parameters.assetToBuy.address + : parameters.assetToSell.mainnetAddress) as AddressOrEth, + flashbots: parameters.flashbots ?? false, + tradeAmountUSD: parameters.quote.tradeAmountUSD, degenMode: isDegenModeEnabled, isSwappingToPopularAsset, }); diff --git a/src/analytics/event.ts b/src/analytics/event.ts index 5b67f2730ee..0352f86656e 100644 --- a/src/analytics/event.ts +++ b/src/analytics/event.ts @@ -1,13 +1,10 @@ -import { GasSettings } from '@/__swaps__/screens/Swap/hooks/useCustomGas'; -import { ExtendedAnimatedAssetWithColors, ParsedSearchAsset } from '@/__swaps__/types/assets'; +import { AddressOrEth, ExtendedAnimatedAssetWithColors, ParsedSearchAsset } from '@/__swaps__/types/assets'; import { ChainId, Network } from '@/chains/types'; -import { GasSpeed } from '@/__swaps__/types/gas'; import { SwapAssetType } from '@/__swaps__/types/swap'; import { UnlockableAppIconKey } from '@/appIcons/appIcons'; import { CardType } from '@/components/cards/GenericCard'; import { LearnCategory } from '@/components/cards/utils/types'; import { FiatProviderName } from '@/entities/f2c'; -import { RapSwapActionParameters } from '@/raps/references'; import { RequestSource } from '@/utils/requestNavigationHandlers'; import { CrosschainQuote, Quote, QuoteError } from '@rainbow-me/swaps'; import { AnyPerformanceLog, Screen } from '../state/performance/operations'; @@ -149,18 +146,28 @@ export const event = { performanceTimeToSign: 'performance.time_to_sign', performanceTimeToSignOperation: 'performance.time_to_sign.operation', + + addFavoriteToken: 'add_favorite_token', + watchWallet: 'watch_wallet', + watchedWalletCohort: 'watched_wallet_cohort', } as const; type SwapEventParameters = { - createdAt: number; type: T; - bridge: boolean; - inputNativeValue: string | number; - outputNativeValue: string | number; - parameters: Omit, 'gasParams' | 'gasFeeParamsBySpeed' | 'selectedGasFee'>; - selectedGas: GasSettings; - selectedGasSpeed: GasSpeed; - slippage: string; + isBridge: boolean; + inputAssetSymbol: string; + inputAssetName: string; + inputAssetAddress: AddressOrEth; + inputAssetChainId: ChainId; + inputAssetAmount: number; + outputAssetSymbol: string; + outputAssetName: string; + outputAssetAddress: AddressOrEth; + outputAssetChainId: ChainId; + outputAssetAmount: number; + mainnetAddress: string; + flashbots: boolean; + tradeAmountUSD: number; degenMode: boolean; isSwappingToPopularAsset: boolean; }; @@ -571,4 +578,21 @@ export type EventProperties = { }; [event.performanceTimeToSignOperation]: AnyPerformanceLog; + + [event.addFavoriteToken]: { + address: AddressOrEth; + chainId: ChainId; + name: string; + symbol: string; + }; + + [event.watchWallet]: { + addressOrEnsName: string; + address: string; + }; + + [event.watchedWalletCohort]: { + numWatchedWallets: number; + watchedWalletsAddresses: string[]; + }; }; diff --git a/src/helpers/runWatchedWalletCohort.ts b/src/helpers/runWatchedWalletCohort.ts new file mode 100644 index 00000000000..bea778eb997 --- /dev/null +++ b/src/helpers/runWatchedWalletCohort.ts @@ -0,0 +1,29 @@ +import { useWallets } from '@/hooks'; +import walletTypes from './walletTypes'; +import { useEffect } from 'react'; +import { analyticsV2 } from '@/analytics'; +import * as ls from '@/storage'; + +const WATCHED_WALLET_COHORT_INTERVAL = 1000 * 60 * 60 * 24; // 1 day between cohort reports + +export function useRunWatchedWalletCohort() { + const { wallets } = useWallets(); + + useEffect(() => { + const watchedWallets = Object.values(wallets || {}).filter(wallet => wallet.type === walletTypes.readOnly); + if (!watchedWallets.length) { + return; + } + + const lastReported = ls.watchedWalletCohort.get(['lastReported']); + if (lastReported && Date.now() - lastReported < WATCHED_WALLET_COHORT_INTERVAL) { + return; + } + + ls.watchedWalletCohort.set(['lastReported'], Date.now()); + analyticsV2.track(analyticsV2.event.watchedWalletCohort, { + numWatchedWallets: watchedWallets.length, + watchedWalletsAddresses: watchedWallets.flatMap(wallet => wallet.addresses.map(acc => acc.address)), + }); + }, [wallets]); +} diff --git a/src/hooks/useApplicationSetup.ts b/src/hooks/useApplicationSetup.ts index 0ce196b74b8..7a07034cdc1 100644 --- a/src/hooks/useApplicationSetup.ts +++ b/src/hooks/useApplicationSetup.ts @@ -14,10 +14,13 @@ import { initListeners as initWalletConnectListeners, initWalletConnectPushNotif import isTestFlight from '@/helpers/isTestFlight'; import { PerformanceTracking } from '@/performance/tracking'; import { PerformanceMetrics } from '@/performance/tracking/types/PerformanceMetrics'; +import { useRunWatchedWalletCohort } from '@/helpers/runWatchedWalletCohort'; export function useApplicationSetup() { const [initialRoute, setInitialRoute] = useState(null); + useRunWatchedWalletCohort(); + const identifyFlow = useCallback(async () => { const address = await loadAddress(); if (address) { diff --git a/src/hooks/useImportingWallet.ts b/src/hooks/useImportingWallet.ts index bd48b4bab07..e78fa43a2e8 100644 --- a/src/hooks/useImportingWallet.ts +++ b/src/hooks/useImportingWallet.ts @@ -14,7 +14,7 @@ import usePrevious from './usePrevious'; import useWalletENSAvatar from './useWalletENSAvatar'; import useWallets from './useWallets'; import { WrappedAlert as Alert } from '@/helpers/alert'; -import { analytics } from '@/analytics'; +import { analytics, analyticsV2 } from '@/analytics'; import { PROFILES, useExperimentalFlag } from '@/config'; import { fetchReverseRecord } from '@/handlers/ens'; import { getProvider, isValidBluetoothDeviceId, resolveUnstoppableDomain } from '@/handlers/web3'; @@ -110,7 +110,19 @@ export default function useImportingWallet({ showImportModal = true } = {}) { ); const handlePressImportButton = useCallback( - async (forceColor: any, forceAddress: any, forceEmoji: any = null, avatarUrl: any) => { + async ({ + forceColor, + forceAddress = '', + forceEmoji, + avatarUrl, + type = 'import', + }: { + forceColor?: string | number; + forceAddress?: string; + forceEmoji?: string; + avatarUrl?: string; + type?: 'import' | 'watch'; + } = {}) => { setBusy(true); analytics.track('Tapped "Import" button'); // guard against pressEvent coming in as forceColor if @@ -135,9 +147,17 @@ export default function useImportingWallet({ showImportModal = true } = {}) { } setResolvedAddress(address); name = forceEmoji ? `${forceEmoji} ${input}` : input; - avatarUrl = avatarUrl || (avatar && avatar?.imageUrl); + const finalAvatarUrl = avatarUrl || (avatar && avatar?.imageUrl); setBusy(false); - startImportProfile(name, guardedForceColor, address, avatarUrl); + + if (type === 'watch') { + analyticsV2.track(analyticsV2.event.watchWallet, { + addressOrEnsName: input, // ENS name + address, + }); + } + + startImportProfile(name, guardedForceColor, address, finalAvatarUrl); analytics.track('Show wallet profile modal for ENS address', { address, input, @@ -159,6 +179,14 @@ export default function useImportingWallet({ showImportModal = true } = {}) { setResolvedAddress(address); name = forceEmoji ? `${forceEmoji} ${input}` : input; setBusy(false); + + if (type === 'watch') { + analyticsV2.track(analyticsV2.event.watchWallet, { + addressOrEnsName: input, // unstoppable domain name + address, + }); + } + // @ts-expect-error ts-migrate(2554) FIXME: Expected 4 arguments, but got 3. startImportProfile(name, guardedForceColor, address); analytics.track('Show wallet profile modal for Unstoppable address', { @@ -171,15 +199,18 @@ export default function useImportingWallet({ showImportModal = true } = {}) { return; } } else if (isValidAddress(input)) { + let finalAvatarUrl: string | null | undefined = avatarUrl; + let ens = input; try { - const ens = await fetchReverseRecord(input); + ens = await fetchReverseRecord(input); if (ens && ens !== input) { name = forceEmoji ? `${forceEmoji} ${ens}` : ens; if (!avatarUrl && profilesEnabled) { const avatar = await fetchENSAvatar(name, { swallowError: true }); - avatarUrl = avatar?.imageUrl; + finalAvatarUrl = avatar?.imageUrl; } } + analytics.track('Show wallet profile modal for read only wallet', { ens, input, @@ -188,8 +219,15 @@ export default function useImportingWallet({ showImportModal = true } = {}) { logger.error(new RainbowError(`[useImportingWallet]: Error resolving ENS during wallet import: ${e}`)); } setBusy(false); - // @ts-expect-error ts-migrate(2554) FIXME: Expected 4 arguments, but got 3. - startImportProfile(name, guardedForceColor, input); + + if (type === 'watch') { + analyticsV2.track(analyticsV2.event.watchWallet, { + addressOrEnsName: ens, + address: input, + }); + } + + startImportProfile(name, guardedForceColor, input, finalAvatarUrl); } else { try { setTimeout(async () => { @@ -200,17 +238,26 @@ export default function useImportingWallet({ showImportModal = true } = {}) { return null; } const ens = await fetchReverseRecord(walletResult.address); + let finalAvatarUrl: string | null | undefined = avatarUrl; if (ens && ens !== input) { name = forceEmoji ? `${forceEmoji} ${ens}` : ens; - if (!avatarUrl && profilesEnabled) { + if (!finalAvatarUrl && profilesEnabled) { const avatar = await fetchENSAvatar(name, { swallowError: true, }); - avatarUrl = avatar?.imageUrl; + finalAvatarUrl = avatar?.imageUrl; } } setBusy(false); - startImportProfile(name, guardedForceColor, walletResult.address, avatarUrl); + + if (type === 'watch') { + analyticsV2.track(analyticsV2.event.watchWallet, { + addressOrEnsName: ens, + address: input, + }); + } + + startImportProfile(name, guardedForceColor, walletResult.address, finalAvatarUrl); analytics.track('Show wallet profile modal for imported wallet', { address: walletResult.address, type: walletResult.type, diff --git a/src/resources/favorites.ts b/src/resources/favorites.ts index e64e758be4b..82413ed4ac7 100644 --- a/src/resources/favorites.ts +++ b/src/resources/favorites.ts @@ -9,6 +9,7 @@ import { useQuery } from '@tanstack/react-query'; import { omit } from 'lodash'; import { externalTokenQueryKey, fetchExternalToken } from './assets/externalAssetsQuery'; import { chainsIdByName, chainsName } from '@/chains'; +import { analyticsV2 } from '@/analytics'; export const favoritesQueryKey = createQueryKey('favorites', {}, { persisterVersion: 4 }); @@ -123,6 +124,13 @@ export async function toggleFavorite(address: string, chainId = ChainId.mainnet) queryClient.setQueryData(favoritesQueryKey, omit(favorites, uniqueId)); } else { const metadata = await fetchMetadata([lowercasedAddress], chainId); + analyticsV2.track(analyticsV2.event.addFavoriteToken, { + address: lowercasedAddress, + chainId, + name: metadata[uniqueId].name, + symbol: metadata[uniqueId].symbol, + }); + queryClient.setQueryData(favoritesQueryKey, { ...favorites, ...metadata }); } } diff --git a/src/screens/AddWalletSheet.tsx b/src/screens/AddWalletSheet.tsx index 67c13b27d42..6021f7ad295 100644 --- a/src/screens/AddWalletSheet.tsx +++ b/src/screens/AddWalletSheet.tsx @@ -182,7 +182,7 @@ export const AddWalletSheet = () => { }; const onPressWatch = () => { - analytics.track('Tapped "Add an existing wallet"'); + analytics.track('Tapped "Watch an Ethereum Address"'); analyticsV2.track(analyticsV2.event.addWalletFlowStarted, { isFirstWallet, type: 'watch', diff --git a/src/screens/ImportOrWatchWalletSheet.tsx b/src/screens/ImportOrWatchWalletSheet.tsx index bcdccc4067e..c027b7d888d 100644 --- a/src/screens/ImportOrWatchWalletSheet.tsx +++ b/src/screens/ImportOrWatchWalletSheet.tsx @@ -74,8 +74,7 @@ export const ImportOrWatchWalletSheet = () => { multiline numberOfLines={3} onSubmitEditing={() => { - // @ts-expect-error callback needs refactor - if (isSecretValid) handlePressImportButton(); + if (isSecretValid) handlePressImportButton({ type }); }} placeholder={i18n.t(TRANSLATIONS[type].placeholder)} placeholderTextColor={labelTertiary} diff --git a/src/storage/index.ts b/src/storage/index.ts index e65fefd1396..1014ce03dbf 100644 --- a/src/storage/index.ts +++ b/src/storage/index.ts @@ -1,6 +1,6 @@ import { MMKV } from 'react-native-mmkv'; -import { Account, Cards, Campaigns, Device, Review } from '@/storage/schema'; +import { Account, Cards, Campaigns, Device, Review, WatchedWalletCohort } from '@/storage/schema'; import { EthereumAddress, RainbowTransaction } from '@/entities'; import { SecureStorage } from '@coinbase/mobile-wallet-protocol-host'; import { ChainId } from '@/chains/types'; @@ -135,3 +135,5 @@ export const mwp: SecureStorage = { mwpStorage.remove([key]); }, }; + +export const watchedWalletCohort = new Storage<[], WatchedWalletCohort>({ id: 'watchedWalletCohort' }); diff --git a/src/storage/schema.ts b/src/storage/schema.ts index 2983aa51317..0d1c898fd84 100644 --- a/src/storage/schema.ts +++ b/src/storage/schema.ts @@ -81,3 +81,7 @@ export type Campaigns = CampaignKeys & CampaignMetadata; export type Cards = { [cardKey: string]: boolean; }; + +export type WatchedWalletCohort = { + lastReported: ReturnType; +};