diff --git a/src/components/AmplitudeAnalytics/constants.ts b/src/components/AmplitudeAnalytics/constants.ts index 63b5414fb2f..0a2c25b3bdd 100644 --- a/src/components/AmplitudeAnalytics/constants.ts +++ b/src/components/AmplitudeAnalytics/constants.ts @@ -40,6 +40,8 @@ export enum WALLET_CONNECTION_RESULT { FAILED = 'Failed', } +export const NATIVE_CHAIN_ID = 'NATIVE' + export enum SWAP_PRICE_UPDATE_USER_RESPONSE { ACCEPTED = 'Accepted', REJECTED = 'Rejected', @@ -89,7 +91,6 @@ export const enum ElementName { SWAP_BUTTON = 'swap-button', SWAP_DETAILS_DROPDOWN = 'swap-details-dropdown', SWAP_TOKENS_REVERSE_ARROW_BUTTON = 'swap-tokens-reverse-arrow-button', - SWAP_TRADE_PRICE_ROW = 'swap-trade-price-row', TOKEN_SELECTOR_ROW = 'token-selector-row', WALLET_TYPE_OPTION = 'wallet-type-option', // alphabetize additional element names. diff --git a/src/components/AmplitudeAnalytics/utils.ts b/src/components/AmplitudeAnalytics/utils.ts index 7d2c9cf3f3f..4126d6b5d96 100644 --- a/src/components/AmplitudeAnalytics/utils.ts +++ b/src/components/AmplitudeAnalytics/utils.ts @@ -1,13 +1,21 @@ import { Currency, CurrencyAmount, Percent, Token } from '@uniswap/sdk-core' -export const getDurationTillTimestampSinceEpoch = (futureTimestampSinceEpoch?: number): number | undefined => { - if (!futureTimestampSinceEpoch) return undefined - return futureTimestampSinceEpoch - new Date().getTime() / 1000 +import { NATIVE_CHAIN_ID } from './constants' + +export const getDurationUntilTimestampSeconds = (futureTimestampInSecondsSinceEpoch?: number): number | undefined => { + if (!futureTimestampInSecondsSinceEpoch) return undefined + return futureTimestampInSecondsSinceEpoch - new Date().getTime() / 1000 +} + +export const getDurationFromDateMilliseconds = (start: Date): number => { + return new Date().getTime() - start.getTime() } -export const getNumberFormattedToDecimalPlace = ( +export const formatToDecimal = ( intialNumberObject: Percent | CurrencyAmount, decimalPlace: number ): number => parseFloat(intialNumberObject.toFixed(decimalPlace)) -export const formatPercentInBasisPointsNumber = (percent: Percent): number => parseFloat(percent.toFixed(6)) * 100 +export const getTokenAddress = (currency: Currency) => (currency.isNative ? NATIVE_CHAIN_ID : currency.address) + +export const formatPercentInBasisPointsNumber = (percent: Percent): number => parseFloat(percent.toFixed(2)) * 100 diff --git a/src/components/SearchModal/CommonBases.tsx b/src/components/SearchModal/CommonBases.tsx index d27485bdec0..ad410bf1995 100644 --- a/src/components/SearchModal/CommonBases.tsx +++ b/src/components/SearchModal/CommonBases.tsx @@ -1,6 +1,7 @@ -import { Currency, Token } from '@uniswap/sdk-core' +import { Currency } from '@uniswap/sdk-core' import { ElementName, Event, EventName } from 'components/AmplitudeAnalytics/constants' import { TraceEvent } from 'components/AmplitudeAnalytics/TraceEvent' +import { getTokenAddress } from 'components/AmplitudeAnalytics/utils' import { AutoColumn } from 'components/Column' import CurrencyLogo from 'components/CurrencyLogo' import { AutoRow } from 'components/Row' @@ -33,15 +34,10 @@ const BaseWrapper = styled.div<{ disable?: boolean }>` filter: ${({ disable }) => disable && 'grayscale(1)'}; ` -const formatAnalyticsEventProperties = ( - currency: Currency, - tokenAddress: string | undefined, - searchQuery: string, - isAddressSearch: string | false -) => ({ +const formatAnalyticsEventProperties = (currency: Currency, searchQuery: string, isAddressSearch: string | false) => ({ token_symbol: currency?.symbol, token_chain_id: currency?.chainId, - ...(tokenAddress ? { token_address: tokenAddress } : {}), + token_address: getTokenAddress(currency), is_suggested_token: true, is_selected_from_list: false, is_imported_by_user: false, @@ -70,13 +66,12 @@ export default function CommonBases({ {bases.map((currency: Currency) => { const isSelected = selectedCurrency?.equals(currency) - const tokenAddress = currency instanceof Token ? currency?.address : undefined return ( diff --git a/src/components/SearchModal/CurrencySearch.tsx b/src/components/SearchModal/CurrencySearch.tsx index 216acc884da..515f44345f9 100644 --- a/src/components/SearchModal/CurrencySearch.tsx +++ b/src/components/SearchModal/CurrencySearch.tsx @@ -191,8 +191,8 @@ export function CurrencySearch({ }, []) return ( - - + + @@ -270,7 +270,7 @@ export function CurrencySearch({ - - + + ) } diff --git a/src/components/swap/ConfirmSwapModal.tsx b/src/components/swap/ConfirmSwapModal.tsx index 1b76963a6c8..c6dcce1e5bb 100644 --- a/src/components/swap/ConfirmSwapModal.tsx +++ b/src/components/swap/ConfirmSwapModal.tsx @@ -26,6 +26,7 @@ export default function ConfirmSwapModal({ isOpen, attemptingTxn, txHash, + swapQuoteReceivedDate, }: { isOpen: boolean trade: InterfaceTrade | undefined @@ -38,6 +39,7 @@ export default function ConfirmSwapModal({ onConfirm: () => void swapErrorMessage: ReactNode | undefined onDismiss: () => void + swapQuoteReceivedDate: Date | undefined }) { // shouldLogModalCloseEvent lets the child SwapModalHeader component know when modal has been closed // and an event triggered by modal closing should be logged. @@ -75,9 +77,10 @@ export default function ConfirmSwapModal({ allowedSlippage={allowedSlippage} disabledConfirm={showAcceptChanges} swapErrorMessage={swapErrorMessage} + swapQuoteReceivedDate={swapQuoteReceivedDate} /> ) : null - }, [onConfirm, showAcceptChanges, swapErrorMessage, trade, allowedSlippage, txHash]) + }, [onConfirm, showAcceptChanges, swapErrorMessage, trade, allowedSlippage, txHash, swapQuoteReceivedDate]) // text to show while loading const pendingText = ( diff --git a/src/components/swap/SwapDetailsDropdown.tsx b/src/components/swap/SwapDetailsDropdown.tsx index b854f1d99a2..2ce1a823e8b 100644 --- a/src/components/swap/SwapDetailsDropdown.tsx +++ b/src/components/swap/SwapDetailsDropdown.tsx @@ -2,9 +2,7 @@ import { Trans } from '@lingui/macro' import { Currency, Percent, TradeType } from '@uniswap/sdk-core' import { useWeb3React } from '@web3-react/core' import { ElementName, Event, EventName } from 'components/AmplitudeAnalytics/constants' -import { Trace } from 'components/AmplitudeAnalytics/Trace' import { TraceEvent } from 'components/AmplitudeAnalytics/TraceEvent' -import { formatPercentInBasisPointsNumber, getNumberFormattedToDecimalPlace } from 'components/AmplitudeAnalytics/utils' import AnimatedDropdown from 'components/AnimatedDropdown' import Card, { OutlineCard } from 'components/Card' import { AutoColumn } from 'components/Column' @@ -13,15 +11,13 @@ import Row, { RowBetween, RowFixed } from 'components/Row' import { MouseoverTooltipContent } from 'components/Tooltip' import { SUPPORTED_GAS_ESTIMATE_CHAIN_IDS } from 'constants/chains' import { darken } from 'polished' -import { useEffect, useState } from 'react' +import { useState } from 'react' import { ChevronDown, Info } from 'react-feather' import { InterfaceTrade } from 'state/routing/types' import styled, { keyframes, useTheme } from 'styled-components/macro' import { HideSmall, ThemedText } from 'theme' -import { computeRealizedLPFeePercent } from 'utils/prices' import { AdvancedSwapDetails } from './AdvancedSwapDetails' -import { getPriceImpactPercent } from './AdvancedSwapDetails' import GasEstimateBadge from './GasEstimateBadge' import { ResponsiveTooltipContainer } from './styleds' import SwapRoute from './SwapRoute' @@ -124,29 +120,6 @@ interface SwapDetailsInlineProps { allowedSlippage: Percent } -const formatAnalyticsEventProperties = (trade: InterfaceTrade) => { - const lpFeePercent = trade ? computeRealizedLPFeePercent(trade) : undefined - return { - token_in_symbol: trade.inputAmount.currency.symbol, - token_out_symbol: trade.outputAmount.currency.symbol, - token_in_address: trade.inputAmount.currency.isToken ? trade.inputAmount.currency.address : undefined, - token_out_address: trade.outputAmount.currency.isToken ? trade.outputAmount.currency.address : undefined, - price_impact_basis_points: lpFeePercent - ? formatPercentInBasisPointsNumber(getPriceImpactPercent(lpFeePercent, trade)) - : undefined, - estimated_network_fee_usd: trade.gasUseEstimateUSD - ? getNumberFormattedToDecimalPlace(trade.gasUseEstimateUSD, 2) - : undefined, - chain_id: - trade.inputAmount.currency.chainId === trade.outputAmount.currency.chainId - ? trade.inputAmount.currency.chainId - : undefined, - token_in_amount: getNumberFormattedToDecimalPlace(trade.inputAmount, trade.inputAmount.currency.decimals), - token_out_amount: getNumberFormattedToDecimalPlace(trade.outputAmount, trade.outputAmount.currency.decimals), - // TODO(lynnshaoyu): Implement quote_latency_milliseconds. - } -} - export default function SwapDetailsDropdown({ trade, syncing, @@ -158,11 +131,6 @@ export default function SwapDetailsDropdown({ const theme = useTheme() const { chainId } = useWeb3React() const [showDetails, setShowDetails] = useState(false) - const [isFirstPriceFetch, setIsFirstPriceFetch] = useState(true) - - useEffect(() => { - if (isFirstPriceFetch && syncing) setIsFirstPriceFetch(false) - }, [isFirstPriceFetch, syncing]) return ( @@ -206,18 +174,11 @@ export default function SwapDetailsDropdown({ )} {trade ? ( - - - + ) : loading || syncing ? ( diff --git a/src/components/swap/SwapModalFooter.tsx b/src/components/swap/SwapModalFooter.tsx index 50cd79f2b20..2c3072e4f0b 100644 --- a/src/components/swap/SwapModalFooter.tsx +++ b/src/components/swap/SwapModalFooter.tsx @@ -5,8 +5,10 @@ import { Event } from 'components/AmplitudeAnalytics/constants' import { TraceEvent } from 'components/AmplitudeAnalytics/TraceEvent' import { formatPercentInBasisPointsNumber, - getDurationTillTimestampSinceEpoch, - getNumberFormattedToDecimalPlace, + formatToDecimal, + getDurationFromDateMilliseconds, + getDurationUntilTimestampSeconds, + getTokenAddress, } from 'components/AmplitudeAnalytics/utils' import { useStablecoinValue } from 'hooks/useStablecoinPrice' import useTransactionDeadline from 'hooks/useTransactionDeadline' @@ -31,6 +33,7 @@ interface AnalyticsEventProps { tokenInAmountUsd: string | undefined tokenOutAmountUsd: string | undefined lpFeePercent: Percent + swapQuoteReceivedDate: Date | undefined } const formatAnalyticsEventProperties = ({ @@ -43,20 +46,19 @@ const formatAnalyticsEventProperties = ({ tokenInAmountUsd, tokenOutAmountUsd, lpFeePercent, + swapQuoteReceivedDate, }: AnalyticsEventProps) => ({ - estimated_network_fee_usd: trade.gasUseEstimateUSD - ? getNumberFormattedToDecimalPlace(trade.gasUseEstimateUSD, 2) - : undefined, + estimated_network_fee_usd: trade.gasUseEstimateUSD ? formatToDecimal(trade.gasUseEstimateUSD, 2) : undefined, transaction_hash: txHash, - transaction_deadline_seconds: getDurationTillTimestampSinceEpoch(transactionDeadlineSecondsSinceEpoch), + transaction_deadline_seconds: getDurationUntilTimestampSeconds(transactionDeadlineSecondsSinceEpoch), token_in_amount_usd: tokenInAmountUsd ? parseFloat(tokenInAmountUsd) : undefined, token_out_amount_usd: tokenOutAmountUsd ? parseFloat(tokenOutAmountUsd) : undefined, - token_in_address: trade.inputAmount.currency.isToken ? trade.inputAmount.currency.address : undefined, - token_out_address: trade.outputAmount.currency.isToken ? trade.outputAmount.currency.address : undefined, + token_in_address: getTokenAddress(trade.inputAmount.currency), + token_out_address: getTokenAddress(trade.outputAmount.currency), token_in_symbol: trade.inputAmount.currency.symbol, token_out_symbol: trade.outputAmount.currency.symbol, - token_in_amount: getNumberFormattedToDecimalPlace(trade.inputAmount, trade.inputAmount.currency.decimals), - token_out_amount: getNumberFormattedToDecimalPlace(trade.outputAmount, trade.outputAmount.currency.decimals), + token_in_amount: formatToDecimal(trade.inputAmount, trade.inputAmount.currency.decimals), + token_out_amount: formatToDecimal(trade.outputAmount, trade.outputAmount.currency.decimals), price_impact_basis_points: formatPercentInBasisPointsNumber(getPriceImpactPercent(lpFeePercent, trade)), allowed_slippage_basis_points: formatPercentInBasisPointsNumber(allowedSlippage), is_auto_router_api: isAutoRouterApi, @@ -65,7 +67,9 @@ const formatAnalyticsEventProperties = ({ trade.inputAmount.currency.chainId === trade.outputAmount.currency.chainId ? trade.inputAmount.currency.chainId : undefined, - // TODO(lynnshaoyu): implement duration_from_first_quote_to_swap_submission_seconds + duration_from_first_quote_to_swap_submission_milliseconds: swapQuoteReceivedDate + ? getDurationFromDateMilliseconds(swapQuoteReceivedDate) + : undefined, }) export default function SwapModalFooter({ @@ -75,6 +79,7 @@ export default function SwapModalFooter({ onConfirm, swapErrorMessage, disabledConfirm, + swapQuoteReceivedDate, }: { trade: InterfaceTrade txHash: string | undefined @@ -82,6 +87,7 @@ export default function SwapModalFooter({ onConfirm: () => void swapErrorMessage: ReactNode | undefined disabledConfirm: boolean + swapQuoteReceivedDate: Date | undefined }) { const transactionDeadlineSecondsSinceEpoch = useTransactionDeadline()?.toNumber() // in seconds since epoch const isAutoSlippage = useUserSlippageTolerance() === 'auto' @@ -107,6 +113,7 @@ export default function SwapModalFooter({ tokenInAmountUsd, tokenOutAmountUsd, lpFeePercent, + swapQuoteReceivedDate, })} > , + fetchingSwapQuoteStartTime: Date | undefined +) => { + const lpFeePercent = trade ? computeRealizedLPFeePercent(trade) : undefined + return { + token_in_symbol: trade.inputAmount.currency.symbol, + token_out_symbol: trade.outputAmount.currency.symbol, + token_in_address: getTokenAddress(trade.inputAmount.currency), + token_out_address: getTokenAddress(trade.outputAmount.currency), + price_impact_basis_points: lpFeePercent + ? formatPercentInBasisPointsNumber(getPriceImpactPercent(lpFeePercent, trade)) + : undefined, + estimated_network_fee_usd: trade.gasUseEstimateUSD ? formatToDecimal(trade.gasUseEstimateUSD, 2) : undefined, + chain_id: + trade.inputAmount.currency.chainId === trade.outputAmount.currency.chainId + ? trade.inputAmount.currency.chainId + : undefined, + token_in_amount: formatToDecimal(trade.inputAmount, trade.inputAmount.currency.decimals), + token_out_amount: formatToDecimal(trade.outputAmount, trade.outputAmount.currency.decimals), + quote_latency_milliseconds: fetchingSwapQuoteStartTime + ? getDurationFromDateMilliseconds(fetchingSwapQuoteStartTime) + : undefined, + } +} + export default function Swap() { const navigate = useNavigate() const { account, chainId } = useWeb3React() const loadedUrlParams = useDefaultsFromURLSearch() + const [newSwapQuoteNeedsLogging, setNewSwapQuoteNeedsLogging] = useState(true) + const [fetchingSwapQuoteStartTime, setFetchingSwapQuoteStartTime] = useState() // token warning stuff const [loadedInputCurrency, loadedOutputCurrency] = [ @@ -350,6 +386,7 @@ export default function Swap() { // errors const [showInverted, setShowInverted] = useState(false) + const [swapQuoteReceivedDate, setSwapQuoteReceivedDate] = useState() // warnings on the greater of fiat value price impact and execution price impact const priceImpactSeverity = useMemo(() => { @@ -412,6 +449,38 @@ export default function Swap() { const priceImpactTooHigh = priceImpactSeverity > 3 && !isExpertMode + // Handle time based logging events and event properties. + useEffect(() => { + const now = new Date() + // If a trade exists, and we need to log the receipt of this new swap quote: + if (newSwapQuoteNeedsLogging && !!trade) { + // Set the current datetime as the time of receipt of latest swap quote. + setSwapQuoteReceivedDate(now) + // Log swap quote. + sendAnalyticsEvent( + EventName.SWAP_QUOTE_RECEIVED, + formatAnalyticsEventProperties(trade, fetchingSwapQuoteStartTime) + ) + // Latest swap quote has just been logged, so we don't need to log the current trade anymore + // unless user inputs change again and a new trade is in the process of being generated. + setNewSwapQuoteNeedsLogging(false) + // New quote is not being fetched, so set start time of quote fetch to undefined. + setFetchingSwapQuoteStartTime(undefined) + } + // If another swap quote is being loaded based on changed user inputs: + if (routeIsLoading) { + setNewSwapQuoteNeedsLogging(true) + if (!fetchingSwapQuoteStartTime) setFetchingSwapQuoteStartTime(now) + } + }, [ + newSwapQuoteNeedsLogging, + routeIsSyncing, + routeIsLoading, + fetchingSwapQuoteStartTime, + trade, + setSwapQuoteReceivedDate, + ]) + return ( <> @@ -436,6 +505,7 @@ export default function Swap() { onConfirm={handleSwap} swapErrorMessage={swapErrorMessage} onDismiss={handleConfirmDismiss} + swapQuoteReceivedDate={swapQuoteReceivedDate} />