feat: Log NFT Sell events (#5106)

* Log profile page view

* Log sell flow started

* Add Start Listing event

* Add constant for list modal + useTrace

* Log sell item added

* Log listing completed

* Fix usd_value property

* Move log to startListingFlow

* Use Set to remove duplicate marketplaces

* Move listing completed event
This commit is contained in:
Greg Bugyis 2022-11-10 01:57:30 +02:00 committed by GitHub
parent dbf5c63ece
commit 48d5955185
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
5 changed files with 186 additions and 118 deletions

@ -24,6 +24,11 @@ export enum EventName {
NFT_BUY_BAG_SUCCEEDED = 'NFT Buy Bag Succeeded',
NFT_FILTER_OPENED = 'NFT Collection Filter Opened',
NFT_FILTER_SELECTED = 'NFT Filter Selected',
NFT_LISTING_SIGNED = 'NFT Listing Signed',
NFT_LISTING_COMPLETED = 'NFT Listing Success',
NFT_SELL_ITEM_ADDED = 'NFT Sell Item Added',
NFT_SELL_SELECTED = 'NFT Sell Selected',
NFT_SELL_START_LISTING = 'NFT Sell Start Listing',
NFT_TRENDING_ROW_SELECTED = 'Trending Row Selected',
SWAP_AUTOROUTER_VISUALIZATION_EXPANDED = 'Swap Autorouter Visualization Expanded',
SWAP_DETAILS_EXPANDED = 'Swap Details Expanded',
@ -88,6 +93,7 @@ export enum PageName {
NFT_COLLECTION_PAGE = 'nft-collection-page',
NFT_DETAILS_PAGE = 'nft-details-page',
NFT_EXPLORE_PAGE = 'nft-explore-page',
NFT_PROFILE_PAGE = 'nft-profile-page',
TOKEN_DETAILS_PAGE = 'token-details',
TOKENS_PAGE = 'tokens-page',
POOL_PAGE = 'pool-page',
@ -112,6 +118,7 @@ export enum SectionName {
/** Known modals for analytics purposes. */
export enum ModalName {
CONFIRM_SWAP = 'confirm-swap-modal',
NFT_LISTING = 'nft-listing-modal',
NFT_TX_COMPLETE = 'nft-tx-complete-modal',
TOKEN_SELECTOR = 'token-selector-modal',
// alphabetize additional modal names.

@ -1,5 +1,9 @@
import { addressesByNetwork, SupportedChainId } from '@looksrare/sdk'
import { useWeb3React } from '@web3-react/core'
import { sendAnalyticsEvent } from 'analytics'
import { EventName, ModalName } from 'analytics/constants'
import { Trace } from 'analytics/Trace'
import { useTrace } from 'analytics/Trace'
import { Box } from 'nft/components/Box'
import { Column, Row } from 'nft/components/Flex'
import { ChevronLeftIcon, XMarkIcon } from 'nft/components/icons'
@ -8,6 +12,7 @@ import { themeVars } from 'nft/css/sprinkles.css'
import { useBag, useIsMobile, useNFTList, useSellAsset } from 'nft/hooks'
import { logListing, looksRareNonceFetcher } from 'nft/queries'
import { AssetRow, CollectionRow, ListingRow, ListingStatus } from 'nft/types'
import { fetchPrice } from 'nft/utils/fetchPrice'
import { pluralize } from 'nft/utils/roundAndPluralize'
import { Dispatch, useEffect, useMemo, useRef, useState } from 'react'
@ -34,6 +39,7 @@ const ListingModal = () => {
const toggleCart = useBag((state) => state.toggleBag)
const looksRareNonceRef = useRef(looksRareNonce)
const isMobile = useIsMobile()
const trace = useTrace({ modal: ModalName.NFT_LISTING })
useEffect(() => {
useNFTList.subscribe((state) => (looksRareNonceRef.current = state.looksRareNonce))
@ -41,6 +47,29 @@ const ListingModal = () => {
const totalEthListingValue = useMemo(() => getTotalEthValue(sellAssets), [sellAssets])
const [ethPriceInUSD, setEthPriceInUSD] = useState(0)
useEffect(() => {
fetchPrice().then((price) => {
setEthPriceInUSD(price || 0)
})
}, [])
const startListingEventProperties = {
collection_addresses: sellAssets.map((asset) => asset.asset_contract.address),
token_ids: sellAssets.map((asset) => asset.tokenId),
marketplaces: Array.from(new Set(listings.map((asset) => asset.marketplace.name))),
list_quantity: listings.length,
usd_value: ethPriceInUSD * totalEthListingValue,
...trace,
}
const approvalEventProperties = {
list_quantity: listings.length,
usd_value: ethPriceInUSD * totalEthListingValue,
...trace,
}
// when all collections have been approved, auto start the signing process
useEffect(() => {
collectionsRequiringApproval?.length &&
@ -60,6 +89,7 @@ const ListingModal = () => {
const startListingFlow = async () => {
if (!signer) return
sendAnalyticsEvent(EventName.NFT_SELL_START_LISTING, { ...startListingEventProperties })
setListingStatus(ListingStatus.SIGNING)
const addresses = addressesByNetwork[SupportedChainId.MAINNET]
const signerAddress = await signer.getAddress()
@ -111,6 +141,11 @@ const ListingModal = () => {
} else if (!paused) {
setListingStatus(ListingStatus.FAILED)
}
sendAnalyticsEvent(EventName.NFT_LISTING_COMPLETED, {
signatures_requested: listings.length,
signatures_approved: listings.filter((asset) => asset.status === ListingStatus.APPROVED),
...approvalEventProperties,
})
await logListing(listings, (await signer?.getAddress()) ?? '')
}
@ -144,93 +179,100 @@ const ListingModal = () => {
const showSuccessScreen = useMemo(() => listingStatus === ListingStatus.APPROVED, [listingStatus])
return (
<Column paddingTop="20" paddingBottom="20" paddingLeft="12" paddingRight="12">
<Row className={headlineSmall} marginBottom="10">
{isMobile && !showSuccessScreen && (
<Box paddingTop="4" marginRight="4" onClick={toggleCart}>
<ChevronLeftIcon height={28} width={28} />
<Trace modal={ModalName.NFT_LISTING}>
<Column paddingTop="20" paddingBottom="20" paddingLeft="12" paddingRight="12">
<Row className={headlineSmall} marginBottom="10">
{isMobile && !showSuccessScreen && (
<Box paddingTop="4" marginRight="4" onClick={toggleCart}>
<ChevronLeftIcon height={28} width={28} />
</Box>
)}
{showSuccessScreen ? 'Success!' : `Listing ${sellAssets.length} NFTs`}
<Box
as="button"
border="none"
color="textSecondary"
backgroundColor="backgroundSurface"
marginLeft="auto"
marginRight="0"
paddingRight="0"
display={{ sm: 'flex', md: 'none' }}
cursor="pointer"
onClick={toggleCart}
>
<XMarkIcon height={28} width={28} fill={themeVars.colors.textPrimary} />
</Box>
)}
{showSuccessScreen ? 'Success!' : `Listing ${sellAssets.length} NFTs`}
<Box
as="button"
border="none"
color="textSecondary"
backgroundColor="backgroundSurface"
marginLeft="auto"
marginRight="0"
paddingRight="0"
display={{ sm: 'flex', md: 'none' }}
cursor="pointer"
onClick={toggleCart}
>
<XMarkIcon height={28} width={28} fill={themeVars.colors.textPrimary} />
</Box>
</Row>
<Column overflowX="hidden" overflowY="auto" style={{ maxHeight: '60vh' }}>
</Row>
<Column overflowX="hidden" overflowY="auto" style={{ maxHeight: '60vh' }}>
{showSuccessScreen ? (
<Trace
name={EventName.NFT_LISTING_COMPLETED}
properties={{ list_quantity: listings.length, usd_value: ethPriceInUSD * totalEthListingValue, ...trace }}
>
<ListingSection
sectionTitle={`Listed ${listings.length} item${pluralize(listings.length)} for sale`}
rows={listings}
index={0}
openIndex={openIndex}
isSuccessScreen={true}
/>
</Trace>
) : (
<>
<ListingSection
sectionTitle={`Approve ${collectionsRequiringApproval.length} collection${pluralize(
collectionsRequiringApproval.length
)}`}
title="COLLECTIONS"
rows={collectionsRequiringApproval}
index={1}
openIndex={openIndex}
/>
<ListingSection
sectionTitle={`Confirm ${listings.length} listing${pluralize(listings.length)}`}
caption="Now you can sign to list each item"
title="NFTS"
rows={listings}
index={2}
openIndex={openIndex}
/>
</>
)}
</Column>
<hr className={styles.sectionDivider} />
<Row className={subhead} marginTop="12" marginBottom={showSuccessScreen ? '8' : '20'}>
Return if sold
<Row className={subheadSmall} marginLeft="auto" marginRight="0">
{totalEthListingValue}
&nbsp;ETH
</Row>
</Row>
{showSuccessScreen ? (
<ListingSection
sectionTitle={`Listed ${listings.length} item${pluralize(listings.length)} for sale`}
rows={listings}
index={0}
openIndex={openIndex}
isSuccessScreen={true}
/>
<Box as="span" className={caption} color="textSecondary">
Status:{' '}
<Box as="span" color="green200">
Confirmed
</Box>
</Box>
) : (
<>
<ListingSection
sectionTitle={`Approve ${collectionsRequiringApproval.length} collection${pluralize(
collectionsRequiringApproval.length
)}`}
title="COLLECTIONS"
rows={collectionsRequiringApproval}
index={1}
openIndex={openIndex}
/>
<ListingSection
sectionTitle={`Confirm ${listings.length} listing${pluralize(listings.length)}`}
caption="Now you can sign to list each item"
title="NFTS"
rows={listings}
index={2}
openIndex={openIndex}
/>
</>
<ListingButton onClick={clickStartListingFlow} buttonText={'Start listing'} showWarningOverride={isMobile} />
)}
{(listingStatus === ListingStatus.PENDING || listingStatus === ListingStatus.SIGNING) && (
<Box
as="button"
border="none"
backgroundColor="backgroundSurface"
cursor="pointer"
color="orange"
className={styles.button}
onClick={clickStopListing}
type="button"
>
Stop listing
</Box>
)}
</Column>
<hr className={styles.sectionDivider} />
<Row className={subhead} marginTop="12" marginBottom={showSuccessScreen ? '8' : '20'}>
Return if sold
<Row className={subheadSmall} marginLeft="auto" marginRight="0">
{totalEthListingValue}
&nbsp;ETH
</Row>
</Row>
{showSuccessScreen ? (
<Box as="span" className={caption} color="textSecondary">
Status:{' '}
<Box as="span" color="green200">
Confirmed
</Box>
</Box>
) : (
<ListingButton onClick={clickStartListingFlow} buttonText={'Start listing'} showWarningOverride={isMobile} />
)}
{(listingStatus === ListingStatus.PENDING || listingStatus === ListingStatus.SIGNING) && (
<Box
as="button"
border="none"
backgroundColor="backgroundSurface"
cursor="pointer"
color="orange"
className={styles.button}
onClick={clickStopListing}
type="button"
>
Stop listing
</Box>
)}
</Column>
</Trace>
)
}

@ -1,3 +1,5 @@
import { Event, EventName } from 'analytics/constants'
import { TraceEvent } from 'analytics/TraceEvent'
import { useNftBalanceQuery } from 'graphql/data/nft/NftBalance'
import { AnimatedBox, Box } from 'nft/components/Box'
import { assetList } from 'nft/components/collection/CollectionNfts.css'
@ -137,10 +139,16 @@ export const ProfilePage = () => {
/>
<Row gap="8" flexWrap="nowrap">
{isSellMode && <SelectAllButton ownerAssets={ownerAssets ?? []} />}
<SellModeButton className={buttonTextMedium} active={isSellMode} onClick={handleSellModeClick}>
<TagIcon height={20} width={20} />
Sell
</SellModeButton>
<TraceEvent
events={[Event.onClick]}
name={EventName.NFT_SELL_SELECTED}
shouldLogImpression={!isSellMode}
>
<SellModeButton className={buttonTextMedium} active={isSellMode} onClick={handleSellModeClick}>
<TagIcon height={20} width={20} />
Sell
</SellModeButton>
</TraceEvent>
</Row>
</Row>
<Row>

@ -1,3 +1,5 @@
import { sendAnalyticsEvent } from 'analytics'
import { EventName } from 'analytics/constants'
import { Box } from 'nft/components/Box'
import { Column, Row } from 'nft/components/Flex'
import { VerifiedIcon } from 'nft/components/icons'
@ -33,6 +35,11 @@ export const WalletAssetDisplay = ({ asset, isSellMode }: { asset: WalletAsset;
const handleSelect = () => {
isSelected ? removeSellAsset(asset) : selectSellAsset(asset)
!isSelected &&
sendAnalyticsEvent(EventName.NFT_SELL_ITEM_ADDED, {
collection_address: asset.asset_contract.address,
token_id: asset.tokenId,
})
if (
!cartExpanded &&
!sellAssets.find(

@ -1,4 +1,6 @@
import { useWeb3React } from '@web3-react/core'
import { PageName } from 'analytics/constants'
import { Trace } from 'analytics/Trace'
import { Box } from 'nft/components/Box'
import { Center, Column, Row } from 'nft/components/Flex'
import { ChevronLeftIcon, XMarkIcon } from 'nft/components/icons'
@ -45,42 +47,44 @@ const Profile = () => {
}
return (
<Box className={styles.mobileSellWrapper}>
{/* <Head> TODO: figure out metadata tagging
<Trace page={PageName.NFT_PROFILE_PAGE} shouldLogImpression>
<Box className={styles.mobileSellWrapper}>
{/* <Head> TODO: figure out metadata tagging
<title>Genie | Sell</title>
</Head> */}
<Row className={styles.mobileSellHeader}>
{sellPageState === ProfilePageStateType.LISTING && (
<Box marginRight="4" onClick={() => setSellPageState(ProfilePageStateType.VIEWING)}>
<ChevronLeftIcon height={28} width={28} />
<Row className={styles.mobileSellHeader}>
{sellPageState === ProfilePageStateType.LISTING && (
<Box marginRight="4" onClick={() => setSellPageState(ProfilePageStateType.VIEWING)}>
<ChevronLeftIcon height={28} width={28} />
</Box>
)}
<Box className={headlineSmall} paddingBottom="4" style={{ lineHeight: '28px' }}>
{sellPageState === ProfilePageStateType.VIEWING ? 'Select NFTs' : 'Create Listing'}
</Box>
<Box cursor="pointer" marginLeft="auto" marginRight="0" onClick={exitSellFlow}>
<XMarkIcon height={28} width={28} fill={themeVars.colors.textPrimary} />
</Box>
</Row>
{account != null ? (
<Box style={{ width: `calc(100% - ${cartExpanded ? SHOPPING_BAG_WIDTH : 0}px)` }}>
{sellPageState === ProfilePageStateType.VIEWING ? <ProfilePage /> : <ListPage />}
</Box>
) : (
<Column as="section" gap="60" className={styles.section}>
<div style={{ minHeight: '70vh' }}>
<Center className={styles.notConnected} flexDirection="column">
<Box as="span" className={headlineMedium} color="textSecondary" marginBottom="24" display="block">
No items to display
</Box>
<Box as="button" className={buttonMedium} onClick={toggleWalletModal}>
Connect Wallet
</Box>
</Center>
</div>
</Column>
)}
<Box className={headlineSmall} paddingBottom="4" style={{ lineHeight: '28px' }}>
{sellPageState === ProfilePageStateType.VIEWING ? 'Select NFTs' : 'Create Listing'}
</Box>
<Box cursor="pointer" marginLeft="auto" marginRight="0" onClick={exitSellFlow}>
<XMarkIcon height={28} width={28} fill={themeVars.colors.textPrimary} />
</Box>
</Row>
{account != null ? (
<Box style={{ width: `calc(100% - ${cartExpanded ? SHOPPING_BAG_WIDTH : 0}px)` }}>
{sellPageState === ProfilePageStateType.VIEWING ? <ProfilePage /> : <ListPage />}
</Box>
) : (
<Column as="section" gap="60" className={styles.section}>
<div style={{ minHeight: '70vh' }}>
<Center className={styles.notConnected} flexDirection="column">
<Box as="span" className={headlineMedium} color="textSecondary" marginBottom="24" display="block">
No items to display
</Box>
<Box as="button" className={buttonMedium} onClick={toggleWalletModal}>
Connect Wallet
</Box>
</Center>
</div>
</Column>
)}
</Box>
</Box>
</Trace>
)
}