Commit e3c589ae authored by Zach Pomerantz's avatar Zach Pomerantz Committed by GitHub

fix: wallet connection analytics (#6296)

* test: web3

* fix: wallet connection analytics

* test: wallet connection analytics

* build: upgrade web3-react@^8.2

* test: web3

* chore: better merge

* fix: unmock

* fix: use usePrevious

* fix: naming

* fix: deps
parent 6d60aca4
import { sendAnalyticsEvent, user } from '@uniswap/analytics' import { sendAnalyticsEvent } from '@uniswap/analytics'
import { CustomUserProperties, InterfaceEventName, WalletConnectionResult } from '@uniswap/analytics-events' import { InterfaceEventName, WalletConnectionResult } from '@uniswap/analytics-events'
import { getWalletMeta } from '@uniswap/conedison/provider/meta'
import { useWeb3React } from '@web3-react/core' import { useWeb3React } from '@web3-react/core'
import { useAccountDrawer } from 'components/AccountDrawer' import { useAccountDrawer } from 'components/AccountDrawer'
import IconButton from 'components/AccountDrawer/IconButton' import IconButton from 'components/AccountDrawer/IconButton'
...@@ -8,14 +7,12 @@ import { sendEvent } from 'components/analytics' ...@@ -8,14 +7,12 @@ import { sendEvent } from 'components/analytics'
import { AutoColumn } from 'components/Column' import { AutoColumn } from 'components/Column'
import { AutoRow } from 'components/Row' import { AutoRow } from 'components/Row'
import { Connection, ConnectionType, getConnections, networkConnection } from 'connection' import { Connection, ConnectionType, getConnections, networkConnection } from 'connection'
import { useGetConnection } from 'connection'
import { ErrorCode } from 'connection/utils' import { ErrorCode } from 'connection/utils'
import { isSupportedChain } from 'constants/chains' import { isSupportedChain } from 'constants/chains'
import { useCallback, useEffect, useRef, useState } from 'react' import { useCallback, useEffect, useRef, useState } from 'react'
import { Settings } from 'react-feather' import { Settings } from 'react-feather'
import { useAppDispatch } from 'state/hooks' import { useAppDispatch } from 'state/hooks'
import { updateSelectedWallet } from 'state/user/reducer' import { updateSelectedWallet } from 'state/user/reducer'
import { useConnectedWallets } from 'state/wallets/hooks'
import styled from 'styled-components/macro' import styled from 'styled-components/macro'
import { ThemedText } from 'theme' import { ThemedText } from 'theme'
import { flexColumnNoWrap } from 'theme/styles' import { flexColumnNoWrap } from 'theme/styles'
...@@ -46,32 +43,6 @@ const PrivacyPolicyWrapper = styled.div` ...@@ -46,32 +43,6 @@ const PrivacyPolicyWrapper = styled.div`
padding: 0 4px; padding: 0 4px;
` `
const sendAnalyticsEventAndUserInfo = (
account: string,
walletType: string,
chainId: number | undefined,
isReconnect: boolean,
peerWalletAgent: string | undefined
) => {
// User properties *must* be set before sending corresponding event properties,
// so that the event contains the correct and up-to-date user properties.
user.set(CustomUserProperties.WALLET_ADDRESS, account)
user.set(CustomUserProperties.WALLET_TYPE, walletType)
user.set(CustomUserProperties.PEER_WALLET_AGENT, peerWalletAgent ?? '')
if (chainId) {
user.postInsert(CustomUserProperties.ALL_WALLET_CHAIN_IDS, chainId)
}
user.postInsert(CustomUserProperties.ALL_WALLET_ADDRESSES_CONNECTED, account)
sendAnalyticsEvent(InterfaceEventName.WALLET_CONNECT_TXN_COMPLETED, {
result: WalletConnectionResult.SUCCEEDED,
wallet_address: account,
wallet_type: walletType,
is_reconnect: isReconnect,
peer_wallet_agent: peerWalletAgent,
})
}
function didUserReject(connection: Connection, error: any): boolean { function didUserReject(connection: Connection, error: any): boolean {
return ( return (
error?.code === ErrorCode.USER_REJECTED_REQUEST || error?.code === ErrorCode.USER_REJECTED_REQUEST ||
...@@ -82,16 +53,13 @@ function didUserReject(connection: Connection, error: any): boolean { ...@@ -82,16 +53,13 @@ function didUserReject(connection: Connection, error: any): boolean {
export default function WalletModal({ openSettings }: { openSettings: () => void }) { export default function WalletModal({ openSettings }: { openSettings: () => void }) {
const dispatch = useAppDispatch() const dispatch = useAppDispatch()
const { connector, account, chainId, provider } = useWeb3React() const { connector, chainId } = useWeb3React()
const [drawerOpen, toggleWalletDrawer] = useAccountDrawer() const [drawerOpen, toggleWalletDrawer] = useAccountDrawer()
const [connectedWallets, addWalletToConnectedWallets] = useConnectedWallets()
const [lastActiveWalletAddress, setLastActiveWalletAddress] = useState<string | undefined>(account)
const [pendingConnection, setPendingConnection] = useState<Connection | undefined>() const [pendingConnection, setPendingConnection] = useState<Connection | undefined>()
const [pendingError, setPendingError] = useState<any>() const [pendingError, setPendingError] = useState<any>()
const connections = getConnections() const connections = getConnections()
const getConnection = useGetConnection()
useEffect(() => { useEffect(() => {
// Clean up errors when the dropdown closes // Clean up errors when the dropdown closes
...@@ -112,28 +80,6 @@ export default function WalletModal({ openSettings }: { openSettings: () => void ...@@ -112,28 +80,6 @@ export default function WalletModal({ openSettings }: { openSettings: () => void
} }
}, [chainId, connector]) }, [chainId, connector])
// When new wallet is successfully set by the user, trigger logging of Amplitude analytics event.
useEffect(() => {
if (account && account !== lastActiveWalletAddress) {
const walletName = getConnection(connector).getName()
const peerWalletAgent = provider ? getWalletMeta(provider)?.agent : undefined
const isReconnect =
connectedWallets.filter((wallet) => wallet.account === account && wallet.walletType === walletName).length > 0
sendAnalyticsEventAndUserInfo(account, walletName, chainId, isReconnect, peerWalletAgent)
if (!isReconnect) addWalletToConnectedWallets({ account, walletType: walletName })
}
setLastActiveWalletAddress(account)
}, [
connectedWallets,
addWalletToConnectedWallets,
lastActiveWalletAddress,
account,
connector,
chainId,
provider,
getConnection,
])
// Used to track the state of the drawer in async function // Used to track the state of the drawer in async function
const drawerOpenRef = useRef(drawerOpen) const drawerOpenRef = useRef(drawerOpen)
drawerOpenRef.current = drawerOpen drawerOpenRef.current = drawerOpen
......
import { act, render } from '@testing-library/react' import { act, render } from '@testing-library/react'
import { sendAnalyticsEvent, user } from '@uniswap/analytics'
import { InterfaceEventName, WalletConnectionResult } from '@uniswap/analytics-events'
import { initializeConnector, MockEIP1193Provider } from '@web3-react/core' import { initializeConnector, MockEIP1193Provider } from '@web3-react/core'
import { EIP1193 } from '@web3-react/eip1193' import { EIP1193 } from '@web3-react/eip1193'
import { Provider as EIP1193Provider } from '@web3-react/types' import { Provider as EIP1193Provider } from '@web3-react/types'
import { Connection, ConnectionType } from 'connection' import { Connection, ConnectionType, useGetConnection } from 'connection'
import useEagerlyConnect from 'hooks/useEagerlyConnect' import useEagerlyConnect from 'hooks/useEagerlyConnect'
import useOrderedConnections from 'hooks/useOrderedConnections' import useOrderedConnections from 'hooks/useOrderedConnections'
import { Provider } from 'react-redux' import { Provider } from 'react-redux'
...@@ -11,9 +13,27 @@ import { mocked } from 'test-utils/mocked' ...@@ -11,9 +13,27 @@ import { mocked } from 'test-utils/mocked'
import Web3Provider from '.' import Web3Provider from '.'
jest.mock('@uniswap/analytics', () => ({
sendAnalyticsEvent: jest.fn(),
user: { set: jest.fn(), postInsert: jest.fn() },
}))
jest.mock('connection', () => {
const { ConnectionType } = jest.requireActual('connection')
return { ConnectionType, useGetConnection: jest.fn() }
})
jest.mock('hooks/useEagerlyConnect', () => jest.fn()) jest.mock('hooks/useEagerlyConnect', () => jest.fn())
jest.mock('hooks/useOrderedConnections', () => jest.fn()) jest.mock('hooks/useOrderedConnections', () => jest.fn())
jest.unmock('@web3-react/core')
function first<T>(array: T[]): T {
return array[0]
}
function last<T>(array: T[]): T {
return array[array.length - 1]
}
const UI = ( const UI = (
<Provider store={store}> <Provider store={store}>
<Web3Provider>{null}</Web3Provider> <Web3Provider>{null}</Web3Provider>
...@@ -45,4 +65,70 @@ describe('Web3Provider', () => { ...@@ -45,4 +65,70 @@ describe('Web3Provider', () => {
expect(useEagerlyConnect).toHaveBeenCalled() expect(useEagerlyConnect).toHaveBeenCalled()
expect(result).toBeTruthy() expect(result).toBeTruthy()
}) })
describe('analytics', () => {
beforeEach(() => {
mocked(useGetConnection).mockReturnValue(jest.fn().mockReturnValue(connection))
})
it('sends event when the active account changes', async () => {
// Arrange
const result = render(UI)
await act(async () => {
await result
})
// Act
act(() => {
provider.emitConnect('0x1')
provider.emitAccountsChanged(['0x0000000000000000000000000000000000000000'])
})
// Assert
expect(sendAnalyticsEvent).toHaveBeenCalledTimes(1)
expect(sendAnalyticsEvent).toHaveBeenCalledWith(InterfaceEventName.WALLET_CONNECT_TXN_COMPLETED, {
result: WalletConnectionResult.SUCCEEDED,
wallet_address: '0x0000000000000000000000000000000000000000',
wallet_type: 'test',
is_reconnect: false,
peer_wallet_agent: '(Injected)',
})
expect(first(mocked(sendAnalyticsEvent).mock.invocationCallOrder)).toBeGreaterThan(
last(mocked(user.set).mock.invocationCallOrder)
)
expect(first(mocked(sendAnalyticsEvent).mock.invocationCallOrder)).toBeGreaterThan(
last(mocked(user.postInsert).mock.invocationCallOrder)
)
})
it('sends event with is_reconnect when a previous account reconnects', async () => {
// Arrange
const result = render(UI)
await act(async () => {
await result
})
// Act
act(() => {
provider.emitConnect('0x1')
provider.emitAccountsChanged(['0x0000000000000000000000000000000000000000'])
})
act(() => {
provider.emitAccountsChanged(['0x0000000000000000000000000000000000000001'])
})
act(() => {
provider.emitAccountsChanged(['0x0000000000000000000000000000000000000000'])
})
// Assert
expect(sendAnalyticsEvent).toHaveBeenCalledTimes(3)
expect(sendAnalyticsEvent).toHaveBeenCalledWith(InterfaceEventName.WALLET_CONNECT_TXN_COMPLETED, {
result: WalletConnectionResult.SUCCEEDED,
wallet_address: '0x0000000000000000000000000000000000000000',
wallet_type: 'test',
is_reconnect: true,
peer_wallet_agent: '(Injected)',
})
})
})
}) })
import { sendAnalyticsEvent, user } from '@uniswap/analytics'
import { CustomUserProperties, InterfaceEventName, WalletConnectionResult } from '@uniswap/analytics-events'
import { getWalletMeta } from '@uniswap/conedison/provider/meta'
import { useWeb3React, Web3ReactHooks, Web3ReactProvider } from '@web3-react/core' import { useWeb3React, Web3ReactHooks, Web3ReactProvider } from '@web3-react/core'
import { Connector } from '@web3-react/types' import { Connector } from '@web3-react/types'
import { useGetConnection } from 'connection'
import { isSupportedChain } from 'constants/chains' import { isSupportedChain } from 'constants/chains'
import { RPC_PROVIDERS } from 'constants/providers' import { RPC_PROVIDERS } from 'constants/providers'
import { TraceJsonRpcVariant, useTraceJsonRpcFlag } from 'featureFlags/flags/traceJsonRpc' import { TraceJsonRpcVariant, useTraceJsonRpcFlag } from 'featureFlags/flags/traceJsonRpc'
import useEagerlyConnect from 'hooks/useEagerlyConnect' import useEagerlyConnect from 'hooks/useEagerlyConnect'
import useOrderedConnections from 'hooks/useOrderedConnections' import useOrderedConnections from 'hooks/useOrderedConnections'
import usePrevious from 'hooks/usePrevious'
import { ReactNode, useEffect, useMemo } from 'react' import { ReactNode, useEffect, useMemo } from 'react'
import { useConnectedWallets } from 'state/wallets/hooks'
export default function Web3Provider({ children }: { children: ReactNode }) { export default function Web3Provider({ children }: { children: ReactNode }) {
useEagerlyConnect() useEagerlyConnect()
...@@ -16,17 +22,19 @@ export default function Web3Provider({ children }: { children: ReactNode }) { ...@@ -16,17 +22,19 @@ export default function Web3Provider({ children }: { children: ReactNode }) {
return ( return (
<Web3ReactProvider connectors={connectors} key={key}> <Web3ReactProvider connectors={connectors} key={key}>
<Tracer /> <Updater />
{children} {children}
</Web3ReactProvider> </Web3ReactProvider>
) )
} }
function Tracer() { /** A component to run hooks under the Web3ReactProvider context. */
const { chainId, provider } = useWeb3React() function Updater() {
const { account, chainId, connector, provider } = useWeb3React()
// Trace RPC calls (for debugging).
const networkProvider = isSupportedChain(chainId) ? RPC_PROVIDERS[chainId] : undefined const networkProvider = isSupportedChain(chainId) ? RPC_PROVIDERS[chainId] : undefined
const shouldTrace = useTraceJsonRpcFlag() === TraceJsonRpcVariant.Enabled const shouldTrace = useTraceJsonRpcFlag() === TraceJsonRpcVariant.Enabled
useEffect(() => { useEffect(() => {
if (shouldTrace) { if (shouldTrace) {
provider?.on('debug', trace) provider?.on('debug', trace)
...@@ -40,6 +48,40 @@ function Tracer() { ...@@ -40,6 +48,40 @@ function Tracer() {
} }
}, [networkProvider, provider, shouldTrace]) }, [networkProvider, provider, shouldTrace])
// Send analytics events when the active account changes.
const previousAccount = usePrevious(account)
const getConnection = useGetConnection()
const [connectedWallets, addConnectedWallet] = useConnectedWallets()
useEffect(() => {
if (account && account !== previousAccount) {
const walletType = getConnection(connector).getName()
const peerWalletAgent = provider ? getWalletMeta(provider)?.agent : undefined
const isReconnect = connectedWallets.some(
(wallet) => wallet.account === account && wallet.walletType === walletType
)
// User properties *must* be set before sending corresponding event properties,
// so that the event contains the correct and up-to-date user properties.
user.set(CustomUserProperties.WALLET_ADDRESS, account)
user.set(CustomUserProperties.WALLET_TYPE, walletType)
user.set(CustomUserProperties.PEER_WALLET_AGENT, peerWalletAgent ?? '')
if (chainId) {
user.postInsert(CustomUserProperties.ALL_WALLET_CHAIN_IDS, chainId)
}
user.postInsert(CustomUserProperties.ALL_WALLET_ADDRESSES_CONNECTED, account)
sendAnalyticsEvent(InterfaceEventName.WALLET_CONNECT_TXN_COMPLETED, {
result: WalletConnectionResult.SUCCEEDED,
wallet_address: account,
wallet_type: walletType,
is_reconnect: isReconnect,
peer_wallet_agent: peerWalletAgent,
})
addConnectedWallet({ account, walletType })
}
}, [account, addConnectedWallet, chainId, connectedWallets, connector, getConnection, previousAccount, provider])
return null return null
} }
......
...@@ -10,7 +10,7 @@ import { FeeOptions, toHex } from '@uniswap/v3-sdk' ...@@ -10,7 +10,7 @@ import { FeeOptions, toHex } from '@uniswap/v3-sdk'
import { useWeb3React } from '@web3-react/core' import { useWeb3React } from '@web3-react/core'
import { formatSwapSignedAnalyticsEventProperties } from 'lib/utils/analytics' import { formatSwapSignedAnalyticsEventProperties } from 'lib/utils/analytics'
import { useCallback } from 'react' import { useCallback } from 'react'
import { trace } from 'tracing' import { trace } from 'tracing/trace'
import { calculateGasMargin } from 'utils/calculateGasMargin' import { calculateGasMargin } from 'utils/calculateGasMargin'
import isZero from 'utils/isZero' import isZero from 'utils/isZero'
import { didUserReject, swapErrorToUserReadableMessage } from 'utils/swapErrorToUserReadableMessage' import { didUserReject, swapErrorToUserReadableMessage } from 'utils/swapErrorToUserReadableMessage'
......
...@@ -5,7 +5,7 @@ import { RPC_PROVIDERS } from 'constants/providers' ...@@ -5,7 +5,7 @@ import { RPC_PROVIDERS } from 'constants/providers'
import { getClientSideQuote, toSupportedChainId } from 'lib/hooks/routing/clientSideSmartOrderRouter' import { getClientSideQuote, toSupportedChainId } from 'lib/hooks/routing/clientSideSmartOrderRouter'
import ms from 'ms.macro' import ms from 'ms.macro'
import qs from 'qs' import qs from 'qs'
import { trace } from 'tracing' import { trace } from 'tracing/trace'
import { GetQuoteResult } from './types' import { GetQuoteResult } from './types'
......
import walletsReducer from './reducer' import walletsReducer from './reducer'
import { Wallet } from './types' import { Wallet } from './types'
const WALLET: Wallet = { account: '0x123', walletType: 'test' }
describe('walletsSlice reducers', () => { describe('walletsSlice reducers', () => {
it('should add a connected wallet', () => { it('should add a connected wallet', () => {
const initialState = { const initialState = { connectedWallets: [] }
connectedWallets: [],
}
const wallet = {
address: '0x123',
chainId: 1,
}
const action = { const action = {
type: 'wallets/addConnectedWallet', type: 'wallets/addConnectedWallet',
payload: wallet, payload: WALLET,
}
const expectedState = {
connectedWallets: [wallet],
} }
const expectedState = { connectedWallets: [WALLET] }
expect(walletsReducer(initialState, action)).toEqual(expectedState) expect(walletsReducer(initialState, action)).toEqual(expectedState)
}) })
it('should remove a connected wallet', () => { it('should not duplicate a connected wallet', () => {
const wallet: Wallet = { const initialState = { connectedWallets: [WALLET] }
walletType: 'metamask',
account: '0x123',
}
const initialState = {
connectedWallets: [wallet],
}
const action = { const action = {
type: 'wallets/removeConnectedWallet', type: 'wallets/addConnectedWallet',
payload: wallet, payload: WALLET,
}
const expectedState = {
connectedWallets: [],
} }
const expectedState = { connectedWallets: [WALLET] }
expect(walletsReducer(initialState, action)).toEqual(expectedState) expect(walletsReducer(initialState, action)).toEqual(expectedState)
}) })
}) })
...@@ -18,13 +18,8 @@ const walletsSlice = createSlice({ ...@@ -18,13 +18,8 @@ const walletsSlice = createSlice({
initialState, initialState,
reducers: { reducers: {
addConnectedWallet(state, { payload }) { addConnectedWallet(state, { payload }) {
const existsAlready = state.connectedWallets.find((wallet) => shallowEqual(payload, wallet)) if (state.connectedWallets.some((wallet) => shallowEqual(payload, wallet))) return
if (!existsAlready) { state.connectedWallets = [...state.connectedWallets, payload]
state.connectedWallets = state.connectedWallets.concat(payload)
}
},
removeConnectedWallet(state, { payload }) {
state.connectedWallets = state.connectedWallets.filter((wallet) => !shallowEqual(wallet, payload))
}, },
}, },
}) })
......
...@@ -9,8 +9,6 @@ import { getEnvName, isProductionEnv } from 'utils/env' ...@@ -9,8 +9,6 @@ import { getEnvName, isProductionEnv } from 'utils/env'
import { beforeSend } from './errors' import { beforeSend } from './errors'
export { trace } from './trace'
// Dump some metadata into the window to allow client verification. // Dump some metadata into the window to allow client verification.
window.GIT_COMMIT_HASH = process.env.REACT_APP_GIT_COMMIT_HASH window.GIT_COMMIT_HASH = process.env.REACT_APP_GIT_COMMIT_HASH
......
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