Commit 57b098f3 authored by eddie's avatar eddie Committed by GitHub

feat: swap quote latency logging (#7143)

* feat: swap quote latency

* feat: measure quote latency

* feat: swap quote latency

* fix: improve variable name
parent c802132b
import { SwapEventName } from '@uniswap/analytics-events'
import { ChainId } from '@uniswap/sdk-core' import { ChainId } from '@uniswap/sdk-core'
import { UNI, USDC_MAINNET } from '../../../src/constants/tokens' import { UNI, USDC_MAINNET } from '../../../src/constants/tokens'
...@@ -64,6 +65,13 @@ describe('Swap', () => { ...@@ -64,6 +65,13 @@ describe('Swap', () => {
cy.get('#swap-currency-output .token-amount-input').type('1').should('have.value', '1') cy.get('#swap-currency-output .token-amount-input').type('1').should('have.value', '1')
cy.get('#swap-currency-input .token-amount-input').should('not.have.value', '') cy.get('#swap-currency-input .token-amount-input').should('not.have.value', '')
// Verify logging
cy.waitForAmplitudeEvent(SwapEventName.SWAP_QUOTE_RECEIVED).then((event: any) => {
cy.wrap(event.event_properties).should('have.property', 'quote_latency_milliseconds')
cy.wrap(event.event_properties.quote_latency_milliseconds).should('be.a', 'number')
cy.wrap(event.event_properties.quote_latency_milliseconds).should('be.gte', 0)
})
// Submit transaction // Submit transaction
cy.get('#swap-button').click() cy.get('#swap-button').click()
cy.contains('Review swap') cy.contains('Review swap')
......
...@@ -267,7 +267,6 @@ export default function ConfirmSwapModal({ ...@@ -267,7 +267,6 @@ export default function ConfirmSwapModal({
onCurrencySelection, onCurrencySelection,
swapError, swapError,
swapResult, swapResult,
swapQuoteReceivedDate,
fiatValueInput, fiatValueInput,
fiatValueOutput, fiatValueOutput,
}: { }: {
...@@ -282,7 +281,6 @@ export default function ConfirmSwapModal({ ...@@ -282,7 +281,6 @@ export default function ConfirmSwapModal({
swapError?: Error swapError?: Error
onDismiss: () => void onDismiss: () => void
onCurrencySelection: (field: Field, currency: Currency) => void onCurrencySelection: (field: Field, currency: Currency) => void
swapQuoteReceivedDate?: Date
fiatValueInput: { data?: number; isLoading: boolean } fiatValueInput: { data?: number; isLoading: boolean }
fiatValueOutput: { data?: number; isLoading: boolean } fiatValueOutput: { data?: number; isLoading: boolean }
}) { }) {
...@@ -356,7 +354,6 @@ export default function ConfirmSwapModal({ ...@@ -356,7 +354,6 @@ export default function ConfirmSwapModal({
swapResult={swapResult} swapResult={swapResult}
allowedSlippage={allowedSlippage} allowedSlippage={allowedSlippage}
disabledConfirm={showAcceptChanges} disabledConfirm={showAcceptChanges}
swapQuoteReceivedDate={swapQuoteReceivedDate}
fiatValueInput={fiatValueInput} fiatValueInput={fiatValueInput}
fiatValueOutput={fiatValueOutput} fiatValueOutput={fiatValueOutput}
showAcceptChanges={showAcceptChanges} showAcceptChanges={showAcceptChanges}
...@@ -386,7 +383,6 @@ export default function ConfirmSwapModal({ ...@@ -386,7 +383,6 @@ export default function ConfirmSwapModal({
wrapTxHash, wrapTxHash,
allowance, allowance,
allowedSlippage, allowedSlippage,
swapQuoteReceivedDate,
fiatValueInput, fiatValueInput,
fiatValueOutput, fiatValueOutput,
onAcceptChanges, onAcceptChanges,
......
...@@ -13,7 +13,6 @@ describe('SwapModalFooter.tsx', () => { ...@@ -13,7 +13,6 @@ describe('SwapModalFooter.tsx', () => {
onConfirm={jest.fn()} onConfirm={jest.fn()}
swapErrorMessage={undefined} swapErrorMessage={undefined}
disabledConfirm={false} disabledConfirm={false}
swapQuoteReceivedDate={undefined}
fiatValueInput={{ fiatValueInput={{
data: undefined, data: undefined,
isLoading: false, isLoading: false,
...@@ -49,7 +48,6 @@ describe('SwapModalFooter.tsx', () => { ...@@ -49,7 +48,6 @@ describe('SwapModalFooter.tsx', () => {
onConfirm={jest.fn()} onConfirm={jest.fn()}
swapErrorMessage={undefined} swapErrorMessage={undefined}
disabledConfirm={false} disabledConfirm={false}
swapQuoteReceivedDate={undefined}
fiatValueInput={{ fiatValueInput={{
data: undefined, data: undefined,
isLoading: false, isLoading: false,
...@@ -77,7 +75,6 @@ describe('SwapModalFooter.tsx', () => { ...@@ -77,7 +75,6 @@ describe('SwapModalFooter.tsx', () => {
onConfirm={jest.fn()} onConfirm={jest.fn()}
swapErrorMessage={undefined} swapErrorMessage={undefined}
disabledConfirm={false} disabledConfirm={false}
swapQuoteReceivedDate={undefined}
fiatValueInput={{ fiatValueInput={{
data: undefined, data: undefined,
isLoading: false, isLoading: false,
......
...@@ -53,7 +53,6 @@ export default function SwapModalFooter({ ...@@ -53,7 +53,6 @@ export default function SwapModalFooter({
onConfirm, onConfirm,
swapErrorMessage, swapErrorMessage,
disabledConfirm, disabledConfirm,
swapQuoteReceivedDate,
fiatValueInput, fiatValueInput,
fiatValueOutput, fiatValueOutput,
showAcceptChanges, showAcceptChanges,
...@@ -65,7 +64,6 @@ export default function SwapModalFooter({ ...@@ -65,7 +64,6 @@ export default function SwapModalFooter({
onConfirm: () => void onConfirm: () => void
swapErrorMessage?: ReactNode swapErrorMessage?: ReactNode
disabledConfirm: boolean disabledConfirm: boolean
swapQuoteReceivedDate?: Date
fiatValueInput: { data?: number; isLoading: boolean } fiatValueInput: { data?: number; isLoading: boolean }
fiatValueOutput: { data?: number; isLoading: boolean } fiatValueOutput: { data?: number; isLoading: boolean }
showAcceptChanges: boolean showAcceptChanges: boolean
...@@ -187,7 +185,6 @@ export default function SwapModalFooter({ ...@@ -187,7 +185,6 @@ export default function SwapModalFooter({
transactionDeadlineSecondsSinceEpoch, transactionDeadlineSecondsSinceEpoch,
isAutoSlippage, isAutoSlippage,
isAutoRouterApi: routerPreference === RouterPreference.API, isAutoRouterApi: routerPreference === RouterPreference.API,
swapQuoteReceivedDate,
routes, routes,
fiatValueInput: fiatValueInput.data, fiatValueInput: fiatValueInput.data,
fiatValueOutput: fiatValueOutput.data, fiatValueOutput: fiatValueOutput.data,
......
...@@ -26,6 +26,7 @@ export function useDebouncedTrade( ...@@ -26,6 +26,7 @@ export function useDebouncedTrade(
): { ): {
state: TradeState state: TradeState
trade?: InterfaceTrade trade?: InterfaceTrade
swapQuoteLatency?: number
} }
export function useDebouncedTrade( export function useDebouncedTrade(
...@@ -37,6 +38,7 @@ export function useDebouncedTrade( ...@@ -37,6 +38,7 @@ export function useDebouncedTrade(
): { ): {
state: TradeState state: TradeState
trade?: ClassicTrade trade?: ClassicTrade
swapQuoteLatency?: number
} }
/** /**
* Returns the debounced v2+v3 trade for a desired swap. * Returns the debounced v2+v3 trade for a desired swap.
...@@ -57,6 +59,7 @@ export function useDebouncedTrade( ...@@ -57,6 +59,7 @@ export function useDebouncedTrade(
state: TradeState state: TradeState
trade?: InterfaceTrade trade?: InterfaceTrade
method?: QuoteMethod method?: QuoteMethod
swapQuoteLatency?: number
} { } {
const { chainId } = useWeb3React() const { chainId } = useWeb3React()
const autoRouterSupported = useAutoRouterSupported() const autoRouterSupported = useAutoRouterSupported()
......
...@@ -9,11 +9,6 @@ export const getDurationUntilTimestampSeconds = (futureTimestampInSecondsSinceEp ...@@ -9,11 +9,6 @@ export const getDurationUntilTimestampSeconds = (futureTimestampInSecondsSinceEp
return futureTimestampInSecondsSinceEpoch - new Date().getTime() / 1000 return futureTimestampInSecondsSinceEpoch - new Date().getTime() / 1000
} }
export const getDurationFromDateMilliseconds = (start?: Date): number | undefined => {
if (!start) return undefined
return new Date().getTime() - start.getTime()
}
export const formatToDecimal = ( export const formatToDecimal = (
intialNumberObject: Percent | CurrencyAmount<Token | Currency>, intialNumberObject: Percent | CurrencyAmount<Token | Currency>,
decimalPlace: number decimalPlace: number
...@@ -90,14 +85,14 @@ function getQuoteMethod(trade: InterfaceTrade) { ...@@ -90,14 +85,14 @@ function getQuoteMethod(trade: InterfaceTrade) {
export const formatSwapQuoteReceivedEventProperties = ( export const formatSwapQuoteReceivedEventProperties = (
trade: InterfaceTrade, trade: InterfaceTrade,
allowedSlippage: Percent, allowedSlippage: Percent,
swapQuoteReceivedDate: Date swapQuoteLatencyMs: number | undefined
) => { ) => {
return { return {
...formatCommonPropertiesForTrade(trade, allowedSlippage), ...formatCommonPropertiesForTrade(trade, allowedSlippage),
swap_quote_block_number: isClassicTrade(trade) ? trade.blockNumber : undefined, swap_quote_block_number: isClassicTrade(trade) ? trade.blockNumber : undefined,
swap_quote_received_timestamp: swapQuoteReceivedDate.getTime(),
allowed_slippage_basis_points: formatPercentInBasisPointsNumber(allowedSlippage), allowed_slippage_basis_points: formatPercentInBasisPointsNumber(allowedSlippage),
token_in_amount_max: trade.maximumAmountIn(allowedSlippage).toExact(), token_in_amount_max: trade.maximumAmountIn(allowedSlippage).toExact(),
token_out_amount_min: trade.minimumAmountOut(allowedSlippage).toExact(), token_out_amount_min: trade.minimumAmountOut(allowedSlippage).toExact(),
quote_latency_milliseconds: swapQuoteLatencyMs,
} }
} }
...@@ -268,7 +268,7 @@ export function Swap({ ...@@ -268,7 +268,7 @@ export function Swap({
const swapInfo = useDerivedSwapInfo(state, chainId) const swapInfo = useDerivedSwapInfo(state, chainId)
const { const {
trade: { state: tradeState, trade }, trade: { state: tradeState, trade, swapQuoteLatency },
allowedSlippage, allowedSlippage,
autoSlippage, autoSlippage,
currencyBalances, currencyBalances,
...@@ -466,9 +466,6 @@ export function Swap({ ...@@ -466,9 +466,6 @@ export function Swap({
} }
}, [currencies, onUserInput, onWrap, wrapType]) }, [currencies, onUserInput, onWrap, wrapType])
// errors
const [swapQuoteReceivedDate, setSwapQuoteReceivedDate] = useState<Date | undefined>()
// warnings on the greater of fiat value price impact and execution price impact // warnings on the greater of fiat value price impact and execution price impact
const { priceImpactSeverity, largerPriceImpact } = useMemo(() => { const { priceImpactSeverity, largerPriceImpact } = useMemo(() => {
if (isUniswapXTrade(trade)) { if (isUniswapXTrade(trade)) {
...@@ -528,13 +525,11 @@ export function Swap({ ...@@ -528,13 +525,11 @@ export function Swap({
useEffect(() => { useEffect(() => {
if (!trade || prevTrade === trade) return // no new swap quote to log if (!trade || prevTrade === trade) return // no new swap quote to log
const now = new Date()
setSwapQuoteReceivedDate(now)
sendAnalyticsEvent(SwapEventName.SWAP_QUOTE_RECEIVED, { sendAnalyticsEvent(SwapEventName.SWAP_QUOTE_RECEIVED, {
...formatSwapQuoteReceivedEventProperties(trade, allowedSlippage, now), ...formatSwapQuoteReceivedEventProperties(trade, allowedSlippage, swapQuoteLatency),
...trace, ...trace,
}) })
}, [prevTrade, trade, trace, allowedSlippage]) }, [prevTrade, trade, trace, allowedSlippage, swapQuoteLatency])
const showDetailsDropdown = Boolean( const showDetailsDropdown = Boolean(
!showWrap && userHasSpecifiedInputOutput && (trade || routeIsLoading || routeIsSyncing) !showWrap && userHasSpecifiedInputOutput && (trade || routeIsLoading || routeIsSyncing)
...@@ -569,7 +564,6 @@ export function Swap({ ...@@ -569,7 +564,6 @@ export function Swap({
allowance={allowance} allowance={allowance}
swapError={swapError} swapError={swapError}
onDismiss={handleConfirmDismiss} onDismiss={handleConfirmDismiss}
swapQuoteReceivedDate={swapQuoteReceivedDate}
fiatValueInput={fiatValueTradeInput} fiatValueInput={fiatValueTradeInput}
fiatValueOutput={fiatValueTradeOutput} fiatValueOutput={fiatValueTradeOutput}
/> />
......
...@@ -36,6 +36,11 @@ const DEFAULT_QUERY_PARAMS = { ...@@ -36,6 +36,11 @@ const DEFAULT_QUERY_PARAMS = {
protocols, protocols,
} }
function getQuoteLatencyMeasure(mark: PerformanceMark): PerformanceMeasure {
performance.mark('quote-fetch-end')
return performance.measure('quote-fetch-latency', mark.name, 'quote-fetch-end')
}
function getRoutingAPIConfig(args: GetQuoteArgs): RoutingConfig { function getRoutingAPIConfig(args: GetQuoteArgs): RoutingConfig {
const { const {
account, account,
...@@ -114,6 +119,7 @@ export const routingApi = createApi({ ...@@ -114,6 +119,7 @@ export const routingApi = createApi({
}, },
async queryFn(args, _api, _extraOptions, fetch) { async queryFn(args, _api, _extraOptions, fetch) {
let fellBack = false let fellBack = false
const quoteStartMark = performance.mark(`quote-fetch-start-${Date.now()}`)
if (shouldUseAPIRouter(args)) { if (shouldUseAPIRouter(args)) {
fellBack = true fellBack = true
try { try {
...@@ -155,7 +161,9 @@ export const routingApi = createApi({ ...@@ -155,7 +161,9 @@ export const routingApi = createApi({
typeof errorData === 'object' && typeof errorData === 'object' &&
(errorData?.errorCode === 'NO_ROUTE' || errorData?.detail === 'No quotes available') (errorData?.errorCode === 'NO_ROUTE' || errorData?.detail === 'No quotes available')
) { ) {
return { data: { state: QuoteState.NOT_FOUND } } return {
data: { state: QuoteState.NOT_FOUND, latencyMs: getQuoteLatencyMeasure(quoteStartMark).duration },
}
} }
} catch { } catch {
throw response.error throw response.error
...@@ -164,8 +172,7 @@ export const routingApi = createApi({ ...@@ -164,8 +172,7 @@ export const routingApi = createApi({
const uraQuoteResponse = response.data as URAQuoteResponse const uraQuoteResponse = response.data as URAQuoteResponse
const tradeResult = await transformRoutesToTrade(args, uraQuoteResponse, QuoteMethod.ROUTING_API) const tradeResult = await transformRoutesToTrade(args, uraQuoteResponse, QuoteMethod.ROUTING_API)
return { data: { ...tradeResult, latencyMs: getQuoteLatencyMeasure(quoteStartMark).duration } }
return { data: tradeResult }
} catch (error: any) { } catch (error: any) {
console.warn( console.warn(
`GetQuote failed on Unified Routing API, falling back to client: ${ `GetQuote failed on Unified Routing API, falling back to client: ${
...@@ -179,15 +186,18 @@ export const routingApi = createApi({ ...@@ -179,15 +186,18 @@ export const routingApi = createApi({
const router = getRouter(args.tokenInChainId) const router = getRouter(args.tokenInChainId)
const quoteResult = await getClientSideQuote(args, router, CLIENT_PARAMS) const quoteResult = await getClientSideQuote(args, router, CLIENT_PARAMS)
if (quoteResult.state === QuoteState.SUCCESS) { if (quoteResult.state === QuoteState.SUCCESS) {
const trade = await transformRoutesToTrade(args, quoteResult.data, method)
return { return {
data: await transformRoutesToTrade(args, quoteResult.data, method), data: { ...trade, latencyMs: getQuoteLatencyMeasure(quoteStartMark).duration },
} }
} else { } else {
return { data: quoteResult } return { data: { ...quoteResult, latencyMs: getQuoteLatencyMeasure(quoteStartMark).duration } }
} }
} catch (error: any) { } catch (error: any) {
console.warn(`GetQuote failed on client: ${error}`) console.warn(`GetQuote failed on client: ${error}`)
return { error: { status: 'CUSTOM_ERROR', error: error?.detail ?? error?.message ?? error } } return {
error: { status: 'CUSTOM_ERROR', error: error?.detail ?? error?.message ?? error },
}
} }
}, },
keepUnusedDataFor: ms(`10s`), keepUnusedDataFor: ms(`10s`),
......
...@@ -282,10 +282,12 @@ export type TradeResult = ...@@ -282,10 +282,12 @@ export type TradeResult =
| { | {
state: QuoteState.NOT_FOUND state: QuoteState.NOT_FOUND
trade?: undefined trade?: undefined
latencyMs?: number
} }
| { | {
state: QuoteState.SUCCESS state: QuoteState.SUCCESS
trade: InterfaceTrade trade: InterfaceTrade
latencyMs?: number
} }
export enum PoolType { export enum PoolType {
......
...@@ -31,6 +31,7 @@ export function useRoutingAPITrade<TTradeType extends TradeType>( ...@@ -31,6 +31,7 @@ export function useRoutingAPITrade<TTradeType extends TradeType>(
): { ): {
state: TradeState state: TradeState
trade?: ClassicTrade trade?: ClassicTrade
swapQuoteLatency?: number
} }
export function useRoutingAPITrade<TTradeType extends TradeType>( export function useRoutingAPITrade<TTradeType extends TradeType>(
...@@ -43,6 +44,7 @@ export function useRoutingAPITrade<TTradeType extends TradeType>( ...@@ -43,6 +44,7 @@ export function useRoutingAPITrade<TTradeType extends TradeType>(
): { ): {
state: TradeState state: TradeState
trade?: InterfaceTrade trade?: InterfaceTrade
swapQuoteLatency?: number
} }
/** /**
...@@ -62,6 +64,7 @@ export function useRoutingAPITrade<TTradeType extends TradeType>( ...@@ -62,6 +64,7 @@ export function useRoutingAPITrade<TTradeType extends TradeType>(
state: TradeState state: TradeState
trade?: InterfaceTrade trade?: InterfaceTrade
method?: QuoteMethod method?: QuoteMethod
swapQuoteLatency?: number
} { } {
const [currencyIn, currencyOut]: [Currency | undefined, Currency | undefined] = useMemo( const [currencyIn, currencyOut]: [Currency | undefined, Currency | undefined] = useMemo(
() => () =>
...@@ -96,7 +99,7 @@ export function useRoutingAPITrade<TTradeType extends TradeType>( ...@@ -96,7 +99,7 @@ export function useRoutingAPITrade<TTradeType extends TradeType>(
return useMemo(() => { return useMemo(() => {
if (skipFetch && amountSpecified) { if (skipFetch && amountSpecified) {
// If we don't want to fetch new trades, but have valid inputs, return the stale trade. // If we don't want to fetch new trades, but have valid inputs, return the stale trade.
return { state: TradeState.STALE, trade: tradeResult?.trade } return { state: TradeState.STALE, trade: tradeResult?.trade, swapQuoteLatency: tradeResult?.latencyMs }
} else if (!amountSpecified || isError || !queryArgs) { } else if (!amountSpecified || isError || !queryArgs) {
return { return {
state: TradeState.INVALID, state: TradeState.INVALID,
...@@ -112,9 +115,20 @@ export function useRoutingAPITrade<TTradeType extends TradeType>( ...@@ -112,9 +115,20 @@ export function useRoutingAPITrade<TTradeType extends TradeType>(
return { return {
state: isCurrent ? TradeState.VALID : TradeState.LOADING, state: isCurrent ? TradeState.VALID : TradeState.LOADING,
trade: tradeResult.trade, trade: tradeResult.trade,
swapQuoteLatency: tradeResult.latencyMs,
} }
} }
}, [amountSpecified, error, isCurrent, isError, queryArgs, skipFetch, tradeResult?.state, tradeResult?.trade]) }, [
amountSpecified,
error,
isCurrent,
isError,
queryArgs,
skipFetch,
tradeResult?.state,
tradeResult?.latencyMs,
tradeResult?.trade,
])
} }
// only want to enable this when app hook called // only want to enable this when app hook called
......
...@@ -81,6 +81,7 @@ export type SwapInfo = { ...@@ -81,6 +81,7 @@ export type SwapInfo = {
state: TradeState state: TradeState
uniswapXGasUseEstimateUSD?: number uniswapXGasUseEstimateUSD?: number
error?: any error?: any
swapQuoteLatency?: number
} }
allowedSlippage: Percent allowedSlippage: Percent
autoSlippage: Percent autoSlippage: Percent
......
...@@ -5,7 +5,6 @@ import { ...@@ -5,7 +5,6 @@ import {
formatPercentInBasisPointsNumber, formatPercentInBasisPointsNumber,
formatPercentNumber, formatPercentNumber,
formatToDecimal, formatToDecimal,
getDurationFromDateMilliseconds,
getDurationUntilTimestampSeconds, getDurationUntilTimestampSeconds,
getTokenAddress, getTokenAddress,
} from 'lib/utils/analytics' } from 'lib/utils/analytics'
...@@ -66,7 +65,6 @@ interface AnalyticsEventProps { ...@@ -66,7 +65,6 @@ interface AnalyticsEventProps {
transactionDeadlineSecondsSinceEpoch?: number transactionDeadlineSecondsSinceEpoch?: number
isAutoSlippage: boolean isAutoSlippage: boolean
isAutoRouterApi: boolean isAutoRouterApi: boolean
swapQuoteReceivedDate?: Date
routes?: RoutingDiagramEntry[] routes?: RoutingDiagramEntry[]
fiatValueInput?: number fiatValueInput?: number
fiatValueOutput?: number fiatValueOutput?: number
...@@ -79,7 +77,6 @@ export const formatSwapButtonClickEventProperties = ({ ...@@ -79,7 +77,6 @@ export const formatSwapButtonClickEventProperties = ({
transactionDeadlineSecondsSinceEpoch, transactionDeadlineSecondsSinceEpoch,
isAutoSlippage, isAutoSlippage,
isAutoRouterApi, isAutoRouterApi,
swapQuoteReceivedDate,
routes, routes,
fiatValueInput, fiatValueInput,
fiatValueOutput, fiatValueOutput,
...@@ -106,9 +103,6 @@ export const formatSwapButtonClickEventProperties = ({ ...@@ -106,9 +103,6 @@ export const formatSwapButtonClickEventProperties = ({
trade.inputAmount.currency.chainId === trade.outputAmount.currency.chainId trade.inputAmount.currency.chainId === trade.outputAmount.currency.chainId
? trade.inputAmount.currency.chainId ? trade.inputAmount.currency.chainId
: undefined, : undefined,
duration_from_first_quote_to_swap_submission_milliseconds: swapQuoteReceivedDate
? getDurationFromDateMilliseconds(swapQuoteReceivedDate)
: undefined,
swap_quote_block_number: isClassicTrade(trade) ? trade.blockNumber : undefined, swap_quote_block_number: isClassicTrade(trade) ? trade.blockNumber : undefined,
...formatRoutesEventProperties(routes), ...formatRoutesEventProperties(routes),
}) })
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment