feat: time-to-swap metric (#7051)

* test(cypress): disable infura from browser

* build: typecheck cypress

* build: es5

* build: rm cypress videos

* fix failing tests

* skip nft failure and rm infra-175

* feat: implement tts metric

* wip e2e test

* fix: improve v2 network support (#7012)

* fix: improve v2 network support

* add an unsupported message to all v2 pages

* test: add v2 pool tests

* add guard on transaction callbacks

* fix: dep array

---------

Co-authored-by: eddie <66155195+just-toby@users.noreply.github.com>

* test: adjust test options

* fix: move to helper method

* fix: merge and make code style change

* fix: use local variable to track first event

* fix: amplitude cypress command

* fix: use file-level var

* fix: clear input in test

---------

Co-authored-by: Zach Pomerantz <zzmp@uniswap.org>
Co-authored-by: Jordan Frankfurt <jordanwfrankfurt@gmail.com>
This commit is contained in:
eddie 2023-08-03 11:33:16 -07:00 committed by GitHub
parent 6a1f17ab5a
commit 715555f340
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
4 changed files with 103 additions and 1 deletions

@ -0,0 +1,45 @@
import { SwapEventName } from '@uniswap/analytics-events'
import { USDC_MAINNET } from '../../../src/constants/tokens'
import { getTestSelector } from '../../utils'
describe('time-to-swap logging', () => {
it('completes two swaps and verifies the TTS logging for the first', () => {
cy.visit(`/swap?inputCurrency=ETH&outputCurrency=${USDC_MAINNET.address}`)
cy.hardhat()
// First swap in the session:
// Enter amount to 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', '')
// Submit transaction
cy.get('#swap-button').click()
cy.contains('Confirm swap').click()
cy.get(getTestSelector('confirmation-close-icon')).click()
cy.get(getTestSelector('popups')).contains('Swapped')
// Verify logging
cy.waitForAmplitudeEvent(SwapEventName.SWAP_TRANSACTION_COMPLETED).then((event: any) => {
cy.wrap(event.event_properties).should('have.property', 'time_to_swap')
cy.wrap(event.event_properties.time_to_swap).should('be.a', 'number')
cy.wrap(event.event_properties.time_to_swap).should('be.gte', 0)
})
// Second swap in the session:
// Enter amount to swap
cy.get('#swap-currency-output .token-amount-input').clear().type('1').should('have.value', '1')
cy.get('#swap-currency-input .token-amount-input').should('not.have.value', '')
// Submit transaction
cy.get('#swap-button').click()
cy.contains('Confirm swap').click()
cy.get(getTestSelector('confirmation-close-icon')).click()
cy.get(getTestSelector('popups')).contains('Swapped')
cy.waitForAmplitudeEvent(SwapEventName.SWAP_TRANSACTION_COMPLETED).then((event: any) => {
cy.wrap(event.event_properties).should('not.have.property', 'time_to_swap')
})
})
})

@ -12,6 +12,16 @@ declare global {
interface ApplicationWindow { interface ApplicationWindow {
ethereum: Eip1193Bridge ethereum: Eip1193Bridge
} }
interface Chainable<Subject> {
/**
* Wait for a specific event to be sent to amplitude. If the event is found, the subject will be the event.
*
* @param {string} eventName - The type of the event to search for e.g. SwapEventName.SWAP_TRANSACTION_COMPLETED
* @param {number} timeout - The maximum amount of time (in ms) to wait for the event.
* @returns {Chainable<Subject>}
*/
waitForAmplitudeEvent(eventName: string, timeout?: number): Chainable<Subject>
}
interface VisitOptions { interface VisitOptions {
serviceWorker?: true serviceWorker?: true
featureFlags?: Array<FeatureFlag> featureFlags?: Array<FeatureFlag>
@ -66,3 +76,23 @@ Cypress.Commands.overwrite(
) )
} }
) )
Cypress.Commands.add('waitForAmplitudeEvent', (eventName, timeout = 5000 /* 5s */) => {
const startTime = new Date().getTime()
function checkRequest() {
return cy.wait('@amplitude', { timeout }).then((interception) => {
const events = interception.request.body.events
const event = events.find((event: any) => event.event_type === eventName)
if (event) {
return cy.wrap(event)
} else if (new Date().getTime() - startTime > timeout) {
throw new Error(`Event ${eventName} not found within the specified timeout`)
} else {
return checkRequest()
}
})
}
return checkRequest()
})

@ -19,6 +19,7 @@ beforeEach(() => {
cy.intercept('https://api.uniswap.org/v1/amplitude-proxy', (req) => { cy.intercept('https://api.uniswap.org/v1/amplitude-proxy', (req) => {
const requestBody = JSON.stringify(req.body) const requestBody = JSON.stringify(req.body)
const byteSize = new Blob([requestBody]).size const byteSize = new Blob([requestBody]).size
req.alias = 'amplitude'
req.reply( req.reply(
JSON.stringify({ JSON.stringify({
code: 200, code: 200,

@ -1,5 +1,7 @@
import { TransactionReceipt } from '@ethersproject/abstract-provider' import { TransactionReceipt } from '@ethersproject/abstract-provider'
import { SwapEventName } from '@uniswap/analytics-events'
import { useWeb3React } from '@web3-react/core' import { useWeb3React } from '@web3-react/core'
import { sendAnalyticsEvent, useTrace } from 'analytics'
import { DEFAULT_TXN_DISMISS_MS, L2_TXN_DISMISS_MS } from 'constants/misc' import { DEFAULT_TXN_DISMISS_MS, L2_TXN_DISMISS_MS } from 'constants/misc'
import LibUpdater from 'lib/hooks/transactions/updater' import LibUpdater from 'lib/hooks/transactions/updater'
import { useCallback, useMemo } from 'react' import { useCallback, useMemo } from 'react'
@ -25,7 +27,20 @@ export function toSerializableReceipt(receipt: TransactionReceipt): Serializable
} }
} }
// We only log the time-to-swap metric for the first swap of a session.
let hasReportedTimeToSwap = false
/**
* Returns the time elapsed between page load and now,
* if the time-to-swap mark doesn't already exist.
*/
function getElapsedTime(): number {
const timeToSwap = performance.mark('time-to-swap')
return timeToSwap.startTime
}
export default function Updater() { export default function Updater() {
const analyticsContext = useTrace()
const { chainId } = useWeb3React() const { chainId } = useWeb3React()
const addPopup = useAddPopup() const addPopup = useAddPopup()
// speed up popup dismisall time if on L2 // speed up popup dismisall time if on L2
@ -55,6 +70,17 @@ export default function Updater() {
}) })
) )
const elapsedTime = getElapsedTime()
sendAnalyticsEvent(SwapEventName.SWAP_TRANSACTION_COMPLETED, {
// if timeToSwap was already set, we already logged this session
time_to_swap: hasReportedTimeToSwap ? undefined : elapsedTime,
hash,
...analyticsContext,
})
hasReportedTimeToSwap = true
addPopup( addPopup(
{ {
type: PopupType.Transaction, type: PopupType.Transaction,
@ -64,7 +90,7 @@ export default function Updater() {
isL2 ? L2_TXN_DISMISS_MS : DEFAULT_TXN_DISMISS_MS isL2 ? L2_TXN_DISMISS_MS : DEFAULT_TXN_DISMISS_MS
) )
}, },
[addPopup, dispatch, isL2] [addPopup, analyticsContext, dispatch, isL2]
) )
return <LibUpdater pendingTransactions={pendingTransactions} onCheck={onCheck} onReceipt={onReceipt} /> return <LibUpdater pendingTransactions={pendingTransactions} onCheck={onCheck} onReceipt={onReceipt} />