From 715555f3409a0426a54ceb3d35b5e18541f8507f Mon Sep 17 00:00:00 2001 From: eddie <66155195+just-toby@users.noreply.github.com> Date: Thu, 3 Aug 2023 11:33:16 -0700 Subject: [PATCH] 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 Co-authored-by: Jordan Frankfurt --- cypress/e2e/swap/timeToSwap.test.ts | 45 +++++++++++++++++++++++++++++ cypress/support/commands.ts | 30 +++++++++++++++++++ cypress/support/setupTests.ts | 1 + src/state/transactions/updater.tsx | 28 +++++++++++++++++- 4 files changed, 103 insertions(+), 1 deletion(-) create mode 100644 cypress/e2e/swap/timeToSwap.test.ts diff --git a/cypress/e2e/swap/timeToSwap.test.ts b/cypress/e2e/swap/timeToSwap.test.ts new file mode 100644 index 0000000000..7d5c6c8be2 --- /dev/null +++ b/cypress/e2e/swap/timeToSwap.test.ts @@ -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') + }) + }) +}) diff --git a/cypress/support/commands.ts b/cypress/support/commands.ts index fcbf3540ed..874822b8c8 100644 --- a/cypress/support/commands.ts +++ b/cypress/support/commands.ts @@ -12,6 +12,16 @@ declare global { interface ApplicationWindow { ethereum: Eip1193Bridge } + interface Chainable { + /** + * 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} + */ + waitForAmplitudeEvent(eventName: string, timeout?: number): Chainable + } interface VisitOptions { serviceWorker?: true featureFlags?: Array @@ -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() +}) diff --git a/cypress/support/setupTests.ts b/cypress/support/setupTests.ts index 81a47e8c3c..80c46df59c 100644 --- a/cypress/support/setupTests.ts +++ b/cypress/support/setupTests.ts @@ -19,6 +19,7 @@ beforeEach(() => { cy.intercept('https://api.uniswap.org/v1/amplitude-proxy', (req) => { const requestBody = JSON.stringify(req.body) const byteSize = new Blob([requestBody]).size + req.alias = 'amplitude' req.reply( JSON.stringify({ code: 200, diff --git a/src/state/transactions/updater.tsx b/src/state/transactions/updater.tsx index 4389ddff80..732b5580cf 100644 --- a/src/state/transactions/updater.tsx +++ b/src/state/transactions/updater.tsx @@ -1,5 +1,7 @@ import { TransactionReceipt } from '@ethersproject/abstract-provider' +import { SwapEventName } from '@uniswap/analytics-events' import { useWeb3React } from '@web3-react/core' +import { sendAnalyticsEvent, useTrace } from 'analytics' import { DEFAULT_TXN_DISMISS_MS, L2_TXN_DISMISS_MS } from 'constants/misc' import LibUpdater from 'lib/hooks/transactions/updater' 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() { + const analyticsContext = useTrace() const { chainId } = useWeb3React() const addPopup = useAddPopup() // 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( { type: PopupType.Transaction, @@ -64,7 +90,7 @@ export default function Updater() { isL2 ? L2_TXN_DISMISS_MS : DEFAULT_TXN_DISMISS_MS ) }, - [addPopup, dispatch, isL2] + [addPopup, analyticsContext, dispatch, isL2] ) return