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 { UNI, USDC_MAINNET } from '../../../src/constants/tokens'
......@@ -64,6 +65,13 @@ describe('Swap', () => {
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', '')
// 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
cy.get('#swap-button').click()
cy.contains('Review swap')
......
......@@ -267,7 +267,6 @@ export default function ConfirmSwapModal({
onCurrencySelection,
swapError,
swapResult,
swapQuoteReceivedDate,
fiatValueInput,
fiatValueOutput,
}: {
......@@ -282,7 +281,6 @@ export default function ConfirmSwapModal({
swapError?: Error
onDismiss: () => void
onCurrencySelection: (field: Field, currency: Currency) => void
swapQuoteReceivedDate?: Date
fiatValueInput: { data?: number; isLoading: boolean }
fiatValueOutput: { data?: number; isLoading: boolean }
}) {
......@@ -356,7 +354,6 @@ export default function ConfirmSwapModal({
swapResult={swapResult}
allowedSlippage={allowedSlippage}
disabledConfirm={showAcceptChanges}
swapQuoteReceivedDate={swapQuoteReceivedDate}
fiatValueInput={fiatValueInput}
fiatValueOutput={fiatValueOutput}
showAcceptChanges={showAcceptChanges}
......@@ -386,7 +383,6 @@ export default function ConfirmSwapModal({
wrapTxHash,
allowance,
allowedSlippage,
swapQuoteReceivedDate,
fiatValueInput,
fiatValueOutput,
onAcceptChanges,
......
......@@ -13,7 +13,6 @@ describe('SwapModalFooter.tsx', () => {
onConfirm={jest.fn()}
swapErrorMessage={undefined}
disabledConfirm={false}
swapQuoteReceivedDate={undefined}
fiatValueInput={{
data: undefined,
isLoading: false,
......@@ -49,7 +48,6 @@ describe('SwapModalFooter.tsx', () => {
onConfirm={jest.fn()}
swapErrorMessage={undefined}
disabledConfirm={false}
swapQuoteReceivedDate={undefined}
fiatValueInput={{
data: undefined,
isLoading: false,
......@@ -77,7 +75,6 @@ describe('SwapModalFooter.tsx', () => {
onConfirm={jest.fn()}
swapErrorMessage={undefined}
disabledConfirm={false}
swapQuoteReceivedDate={undefined}
fiatValueInput={{
data: undefined,
isLoading: false,
......
......@@ -53,7 +53,6 @@ export default function SwapModalFooter({
onConfirm,
swapErrorMessage,
disabledConfirm,
swapQuoteReceivedDate,
fiatValueInput,
fiatValueOutput,
showAcceptChanges,
......@@ -65,7 +64,6 @@ export default function SwapModalFooter({
onConfirm: () => void
swapErrorMessage?: ReactNode
disabledConfirm: boolean
swapQuoteReceivedDate?: Date
fiatValueInput: { data?: number; isLoading: boolean }
fiatValueOutput: { data?: number; isLoading: boolean }
showAcceptChanges: boolean
......@@ -187,7 +185,6 @@ export default function SwapModalFooter({
transactionDeadlineSecondsSinceEpoch,
isAutoSlippage,
isAutoRouterApi: routerPreference === RouterPreference.API,
swapQuoteReceivedDate,
routes,
fiatValueInput: fiatValueInput.data,
fiatValueOutput: fiatValueOutput.data,
......
......@@ -26,6 +26,7 @@ export function useDebouncedTrade(
): {
state: TradeState
trade?: InterfaceTrade
swapQuoteLatency?: number
}
export function useDebouncedTrade(
......@@ -37,6 +38,7 @@ export function useDebouncedTrade(
): {
state: TradeState
trade?: ClassicTrade
swapQuoteLatency?: number
}
/**
* Returns the debounced v2+v3 trade for a desired swap.
......@@ -57,6 +59,7 @@ export function useDebouncedTrade(
state: TradeState
trade?: InterfaceTrade
method?: QuoteMethod
swapQuoteLatency?: number
} {
const { chainId } = useWeb3React()
const autoRouterSupported = useAutoRouterSupported()
......
......@@ -9,11 +9,6 @@ export const getDurationUntilTimestampSeconds = (futureTimestampInSecondsSinceEp
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 = (
intialNumberObject: Percent | CurrencyAmount<Token | Currency>,
decimalPlace: number
......@@ -90,14 +85,14 @@ function getQuoteMethod(trade: InterfaceTrade) {
export const formatSwapQuoteReceivedEventProperties = (
trade: InterfaceTrade,
allowedSlippage: Percent,
swapQuoteReceivedDate: Date
swapQuoteLatencyMs: number | undefined
) => {
return {
...formatCommonPropertiesForTrade(trade, allowedSlippage),
swap_quote_block_number: isClassicTrade(trade) ? trade.blockNumber : undefined,
swap_quote_received_timestamp: swapQuoteReceivedDate.getTime(),
allowed_slippage_basis_points: formatPercentInBasisPointsNumber(allowedSlippage),
token_in_amount_max: trade.maximumAmountIn(allowedSlippage).toExact(),
token_out_amount_min: trade.minimumAmountOut(allowedSlippage).toExact(),
quote_latency_milliseconds: swapQuoteLatencyMs,
}
}
......@@ -268,7 +268,7 @@ export function Swap({
const swapInfo = useDerivedSwapInfo(state, chainId)
const {
trade: { state: tradeState, trade },
trade: { state: tradeState, trade, swapQuoteLatency },
allowedSlippage,
autoSlippage,
currencyBalances,
......@@ -466,9 +466,6 @@ export function Swap({
}
}, [currencies, onUserInput, onWrap, wrapType])
// errors
const [swapQuoteReceivedDate, setSwapQuoteReceivedDate] = useState<Date | undefined>()
// warnings on the greater of fiat value price impact and execution price impact
const { priceImpactSeverity, largerPriceImpact } = useMemo(() => {
if (isUniswapXTrade(trade)) {
......@@ -528,13 +525,11 @@ export function Swap({
useEffect(() => {
if (!trade || prevTrade === trade) return // no new swap quote to log
const now = new Date()
setSwapQuoteReceivedDate(now)
sendAnalyticsEvent(SwapEventName.SWAP_QUOTE_RECEIVED, {
...formatSwapQuoteReceivedEventProperties(trade, allowedSlippage, now),
...formatSwapQuoteReceivedEventProperties(trade, allowedSlippage, swapQuoteLatency),
...trace,
})
}, [prevTrade, trade, trace, allowedSlippage])
}, [prevTrade, trade, trace, allowedSlippage, swapQuoteLatency])
const showDetailsDropdown = Boolean(
!showWrap && userHasSpecifiedInputOutput && (trade || routeIsLoading || routeIsSyncing)
......@@ -569,7 +564,6 @@ export function Swap({
allowance={allowance}
swapError={swapError}
onDismiss={handleConfirmDismiss}
swapQuoteReceivedDate={swapQuoteReceivedDate}
fiatValueInput={fiatValueTradeInput}
fiatValueOutput={fiatValueTradeOutput}
/>
......
......@@ -36,6 +36,11 @@ const DEFAULT_QUERY_PARAMS = {
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 {
const {
account,
......@@ -114,6 +119,7 @@ export const routingApi = createApi({
},
async queryFn(args, _api, _extraOptions, fetch) {
let fellBack = false
const quoteStartMark = performance.mark(`quote-fetch-start-${Date.now()}`)
if (shouldUseAPIRouter(args)) {
fellBack = true
try {
......@@ -155,7 +161,9 @@ export const routingApi = createApi({
typeof errorData === 'object' &&
(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 {
throw response.error
......@@ -164,8 +172,7 @@ export const routingApi = createApi({
const uraQuoteResponse = response.data as URAQuoteResponse
const tradeResult = await transformRoutesToTrade(args, uraQuoteResponse, QuoteMethod.ROUTING_API)
return { data: tradeResult }
return { data: { ...tradeResult, latencyMs: getQuoteLatencyMeasure(quoteStartMark).duration } }
} catch (error: any) {
console.warn(
`GetQuote failed on Unified Routing API, falling back to client: ${
......@@ -179,15 +186,18 @@ export const routingApi = createApi({
const router = getRouter(args.tokenInChainId)
const quoteResult = await getClientSideQuote(args, router, CLIENT_PARAMS)
if (quoteResult.state === QuoteState.SUCCESS) {
const trade = await transformRoutesToTrade(args, quoteResult.data, method)
return {
data: await transformRoutesToTrade(args, quoteResult.data, method),
data: { ...trade, latencyMs: getQuoteLatencyMeasure(quoteStartMark).duration },
}
} else {
return { data: quoteResult }
return { data: { ...quoteResult, latencyMs: getQuoteLatencyMeasure(quoteStartMark).duration } }
}
} catch (error: any) {
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`),
......
......@@ -282,10 +282,12 @@ export type TradeResult =
| {
state: QuoteState.NOT_FOUND
trade?: undefined
latencyMs?: number
}
| {
state: QuoteState.SUCCESS
trade: InterfaceTrade
latencyMs?: number
}
export enum PoolType {
......
......@@ -31,6 +31,7 @@ export function useRoutingAPITrade<TTradeType extends TradeType>(
): {
state: TradeState
trade?: ClassicTrade
swapQuoteLatency?: number
}
export function useRoutingAPITrade<TTradeType extends TradeType>(
......@@ -43,6 +44,7 @@ export function useRoutingAPITrade<TTradeType extends TradeType>(
): {
state: TradeState
trade?: InterfaceTrade
swapQuoteLatency?: number
}
/**
......@@ -62,6 +64,7 @@ export function useRoutingAPITrade<TTradeType extends TradeType>(
state: TradeState
trade?: InterfaceTrade
method?: QuoteMethod
swapQuoteLatency?: number
} {
const [currencyIn, currencyOut]: [Currency | undefined, Currency | undefined] = useMemo(
() =>
......@@ -96,7 +99,7 @@ export function useRoutingAPITrade<TTradeType extends TradeType>(
return useMemo(() => {
if (skipFetch && amountSpecified) {
// 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) {
return {
state: TradeState.INVALID,
......@@ -112,9 +115,20 @@ export function useRoutingAPITrade<TTradeType extends TradeType>(
return {
state: isCurrent ? TradeState.VALID : TradeState.LOADING,
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
......
......@@ -81,6 +81,7 @@ export type SwapInfo = {
state: TradeState
uniswapXGasUseEstimateUSD?: number
error?: any
swapQuoteLatency?: number
}
allowedSlippage: Percent
autoSlippage: Percent
......
......@@ -5,7 +5,6 @@ import {
formatPercentInBasisPointsNumber,
formatPercentNumber,
formatToDecimal,
getDurationFromDateMilliseconds,
getDurationUntilTimestampSeconds,
getTokenAddress,
} from 'lib/utils/analytics'
......@@ -66,7 +65,6 @@ interface AnalyticsEventProps {
transactionDeadlineSecondsSinceEpoch?: number
isAutoSlippage: boolean
isAutoRouterApi: boolean
swapQuoteReceivedDate?: Date
routes?: RoutingDiagramEntry[]
fiatValueInput?: number
fiatValueOutput?: number
......@@ -79,7 +77,6 @@ export const formatSwapButtonClickEventProperties = ({
transactionDeadlineSecondsSinceEpoch,
isAutoSlippage,
isAutoRouterApi,
swapQuoteReceivedDate,
routes,
fiatValueInput,
fiatValueOutput,
......@@ -106,9 +103,6 @@ export const formatSwapButtonClickEventProperties = ({
trade.inputAmount.currency.chainId === trade.outputAmount.currency.chainId
? trade.inputAmount.currency.chainId
: undefined,
duration_from_first_quote_to_swap_submission_milliseconds: swapQuoteReceivedDate
? getDurationFromDateMilliseconds(swapQuoteReceivedDate)
: undefined,
swap_quote_block_number: isClassicTrade(trade) ? trade.blockNumber : undefined,
...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