Skip to content

Commit

Permalink
feat(swap): Adding swap time metrics (#3997)
Browse files Browse the repository at this point in the history
  • Loading branch information
dievazqu authored Jul 27, 2023
1 parent 0c04f6b commit 99c01bf
Show file tree
Hide file tree
Showing 7 changed files with 85 additions and 16 deletions.
28 changes: 17 additions & 11 deletions src/analytics/Properties.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -1203,6 +1203,10 @@ type SwapQuoteEvent = SwapEvent & {
provider: string
}

export interface SwapTimeMetrics {
quoteToTransactionElapsedTimeInMs?: number // The elapsed time since the quote was received until the swap transaction is sent to the blockchain
quoteToUserConfirmsSwapElapsedTimeInMs: number // The elapsed time since the quote was received until the user confirmed to execute the swap
}
interface SwapEventsProperties {
[SwapEvents.swap_screen_open]: undefined
[SwapEvents.swap_screen_select_token]: {
Expand All @@ -1228,17 +1232,19 @@ interface SwapEventsProperties {
toToken: string
fromToken: string
}
[SwapEvents.swap_execute_success]: SwapQuoteEvent & {
fromTokenBalance: string
swapExecuteTxId: string
swapApproveTxId: string
}
[SwapEvents.swap_execute_error]: SwapQuoteEvent & {
error: string
fromTokenBalance: string
swapExecuteTxId: string
swapApproveTxId: string
}
[SwapEvents.swap_execute_success]: SwapQuoteEvent &
SwapTimeMetrics & {
fromTokenBalance: string
swapExecuteTxId: string
swapApproveTxId: string
}
[SwapEvents.swap_execute_error]: SwapQuoteEvent &
SwapTimeMetrics & {
error: string
fromTokenBalance: string
swapExecuteTxId: string
swapApproveTxId: string
}
[SwapEvents.swap_learn_more]: undefined
[SwapEvents.swap_price_impact_warning_displayed]: SwapEvent & {
provider: string
Expand Down
14 changes: 13 additions & 1 deletion src/swap/SwapReviewScreen.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -151,6 +151,10 @@ describe('SwapReviewScreen', () => {
mockFetch.resetMocks()
})

afterEach(() => {
jest.spyOn(Date, 'now').mockRestore()
})

it('should display correct info on fetch', async () => {
mockFeeCurrency.mockImplementation(() => mockCeloAddress)

Expand Down Expand Up @@ -356,6 +360,9 @@ describe('SwapReviewScreen', () => {
})

it('should correctly dispatch swapStart', async () => {
const quoteReceivedTimestamp = 1000
jest.spyOn(Date, 'now').mockReturnValueOnce(quoteReceivedTimestamp) // quote received timestamp

mockStore.dispatch = jest.fn()
mockFetch.mockResponse(JSON.stringify(mock0xResponse))

Expand All @@ -368,7 +375,12 @@ describe('SwapReviewScreen', () => {
await waitFor(() => expect(getByText('swapReviewScreen.complete')).not.toBeDisabled())

fireEvent.press(getByText('swapReviewScreen.complete'))
expect(mockStore.dispatch).toHaveBeenCalledWith(swapStart(mockSwap as any))
expect(mockStore.dispatch).toHaveBeenCalledWith(
swapStart({
...mockSwap,
quoteReceivedAt: quoteReceivedTimestamp,
} as any)
)
})

it('should have correct analytics on swap submission', async () => {
Expand Down
14 changes: 11 additions & 3 deletions src/swap/SwapReviewScreen.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import BigNumber from 'bignumber.js'
import React, { useEffect, useState } from 'react'
import React, { useEffect, useRef, useState } from 'react'
import { useAsync } from 'react-async-hook'
import { useTranslation } from 'react-i18next'
import { RefreshControl, ScrollView, StyleSheet, Text, View } from 'react-native'
Expand Down Expand Up @@ -64,6 +64,7 @@ export function SwapReviewScreen() {
const walletAddress = useSelector(walletAddressSelector)
const celoAddress = useSelector(celoAddressSelector)
const feeCurrency = useFeeCurrency() ?? celoAddress
const quoteReceivedAtRef = useRef<number | undefined>()

const estimateFeeAmount = () => {
if (!feeCurrency || !swapResponse || !celoAddress || !tokensByAddress) {
Expand Down Expand Up @@ -138,6 +139,7 @@ export function SwapReviewScreen() {
const queryParams = new URLSearchParams({ ...params }).toString()
const requestUrl = `${networkConfig.approveSwapUrl}?${queryParams}`
const response = await fetch(requestUrl)
quoteReceivedAtRef.current = Date.now()
if (!response.ok) {
throw new Error(
`Failure response fetching token swap quote. ${response.status} ${response.statusText}`
Expand Down Expand Up @@ -185,8 +187,14 @@ export function SwapReviewScreen() {
provider: swapResponse.details.swapProvider,
})
// Dispatch swap submission
if (userInput !== null) {
dispatch(swapStart({ ...swapResponse, userInput }))
if (userInput && quoteReceivedAtRef.current) {
dispatch(
swapStart({
...swapResponse,
userInput,
quoteReceivedAt: quoteReceivedAtRef.current,
})
)
}
}

Expand Down
17 changes: 17 additions & 0 deletions src/swap/saga.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,8 @@ const mockSwapTransaction = {
estimatedPriceImpact: '0.1',
}

const mockQuoteReceivedTimestamp = 1000000000000

const mockSwap = {
payload: {
approveTransaction: {
Expand All @@ -65,6 +67,7 @@ const mockSwap = {
details: {
swapProvider: '0x',
},
quoteReceivedAt: mockQuoteReceivedTimestamp,
},
}

Expand All @@ -73,6 +76,10 @@ describe(swapSubmitSaga, () => {
jest.clearAllMocks()
})

afterEach(() => {
jest.spyOn(Date, 'now').mockRestore()
})

const defaultProviders: (EffectProviders | StaticProvider)[] = [
[select(walletAddressSelector), mockAccount],
[call(getContractKit), contractKit],
Expand All @@ -93,6 +100,11 @@ describe(swapSubmitSaga, () => {
]

it('should complete swap', async () => {
jest
.spyOn(Date, 'now')
.mockReturnValueOnce(mockQuoteReceivedTimestamp + 2500) // swap submitted timestamp
.mockReturnValueOnce(mockQuoteReceivedTimestamp + 10000) // before send swap timestamp

await expectSaga(swapSubmitSaga, mockSwap)
.withState(store.getState())
.provide(defaultProviders)
Expand All @@ -115,10 +127,13 @@ describe(swapSubmitSaga, () => {
fromTokenBalance: '10000000000000000000',
swapApproveTxId: 'a uuid',
swapExecuteTxId: 'a uuid',
quoteToUserConfirmsSwapElapsedTimeInMs: 2500,
quoteToTransactionElapsedTimeInMs: 10000,
})
})

it('should set swap state correctly on error', async () => {
jest.spyOn(Date, 'now').mockReturnValueOnce(mockQuoteReceivedTimestamp + 30000) // swap submitted timestamp
;(sendTransaction as jest.Mock).mockImplementationOnce(() => {
throw new Error('fake error')
})
Expand All @@ -142,6 +157,8 @@ describe(swapSubmitSaga, () => {
fromTokenBalance: '10000000000000000000',
swapApproveTxId: 'a uuid',
swapExecuteTxId: 'a uuid',
quoteToUserConfirmsSwapElapsedTimeInMs: 30000,
quoteToTransactionElapsedTimeInMs: undefined,
})
})

Expand Down
23 changes: 22 additions & 1 deletion src/swap/saga.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { ContractKit } from '@celo/contractkit'
import { valueToBigNumber } from '@celo/contractkit/lib/wrappers/BaseWrapper'
import { PayloadAction } from '@reduxjs/toolkit'
import { SwapEvents } from 'src/analytics/Events'
import { SwapTimeMetrics } from 'src/analytics/Properties'
import ValoraAnalytics from 'src/analytics/ValoraAnalytics'
import { maxSwapSlippagePercentageSelector } from 'src/app/selectors'
import { navigate } from 'src/navigator/NavigationService'
Expand Down Expand Up @@ -53,6 +54,7 @@ function* handleSendSwapTransaction(
}

export function* swapSubmitSaga(action: PayloadAction<SwapInfo>) {
const swapSubmittedAt = Date.now()
const {
price,
guaranteedPrice,
Expand All @@ -68,6 +70,7 @@ export function* swapSubmitSaga(action: PayloadAction<SwapInfo>) {
? ('buyAmount' as const)
: ('sellAmount' as const)
const amount = action.payload.unvalidatedSwapTransaction[amountType]
const { quoteReceivedAt } = action.payload

const tokenBalances: TokenBalance[] = yield* select(swappableTokensSelector)
const fromToken = tokenBalances.find((token) => token.address === sellTokenAddress)
Expand All @@ -92,6 +95,13 @@ export function* swapSubmitSaga(action: PayloadAction<SwapInfo>) {
swapApproveTxId: swapApproveContext.id,
}

let quoteToTransactionElapsedTimeInMs: number | undefined

const getTimeMetrics = (): SwapTimeMetrics => ({
quoteToUserConfirmsSwapElapsedTimeInMs: swapSubmittedAt - quoteReceivedAt,
quoteToTransactionElapsedTimeInMs,
})

try {
// Navigate to swap pending screen
navigate(Screens.SwapExecuteScreen)
Expand Down Expand Up @@ -136,18 +146,29 @@ export function* swapSubmitSaga(action: PayloadAction<SwapInfo>) {
yield* put(swapExecute())
Logger.debug(TAG, `Starting to swap execute for address: ${walletAddress}`)

const beforeSwapExecutionTimestamp = Date.now()
quoteToTransactionElapsedTimeInMs = beforeSwapExecutionTimestamp - quoteReceivedAt
yield* call(
handleSendSwapTransaction,
{ ...action.payload.unvalidatedSwapTransaction },
swapExecuteContext
)

const timeMetrics = getTimeMetrics()

yield* put(swapSuccess())
vibrateSuccess()
ValoraAnalytics.track(SwapEvents.swap_execute_success, defaultSwapExecuteProps)
ValoraAnalytics.track(SwapEvents.swap_execute_success, {
...defaultSwapExecuteProps,
...timeMetrics,
})
} catch (error) {
const timeMetrics = getTimeMetrics()

Logger.error(TAG, 'Error while swapping', error)
ValoraAnalytics.track(SwapEvents.swap_execute_error, {
...defaultSwapExecuteProps,
...timeMetrics,
error: error.message,
})
yield* put(swapError())
Expand Down
1 change: 1 addition & 0 deletions src/swap/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,7 @@ export interface ApproveTransaction {

export type SwapInfo = FetchQuoteResponse & {
userInput: SwapUserInput
quoteReceivedAt: number
}

export interface FetchQuoteResponse {
Expand Down
4 changes: 4 additions & 0 deletions test/RootStateSchema.json
Original file line number Diff line number Diff line change
Expand Up @@ -3896,6 +3896,9 @@
],
"type": "object"
},
"quoteReceivedAt": {
"type": "number"
},
"unvalidatedSwapTransaction": {
"$ref": "#/definitions/SwapTransaction"
},
Expand All @@ -3906,6 +3909,7 @@
"required": [
"approveTransaction",
"details",
"quoteReceivedAt",
"unvalidatedSwapTransaction",
"userInput"
],
Expand Down

0 comments on commit 99c01bf

Please sign in to comment.