Commit cb094a1f authored by lynn's avatar lynn Committed by GitHub

feat: implement token selector events (#4067)

* init commit

* add amplitude ts sdk to package.json

* add more comments and documentation

* respond to vm comments

* respond to cmcewen comments

* fix: remove unused constants

* init commit

* adapt to web

* add optional event properties to trace

* correct telemetry to analytics

* change telemetry to analytics in doc

* fix: respond to cmcewen comments + initialize analytics in app.tsx + add missing return statement

* init commit

* respond to zzmp comments

* add token selected event

* fixes

* eliminate unnecessary state

* respond to part of zzmp comments

* respond to zzmp comments round 2

* fixes

* respond to zzmp comments

* add imported token event and other fixes

* also log onKeyPress for suggested tokens

* respond to cmcewen comments
parent d05fefc2
......@@ -7,9 +7,11 @@ export interface ITraceContext {
// Highest order context: eg Swap or Explore.
page?: PageName
// Enclosed section name. Can be as wide or narrow as necessary to
// provide context.
section?: SectionName | ModalName
// Enclosed section name. For contexts with modals, refers to the
// section of the page from which the user triggered the modal.
section?: SectionName
modal?: ModalName
// Element name mostly used to identify events sources
// Does not need to be unique given the higher order page and section.
......
......@@ -30,7 +30,7 @@ export const TraceEvent = memo((props: PropsWithChildren<TraceEventProps>) => {
return child
}
// For each child, augment event handlers defined in `actionNames` with event tracing
// For each child, augment event handlers defined in `events` with event tracing.
return cloneElement(child, getEventHandlers(child, traceContext, events, name, properties))
})
}
......@@ -42,7 +42,7 @@ export const TraceEvent = memo((props: PropsWithChildren<TraceEventProps>) => {
TraceEvent.displayName = 'TraceEvent'
/**
* Given a set of child element and action props, returns a spreadable
* Given a set of child element and event props, returns a spreadable
* object of the event handlers augmented with analytics logging.
*/
function getEventHandlers(
......@@ -61,7 +61,7 @@ function getEventHandlers(
child.props[event]?.apply(child, args)
// augment handler with analytics logging
sendAnalyticsEvent(name, { ...traceContext, ...properties })
sendAnalyticsEvent(name, { ...traceContext, ...properties, action: event })
}
}
......
......@@ -7,6 +7,9 @@
export enum EventName {
PAGE_VIEWED = 'Page Viewed',
SWAP_SUBMITTED = 'Swap Submitted',
TOKEN_IMPORTED = 'Token Imported',
TOKEN_SELECTED = 'Token Selected',
TOKEN_SELECTOR_OPENED = 'Token Selector Opened',
// alphabetize additional event names.
}
......@@ -25,12 +28,14 @@ export const enum PageName {
*/
export const enum SectionName {
CURRENCY_INPUT_PANEL = 'swap-currency-input',
CURRENCY_OUTPUT_PANEL = 'swap-currency-output',
// alphabetize additional section names.
}
/** Known modals for analytics purposes. */
export const enum ModalName {
SWAP = 'swap-modal',
TOKEN_SELECTOR = 'token-selector-modal',
// alphabetize additional modal names.
}
......@@ -39,8 +44,11 @@ export const enum ModalName {
* Use to identify low-level components given a TraceContext
*/
export const enum ElementName {
COMMON_BASES_CURRENCY_BUTTON = 'common-bases-currency-button',
CONFIRM_SWAP_BUTTON = 'confirm-swap-or-send',
IMPORT_TOKEN_BUTTON = 'import-token-button',
SWAP_BUTTON = 'swap-button',
TOKEN_SELECTOR_ROW = 'token-selector-row',
// alphabetize additional element names.
}
......@@ -51,5 +59,7 @@ export const enum ElementName {
*/
export enum Event {
onClick = 'onClick',
onKeyPress = 'onKeyPress',
onSelect = 'onSelect',
// alphabetize additional events.
}
......@@ -34,7 +34,7 @@ export function initializeAnalytics(isDevEnvironment = process.env.NODE_ENV ===
/** Sends an event to Amplitude. */
export function sendAnalyticsEvent(eventName: string, eventProperties?: Record<string, unknown>) {
if (process.env.NODE_ENV === 'development') {
console.debug(`[amplitude(${eventName})]: ${JSON.stringify(eventProperties)}`)
console.log(`[amplitude(${eventName})]: ${JSON.stringify(eventProperties)}`)
return
}
......
import { Currency } from '@uniswap/sdk-core'
import { Currency, Token } from '@uniswap/sdk-core'
import { ElementName, Event, EventName } from 'components/AmplitudeAnalytics/constants'
import { TraceEvent } from 'components/AmplitudeAnalytics/TraceEvent'
import { AutoColumn } from 'components/Column'
import CurrencyLogo from 'components/CurrencyLogo'
import { AutoRow } from 'components/Row'
......@@ -31,14 +33,35 @@ const BaseWrapper = styled.div<{ disable?: boolean }>`
filter: ${({ disable }) => disable && 'grayscale(1)'};
`
const formatAnalyticsEventProperties = (
currency: Currency,
tokenAddress: string | undefined,
searchQuery: string,
isAddressSearch: string | false
) => ({
token_symbol: currency?.symbol,
token_chain_id: currency?.chainId,
...(tokenAddress ? { token_address: tokenAddress } : {}),
is_suggested_token: true,
is_selected_from_list: false,
is_imported_by_user: false,
...(isAddressSearch === false
? { search_token_symbol_input: searchQuery }
: { search_token_address_input: isAddressSearch }),
})
export default function CommonBases({
chainId,
onSelect,
selectedCurrency,
searchQuery,
isAddressSearch,
}: {
chainId?: number
selectedCurrency?: Currency | null
onSelect: (currency: Currency) => void
searchQuery: string
isAddressSearch: string | false
}) {
const bases = typeof chainId !== 'undefined' ? COMMON_BASES[chainId] ?? [] : []
......@@ -47,7 +70,16 @@ export default function CommonBases({
<AutoRow gap="4px">
{bases.map((currency: Currency) => {
const isSelected = selectedCurrency?.equals(currency)
const tokenAddress = currency instanceof Token ? currency?.address : undefined
return (
<TraceEvent
events={[Event.onClick, Event.onKeyPress]}
name={EventName.TOKEN_SELECTED}
properties={formatAnalyticsEventProperties(currency, tokenAddress, searchQuery, isAddressSearch)}
element={ElementName.COMMON_BASES_CURRENCY_BUTTON}
key={currencyId(currency)}
>
<BaseWrapper
tabIndex={0}
onKeyPress={(e) => !isSelected && e.key === 'Enter' && onSelect(currency)}
......@@ -60,6 +92,7 @@ export default function CommonBases({
{currency.symbol}
</Text>
</BaseWrapper>
</TraceEvent>
)
})}
</AutoRow>
......
import { Trans } from '@lingui/macro'
import { Currency, CurrencyAmount, Token } from '@uniswap/sdk-core'
import { useWeb3React } from '@web3-react/core'
import { ElementName, Event, EventName } from 'components/AmplitudeAnalytics/constants'
import { TraceEvent } from 'components/AmplitudeAnalytics/TraceEvent'
import { LightGreyCard } from 'components/Card'
import QuestionHelper from 'components/QuestionHelper'
import useTheme from 'hooks/useTheme'
......@@ -106,6 +108,7 @@ function CurrencyRow({
otherSelected,
style,
showCurrencyAmount,
eventProperties,
}: {
currency: Currency
onSelect: () => void
......@@ -113,6 +116,7 @@ function CurrencyRow({
otherSelected: boolean
style: CSSProperties
showCurrencyAmount?: boolean
eventProperties: Record<string, unknown>
}) {
const { account } = useWeb3React()
const key = currencyKey(currency)
......@@ -123,6 +127,12 @@ function CurrencyRow({
// only show add or remove buttons if not on selected list
return (
<TraceEvent
events={[Event.onClick, Event.onKeyPress]}
name={EventName.TOKEN_SELECTED}
properties={{ is_imported_by_user: customAdded, ...eventProperties }}
element={ElementName.TOKEN_SELECTOR_ROW}
>
<MenuItem
tabIndex={0}
style={style}
......@@ -152,6 +162,7 @@ function CurrencyRow({
</RowFixed>
)}
</MenuItem>
</TraceEvent>
)
}
......@@ -186,6 +197,25 @@ function BreakLineComponent({ style }: { style: CSSProperties }) {
)
}
const formatAnalyticsEventProperties = (
token: Token,
index: number,
data: any[],
searchQuery: string,
isAddressSearch: string | false
) => ({
token_symbol: token?.symbol,
token_address: token?.address,
is_suggested_token: false,
is_selected_from_list: true,
scroll_position: '',
token_list_index: index,
token_list_length: data.length,
...(isAddressSearch === false
? { search_token_symbol_input: searchQuery }
: { search_token_address_input: isAddressSearch }),
})
export default function CurrencyList({
height,
currencies,
......@@ -198,6 +228,8 @@ export default function CurrencyList({
setImportToken,
showCurrencyAmount,
isLoading,
searchQuery,
isAddressSearch,
}: {
height: number
currencies: Currency[]
......@@ -210,6 +242,8 @@ export default function CurrencyList({
setImportToken: (token: Token) => void
showCurrencyAmount?: boolean
isLoading: boolean
searchQuery: string
isAddressSearch: string | false
}) {
const itemData: (Currency | BreakLine)[] = useMemo(() => {
if (otherListTokens && otherListTokens?.length > 0) {
......@@ -257,6 +291,7 @@ export default function CurrencyList({
onSelect={handleSelect}
otherSelected={otherSelected}
showCurrencyAmount={showCurrencyAmount}
eventProperties={formatAnalyticsEventProperties(token, index, data, searchQuery, isAddressSearch)}
/>
)
} else {
......@@ -272,6 +307,8 @@ export default function CurrencyList({
showImportView,
showCurrencyAmount,
isLoading,
isAddressSearch,
searchQuery,
]
)
......
......@@ -2,6 +2,8 @@
import { t, Trans } from '@lingui/macro'
import { Currency, Token } from '@uniswap/sdk-core'
import { useWeb3React } from '@web3-react/core'
import { EventName, ModalName } from 'components/AmplitudeAnalytics/constants'
import { Trace } from 'components/AmplitudeAnalytics/Trace'
import { sendEvent } from 'components/analytics'
import useDebounce from 'hooks/useDebounce'
import { useOnClickOutside } from 'hooks/useOnClickOutside'
......@@ -189,6 +191,7 @@ export function CurrencySearch({
}, [])
return (
<Trace name={EventName.TOKEN_SELECTOR_OPENED} modal={ModalName.TOKEN_SELECTOR} shouldLogImpression={true}>
<ContentWrapper>
<PaddedColumn gap="16px">
<RowBetween>
......@@ -210,7 +213,13 @@ export function CurrencySearch({
/>
</Row>
{showCommonBases && (
<CommonBases chainId={chainId} onSelect={handleCurrencySelect} selectedCurrency={selectedCurrency} />
<CommonBases
chainId={chainId}
onSelect={handleCurrencySelect}
selectedCurrency={selectedCurrency}
searchQuery={searchQuery}
isAddressSearch={isAddressSearch}
/>
)}
</PaddedColumn>
<Separator />
......@@ -234,6 +243,8 @@ export function CurrencySearch({
setImportToken={setImportToken}
showCurrencyAmount={showCurrencyAmount}
isLoading={balancesIsLoading && !tokenLoaderTimerElapsed}
searchQuery={searchQuery}
isAddressSearch={isAddressSearch}
/>
)}
</AutoSizer>
......@@ -260,5 +271,6 @@ export function CurrencySearch({
</Row>
</Footer>
</ContentWrapper>
</Trace>
)
}
import { Plural, Trans } from '@lingui/macro'
import { Currency, Token } from '@uniswap/sdk-core'
import { TokenList } from '@uniswap/token-lists'
import { ElementName, Event, EventName } from 'components/AmplitudeAnalytics/constants'
import { TraceEvent } from 'components/AmplitudeAnalytics/TraceEvent'
import { ButtonPrimary } from 'components/Button'
import { AutoColumn } from 'components/Column'
import { RowBetween } from 'components/Row'
......@@ -30,6 +32,12 @@ interface ImportProps {
handleCurrencySelect?: (currency: Currency) => void
}
const formatAnalyticsEventProperties = (tokens: Token[]) => ({
token_symbols: tokens.map((token) => token?.symbol),
token_addresses: tokens.map((token) => token?.address),
token_chain_ids: tokens.map((token) => token?.chainId),
})
export function ImportToken(props: ImportProps) {
const { tokens, list, onBack, onDismiss, handleCurrencySelect } = props
const theme = useTheme()
......@@ -42,6 +50,7 @@ export function ImportToken(props: ImportProps) {
if (intersection.size > 0) {
return <BlockedToken onBack={onBack} onDismiss={onDismiss} blockedTokens={Array.from(intersection)} />
}
return (
<Wrapper>
<PaddedColumn gap="14px" style={{ width: '100%', flex: '1 1' }}>
......@@ -67,6 +76,12 @@ export function ImportToken(props: ImportProps) {
{tokens.map((token) => (
<TokenImportCard token={token} list={list} key={'import' + token.address} />
))}
<TraceEvent
events={[Event.onClick]}
name={EventName.TOKEN_IMPORTED}
properties={formatAnalyticsEventProperties(tokens)}
element={ElementName.IMPORT_TOKEN_BUTTON}
>
<ButtonPrimary
altDisabledStyle={true}
$borderRadius="20px"
......@@ -79,6 +94,7 @@ export function ImportToken(props: ImportProps) {
>
<Trans>Import</Trans>
</ButtonPrimary>
</TraceEvent>
</AutoColumn>
</Wrapper>
)
......
......@@ -4,6 +4,8 @@ import { Currency, CurrencyAmount, Token, TradeType } from '@uniswap/sdk-core'
import { Trade as V2Trade } from '@uniswap/v2-sdk'
import { Trade as V3Trade } from '@uniswap/v3-sdk'
import { useWeb3React } from '@web3-react/core'
import { PageName, SectionName } from 'components/AmplitudeAnalytics/constants'
import { Trace } from 'components/AmplitudeAnalytics/Trace'
import { sendEvent } from 'components/analytics'
import { NetworkAlert } from 'components/NetworkAlert/NetworkAlert'
import SwapDetailsDropdown from 'components/swap/SwapDetailsDropdown'
......@@ -396,6 +398,7 @@ export default function Swap({ history }: RouteComponentProps) {
const priceImpactTooHigh = priceImpactSeverity > 3 && !isExpertMode
return (
<Trace page={PageName.SWAP_PAGE} shouldLogImpression={false}>
<>
<TokenWarningModal
isOpen={importTokensNotInDefault.length > 0 && !dismissTokenWarning}
......@@ -422,9 +425,14 @@ export default function Swap({ history }: RouteComponentProps) {
<AutoColumn gap={'sm'}>
<div style={{ display: 'relative' }}>
<Trace section={SectionName.CURRENCY_INPUT_PANEL}>
<CurrencyInputPanel
label={
independentField === Field.OUTPUT && !showWrap ? <Trans>From (at most)</Trans> : <Trans>From</Trans>
independentField === Field.OUTPUT && !showWrap ? (
<Trans>From (at most)</Trans>
) : (
<Trans>From</Trans>
)
}
value={formattedAmounts[Field.INPUT]}
showMaxButton={showMaxButton}
......@@ -435,9 +443,10 @@ export default function Swap({ history }: RouteComponentProps) {
onCurrencySelect={handleInputSelect}
otherCurrency={currencies[Field.OUTPUT]}
showCommonBases={true}
id="swap-currency-input"
id={SectionName.CURRENCY_INPUT_PANEL}
loading={independentField === Field.OUTPUT && routeIsSyncing}
/>
</Trace>
<ArrowWrapper clickable>
<ArrowDown
size="16"
......@@ -448,10 +457,13 @@ export default function Swap({ history }: RouteComponentProps) {
color={currencies[Field.INPUT] && currencies[Field.OUTPUT] ? theme.text1 : theme.text3}
/>
</ArrowWrapper>
<Trace section={SectionName.CURRENCY_OUTPUT_PANEL}>
<CurrencyInputPanel
value={formattedAmounts[Field.OUTPUT]}
onUserInput={handleTypeOutput}
label={independentField === Field.INPUT && !showWrap ? <Trans>To (at least)</Trans> : <Trans>To</Trans>}
label={
independentField === Field.INPUT && !showWrap ? <Trans>To (at least)</Trans> : <Trans>To</Trans>
}
showMaxButton={false}
hideBalance={false}
fiatValue={fiatValueOutput ?? undefined}
......@@ -460,9 +472,10 @@ export default function Swap({ history }: RouteComponentProps) {
onCurrencySelect={handleOutputSelect}
otherCurrency={currencies[Field.INPUT]}
showCommonBases={true}
id="swap-currency-output"
id={SectionName.CURRENCY_OUTPUT_PANEL}
loading={independentField === Field.INPUT && routeIsSyncing}
/>
</Trace>
</div>
{recipient !== null && !showWrap ? (
......@@ -539,7 +552,8 @@ export default function Swap({ history }: RouteComponentProps) {
style={{ marginRight: '8px', flexShrink: 0 }}
/>
{/* we need to shorten this string on mobile */}
{approvalState === ApprovalState.APPROVED || signatureState === UseERC20PermitState.SIGNED ? (
{approvalState === ApprovalState.APPROVED ||
signatureState === UseERC20PermitState.SIGNED ? (
<Trans>You can now trade {currencies[Field.INPUT]?.symbol}</Trans>
) : (
<Trans>Allow the Uniswap Protocol to use your {currencies[Field.INPUT]?.symbol}</Trans>
......@@ -651,5 +665,6 @@ export default function Swap({ history }: RouteComponentProps) {
/>
)}
</>
</Trace>
)
}
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