From e339afce7a421ea9142660dedffc10c804de4128 Mon Sep 17 00:00:00 2001 From: David Walsh Date: Thu, 27 Apr 2023 09:28:08 -0500 Subject: [PATCH] UX: Multichain: Analytics (#18674) --- shared/constants/metametrics.ts | 13 ++++++ test/e2e/tests/portfolio-site.spec.js | 2 +- .../detected-tokens-link.js | 10 ++++- ...onfirm-page-container-content.component.js | 10 ++++- .../confirm-page-container.component.js | 4 ++ .../detected-token-selection-popover.js | 10 ++++- .../app/detected-token/detected-token.js | 10 ++++- .../app/wallet-overview/eth-overview.js | 28 +++++++++--- .../app/wallet-overview/token-overview.js | 26 ++++++++--- .../wallet-overview/token-overview.test.js | 2 +- .../app/whats-new-popup/whats-new-popup.js | 40 +++++++++++++++-- .../account-list-item-menu.js | 38 ++++++++++++++-- .../account-list-item/account-list-item.js | 16 ++++++- .../multichain/app-header/app-header.js | 43 ++++++++++++++++--- .../detected-token-banner.js | 10 ++++- .../multichain/global-menu/global-menu.js | 24 ++++++++--- .../global-menu/global-menu.test.js | 2 +- .../multichain-token-list-item.js | 21 ++++++++- .../network-list-menu/network-list-menu.js | 33 +++++++++++++- ui/helpers/utils/metrics.js | 8 ++++ ui/helpers/utils/portfolio.js | 8 ++++ ui/pages/add-nft/add-nft.js | 2 +- .../confirm-add-suggested-token.js | 2 +- .../confirm-import-token.js | 2 +- .../import-account/import-account.js | 6 ++- .../create-account/new-account.component.js | 1 + ui/pages/home/home.component.js | 19 +++++++- .../networks-form/networks-form.js | 30 ++++++++++++- ui/selectors/selectors.js | 16 ++++++- 29 files changed, 379 insertions(+), 57 deletions(-) create mode 100644 ui/helpers/utils/portfolio.js diff --git a/shared/constants/metametrics.ts b/shared/constants/metametrics.ts index 24af084f8..1c8ea0140 100644 --- a/shared/constants/metametrics.ts +++ b/shared/constants/metametrics.ts @@ -458,6 +458,7 @@ export enum MetaMetricsEventName { AppInstalled = 'App Installed', AppUnlocked = 'App Unlocked', AppUnlockedFailed = 'App Unlocked Failed', + AppLocked = 'App Locked', AppWindowExpanded = 'App Window Expanded', BridgeLinkClicked = 'Bridge Link Clicked', DecryptionApproved = 'Decryption Approved', @@ -541,6 +542,17 @@ export enum MetaMetricsEventName { ///: BEGIN:ONLY_INCLUDE_IN(build-mmi) UserClickedDeepLink = 'User clicked deeplink', ///: END:ONLY_INCLUDE_IN + AccountDetailMenuOpened = 'Account Details Menu Opened', + BlockExplorerLinkClicked = 'Block Explorer Clicked', + AccountRemoved = 'Account Removed', + TestNetworksDisplayed = 'Test Networks Displayed', + AddNetworkButtonClick = 'Add Network Button Clicked', + CustomNetworkAdded = 'Custom Network Added', + TokenDetailsOpened = 'Token Details Opened', + NftScreenOpened = 'NFT Screen Opened', + ActivityScreenOpened = 'Activity Screen Opened', + WhatsNewViewed = `What's New Viewed`, + WhatsNewClicked = `What's New Link Clicked`, } export enum MetaMetricsEventAccountType { @@ -583,6 +595,7 @@ export enum MetaMetricsEventCategory { ///: BEGIN:ONLY_INCLUDE_IN(build-mmi) MMI = 'Institutional', ///: END:ONLY_INCLUDE_IN + Tokens = 'Tokens', } export enum MetaMetricsEventLinkType { diff --git a/test/e2e/tests/portfolio-site.spec.js b/test/e2e/tests/portfolio-site.spec.js index 06b47ce4b..2bdb413d5 100644 --- a/test/e2e/tests/portfolio-site.spec.js +++ b/test/e2e/tests/portfolio-site.spec.js @@ -34,7 +34,7 @@ describe('Portfolio site', function () { // Verify site assert.equal( await driver.getCurrentUrl(), - 'http://127.0.0.1:8080/?metamaskEntry=ext', + 'http://127.0.0.1:8080/?metamaskEntry=ext&metametricsId=null', ); }, ); diff --git a/ui/components/app/asset-list/detected-tokens-link/detected-tokens-link.js b/ui/components/app/asset-list/detected-tokens-link/detected-tokens-link.js index 9c9ef6230..e550e881a 100644 --- a/ui/components/app/asset-list/detected-tokens-link/detected-tokens-link.js +++ b/ui/components/app/asset-list/detected-tokens-link/detected-tokens-link.js @@ -6,7 +6,10 @@ import classNames from 'classnames'; import Box from '../../../ui/box/box'; import Button from '../../../ui/button'; import { useI18nContext } from '../../../../hooks/useI18nContext'; -import { getDetectedTokensInCurrentNetwork } from '../../../../selectors'; +import { + getCurrentChainId, + getDetectedTokensInCurrentNetwork, +} from '../../../../selectors'; import { MetaMetricsContext } from '../../../../contexts/metametrics'; import { MetaMetricsEventCategory, @@ -23,13 +26,16 @@ const DetectedTokensLink = ({ className = '', setShowDetectedTokens }) => { ({ address, symbol }) => `${symbol} - ${address}`, ); + const chainId = useSelector(getCurrentChainId); + const onClick = () => { setShowDetectedTokens(true); trackEvent({ event: MetaMetricsEventName.TokenImportClicked, category: MetaMetricsEventCategory.Wallet, properties: { - source: MetaMetricsTokenEventSource.Detected, + source_connection_method: MetaMetricsTokenEventSource.Detected, + chain_id: chainId, tokens: detectedTokensDetails, }, }); diff --git a/ui/components/app/confirm-page-container/confirm-page-container-content/confirm-page-container-content.component.js b/ui/components/app/confirm-page-container/confirm-page-container-content/confirm-page-container-content.component.js index 75e766274..1296d4eb4 100644 --- a/ui/components/app/confirm-page-container/confirm-page-container-content/confirm-page-container-content.component.js +++ b/ui/components/app/confirm-page-container/confirm-page-container-content/confirm-page-container-content.component.js @@ -12,6 +12,7 @@ import { TypographyVariant } from '../../../../helpers/constants/design-system'; import SecurityProviderBannerMessage from '../../security-provider-banner-message/security-provider-banner-message'; import { SECURITY_PROVIDER_MESSAGE_SEVERITIES } from '../../security-provider-banner-message/security-provider-banner-message.constants'; +import { getPortfolioUrl } from '../../../../helpers/utils/portfolio'; import { ConfirmPageContainerSummary, ConfirmPageContainerWarning } from '.'; export default class ConfirmPageContainerContent extends Component { @@ -54,6 +55,7 @@ export default class ConfirmPageContainerContent extends Component { transactionType: PropTypes.string, isBuyableChain: PropTypes.bool, txData: PropTypes.object, + metaMetricsId: PropTypes.string, }; renderContent() { @@ -159,6 +161,7 @@ export default class ConfirmPageContainerContent extends Component { transactionType, isBuyableChain, txData, + metaMetricsId, } = this.props; const { t } = this.context; @@ -222,9 +225,12 @@ export default class ConfirmPageContainerContent extends Component { type="inline" className="confirm-page-container-content__link" onClick={() => { - const portfolioUrl = process.env.PORTFOLIO_URL; global.platform.openTab({ - url: `${portfolioUrl}/buy?metamaskEntry=ext_buy_button`, + url: getPortfolioUrl( + 'buy', + 'ext_buy_button', + metaMetricsId, + ), }); }} key={`${nativeCurrency}-buy-button`} diff --git a/ui/components/app/confirm-page-container/confirm-page-container.component.js b/ui/components/app/confirm-page-container/confirm-page-container.component.js index 205d9dd65..eadc1ce1d 100644 --- a/ui/components/app/confirm-page-container/confirm-page-container.component.js +++ b/ui/components/app/confirm-page-container/confirm-page-container.component.js @@ -39,6 +39,7 @@ import { getIsBuyableChain, getMetadataContractName, getMetaMaskIdentities, + getMetaMetricsId, getNetworkIdentifier, getSwapsDefaultToken, } from '../../../selectors'; @@ -116,6 +117,8 @@ const ConfirmPageContainer = (props) => { getMetadataContractName(state, toAddress), ); + const metaMetricsId = useSelector(getMetaMetricsId); + // TODO: Move useRamps hook to the confirm-transaction-base parent component. // TODO: openBuyCryptoInPdapp should be passed to this component as a custom prop. // We try to keep this component for layout purpose only, we need to move this hook to the confirm-transaction-base parent @@ -198,6 +201,7 @@ const ConfirmPageContainer = (props) => { )} {contentComponent || ( { const dispatch = useDispatch(); const trackEvent = useContext(MetaMetricsContext); + const chainId = useSelector(getCurrentChainId); const detectedTokens = useSelector(getDetectedTokensInCurrentNetwork); const [tokensListDetected, setTokensListDetected] = useState(() => @@ -69,9 +73,11 @@ const DetectedToken = ({ setShowDetectedTokens }) => { token_symbol: importedToken.symbol, token_contract_address: importedToken.address, token_decimal_precision: importedToken.decimals, - source: MetaMetricsTokenEventSource.Detected, + source_connection_method: MetaMetricsTokenEventSource.Detected, token_standard: TokenStandard.ERC20, asset_type: AssetType.token, + token_added_type: 'detected', + chain_id: chainId, }, }); }); diff --git a/ui/components/app/wallet-overview/eth-overview.js b/ui/components/app/wallet-overview/eth-overview.js index 6b4fb3d32..09d3a936f 100644 --- a/ui/components/app/wallet-overview/eth-overview.js +++ b/ui/components/app/wallet-overview/eth-overview.js @@ -30,6 +30,8 @@ import { getIsBuyableChain, getNativeCurrencyImage, getSelectedAccountCachedBalance, + getCurrentChainId, + getMetaMetricsId, } from '../../../selectors'; import { setSwapsFromToken } from '../../../ducks/swaps/swaps'; import IconButton from '../../ui/icon-button'; @@ -52,6 +54,7 @@ import { } from '../../component-library'; import { IconColor } from '../../../helpers/constants/design-system'; import useRamps from '../../../hooks/experiences/useRamps'; +import { getPortfolioUrl } from '../../../helpers/utils/portfolio'; import WalletOverview from './wallet-overview'; const EthOverview = ({ className }) => { @@ -70,6 +73,8 @@ const EthOverview = ({ className }) => { const isBuyableChain = useSelector(getIsBuyableChain); const primaryTokenImage = useSelector(getNativeCurrencyImage); const defaultSwapsToken = useSelector(getSwapsDefaultToken); + const chainId = useSelector(getCurrentChainId); + const metaMetricsId = useSelector(getMetaMetricsId); ///: BEGIN:ONLY_INCLUDE_IN(build-mmi) const mmiPortfolioEnabled = useSelector(getMmiPortfolioEnabled); @@ -166,9 +171,13 @@ const EthOverview = ({ className }) => { ariaLabel={t('portfolio')} size={ButtonIconSize.Lg} onClick={() => { - const portfolioUrl = process.env.PORTFOLIO_URL; + const portfolioUrl = getPortfolioUrl( + '', + 'ext', + metaMetricsId, + ); global.platform.openTab({ - url: `${portfolioUrl}?metamaskEntry=ext`, + url: portfolioUrl, }); trackEvent( { @@ -226,6 +235,8 @@ const EthOverview = ({ className }) => { properties: { location: 'Home', text: 'Buy', + chain_id: chainId, + token_symbol: defaultSwapsToken, }, }); }} @@ -257,6 +268,7 @@ const EthOverview = ({ className }) => { token_symbol: 'ETH', location: 'Home', text: 'Send', + chain_id: chainId, }, }); dispatch( @@ -284,6 +296,7 @@ const EthOverview = ({ className }) => { token_symbol: 'ETH', location: MetaMetricsSwapsEventSource.MainView, text: 'Swap', + chain_id: chainId, }, }); dispatch(setSwapsFromToken(defaultSwapsToken)); @@ -320,10 +333,13 @@ const EthOverview = ({ className }) => { label={t('bridge')} onClick={() => { if (isBridgeChain) { - const portfolioUrl = process.env.PORTFOLIO_URL; - const bridgeUrl = `${portfolioUrl}/bridge`; + const portfolioUrl = getPortfolioUrl( + 'bridge', + 'ext_bridge_button', + metaMetricsId, + ); global.platform.openTab({ - url: `${bridgeUrl}?metamaskEntry=ext_bridge_button${ + url: `${portfolioUrl}${ location.pathname.includes('asset') ? '&token=native' : '' }`, }); @@ -333,6 +349,8 @@ const EthOverview = ({ className }) => { properties: { location: 'Home', text: 'Bridge', + chain_id: chainId, + token_symbol: 'ETH', }, }); } diff --git a/ui/components/app/wallet-overview/token-overview.js b/ui/components/app/wallet-overview/token-overview.js index ed99cabee..fff89e457 100644 --- a/ui/components/app/wallet-overview/token-overview.js +++ b/ui/components/app/wallet-overview/token-overview.js @@ -20,6 +20,8 @@ import { getIsSwapsChain, getIsBuyableChain, getIsBridgeToken, + getCurrentChainId, + getMetaMetricsId, } from '../../../selectors'; import IconButton from '../../ui/icon-button'; @@ -39,6 +41,7 @@ import { ButtonIcon, Icon, IconName } from '../../component-library'; import { IconColor } from '../../../helpers/constants/design-system'; import { BUTTON_ICON_SIZES } from '../../component-library/button-icon/deprecated'; +import { getPortfolioUrl } from '../../../helpers/utils/portfolio'; import WalletOverview from './wallet-overview'; const TokenOverview = ({ className, token }) => { @@ -56,9 +59,11 @@ const TokenOverview = ({ className, token }) => { balanceToRender, token.symbol, ); + const chainId = useSelector(getCurrentChainId); const isSwapsChain = useSelector(getIsSwapsChain); const isBridgeToken = useSelector(getIsBridgeToken(token.address)); const isBuyableChain = useSelector(getIsBuyableChain); + const metaMetricsId = useSelector(getMetaMetricsId); const { openBuyCryptoInPdapp } = useRamps(); @@ -92,9 +97,9 @@ const TokenOverview = ({ className, token }) => { ariaLabel={t('portfolio')} size={BUTTON_ICON_SIZES.LG} onClick={() => { - const portfolioUrl = process.env.PORTFOLIO_URL; + const portfolioUrl = getPortfolioUrl('', 'ext', metaMetricsId); global.platform.openTab({ - url: `${portfolioUrl}?metamaskEntry=ext`, + url: portfolioUrl, }); trackEvent( { @@ -137,6 +142,8 @@ const TokenOverview = ({ className, token }) => { properties: { location: 'Token Overview', text: 'Buy', + chain_id: chainId, + token_symbol: token.symbol, }, }); }} @@ -152,6 +159,7 @@ const TokenOverview = ({ className, token }) => { token_symbol: token.symbol, location: MetaMetricsSwapsEventSource.TokenView, text: 'Send', + chain_id: chainId, }, }); try { @@ -195,6 +203,7 @@ const TokenOverview = ({ className, token }) => { token_symbol: token.symbol, location: MetaMetricsSwapsEventSource.TokenView, text: 'Swap', + chain_id: chainId, }, }); dispatch( @@ -225,11 +234,13 @@ const TokenOverview = ({ className, token }) => { } label={t('bridge')} onClick={() => { - const portfolioUrl = process.env.PORTFOLIO_URL; - - const bridgeUrl = `${portfolioUrl}/bridge`; + const portfolioUrl = getPortfolioUrl( + 'bridge', + 'ext_bridge_button', + metaMetricsId, + ); global.platform.openTab({ - url: `${bridgeUrl}?metamaskEntry=ext_bridge_button&token=${token.address}`, + url: `${portfolioUrl}&token=${token.address}`, }); trackEvent({ category: MetaMetricsEventCategory.Navigation, @@ -237,6 +248,9 @@ const TokenOverview = ({ className, token }) => { properties: { location: 'Token Overview', text: 'Bridge', + url: portfolioUrl, + chain_id: chainId, + token_symbol: token.symbol, }, }); }} diff --git a/ui/components/app/wallet-overview/token-overview.test.js b/ui/components/app/wallet-overview/token-overview.test.js index 707b02921..0368e5f4c 100644 --- a/ui/components/app/wallet-overview/token-overview.test.js +++ b/ui/components/app/wallet-overview/token-overview.test.js @@ -307,7 +307,7 @@ describe('TokenOverview', () => { await waitFor(() => expect(openTabSpy).toHaveBeenCalledWith({ url: expect.stringContaining( - '/bridge?metamaskEntry=ext_bridge_button&token=0x7ceb23fd6bc0add59e62ac25578270cff1B9f619', + '/bridge?metamaskEntry=ext_bridge_button&metametricsId=&token=0x7ceb23fd6bc0add59e62ac25578270cff1B9f619', ), }), ); diff --git a/ui/components/app/whats-new-popup/whats-new-popup.js b/ui/components/app/whats-new-popup/whats-new-popup.js index fd0bf14a7..12f81a60c 100644 --- a/ui/components/app/whats-new-popup/whats-new-popup.js +++ b/ui/components/app/whats-new-popup/whats-new-popup.js @@ -20,6 +20,11 @@ import { } from '../../../helpers/constants/routes'; import { TextVariant } from '../../../helpers/constants/design-system'; import ZENDESK_URLS from '../../../helpers/constants/zendesk-url'; +import { MetaMetricsContext } from '../../../contexts/metametrics'; +import { + MetaMetricsEventCategory, + MetaMetricsEventName, +} from '../../../../shared/constants/metametrics'; function getActionFunctionById(id, history) { const actionFunctions = { @@ -107,9 +112,16 @@ const renderDescription = (description) => { ); }; -const renderFirstNotification = (notification, idRefMap, history, isLast) => { +const renderFirstNotification = ( + notification, + idRefMap, + history, + isLast, + trackEvent, +) => { const { id, date, title, description, image, actionText } = notification; const actionFunction = getActionFunctionById(id, history); + const imageComponent = image && ( { @@ -218,6 +236,8 @@ export default function WhatsNewPopup({ onClose }) { [memoizedNotifications], ); + const trackEvent = useContext(MetaMetricsContext); + const handleScrollDownClick = (e) => { e.stopPropagation(); idRefMap[notifications[notifications.length - 1].id].current.scrollIntoView( @@ -267,6 +287,14 @@ export default function WhatsNewPopup({ onClose }) { className="whats-new-popup__popover" onClose={() => { updateViewedNotifications(seenNotifications); + trackEvent({ + category: MetaMetricsEventCategory.Home, + event: MetaMetricsEventName.WhatsNewViewed, + properties: { + number_viewed: Object.keys(seenNotifications).pop(), + completed_all: true, + }, + }); onClose(); }} popoverRef={popoverRef} @@ -280,7 +308,13 @@ export default function WhatsNewPopup({ onClose }) { // Display the swaps notification with full image // Displays the NFTs & OpenSea notifications 18,19 with full image return index === 0 || id === 1 || id === 18 || id === 19 - ? renderFirstNotification(notification, idRefMap, history, isLast) + ? renderFirstNotification( + notification, + idRefMap, + history, + isLast, + trackEvent, + ) : renderSubsequentNotification( notification, idRefMap, diff --git a/ui/components/multichain/account-list-item-menu/account-list-item-menu.js b/ui/components/multichain/account-list-item-menu/account-list-item-menu.js index df3cece9a..cf480dc26 100644 --- a/ui/components/multichain/account-list-item-menu/account-list-item-menu.js +++ b/ui/components/multichain/account-list-item-menu/account-list-item-menu.js @@ -9,7 +9,10 @@ import { getRpcPrefsForCurrentProvider, getBlockExplorerLinkText, getCurrentChainId, + getHardwareWalletType, + getAccountTypeForKeyring, } from '../../../selectors'; +import { findKeyringForAddress } from '../../../ducks/metamask/metamask'; import { NETWORKS_ROUTE } from '../../../helpers/constants/routes'; import { Menu, MenuItem } from '../../ui/menu'; import { Text, IconName } from '../../component-library'; @@ -21,6 +24,7 @@ import { import { getURLHostName } from '../../../helpers/utils/util'; import { showModal } from '../../../store/actions'; import { TextVariant } from '../../../helpers/constants/design-system'; +import { formatAccountType } from '../../../helpers/utils/metrics'; export const AccountListItemMenu = ({ anchorElement, @@ -39,6 +43,13 @@ export const AccountListItemMenu = ({ const rpcPrefs = useSelector(getRpcPrefsForCurrentProvider); const addressLink = getAccountLink(identity.address, chainId, rpcPrefs); + const deviceName = useSelector(getHardwareWalletType); + + const keyring = useSelector((state) => + findKeyringForAddress(state, identity.address), + ); + const accountType = formatAccountType(getAccountTypeForKeyring(keyring)); + const blockExplorerLinkText = useSelector(getBlockExplorerLinkText); const openBlockExplorer = () => { trackEvent({ @@ -50,6 +61,7 @@ export const AccountListItemMenu = ({ url_domain: getURLHostName(addressLink), }, }); + global.platform.openTab({ url: addressLink, }); @@ -67,11 +79,20 @@ export const AccountListItemMenu = ({ onHide={onClose} > { blockExplorerLinkText.firstPart === 'addBlockExplorer' - ? routeToAddBlockExplorerUrl - : openBlockExplorer - } + ? routeToAddBlockExplorerUrl() + : openBlockExplorer(); + + trackEvent({ + event: MetaMetricsEventName.BlockExplorerLinkClicked, + category: MetaMetricsEventCategory.Accounts, + properties: { + location: 'Account Options', + chain_id: chainId, + }, + }); + }} subtitle={blockExplorerUrlSubTitle || null} iconName={IconName.Export} data-testid="account-list-menu-open-explorer" @@ -105,6 +126,15 @@ export const AccountListItemMenu = ({ identity, }), ); + trackEvent({ + event: MetaMetricsEventName.AccountRemoved, + category: MetaMetricsEventCategory.Accounts, + properties: { + account_hardware_type: deviceName, + chain_id: chainId, + account_type: accountType, + }, + }); onClose(); }} iconName={IconName.Trash} diff --git a/ui/components/multichain/account-list-item/account-list-item.js b/ui/components/multichain/account-list-item/account-list-item.js index 5f94ddd5a..135d9397f 100644 --- a/ui/components/multichain/account-list-item/account-list-item.js +++ b/ui/components/multichain/account-list-item/account-list-item.js @@ -1,4 +1,4 @@ -import React, { useState, useRef } from 'react'; +import React, { useState, useRef, useContext } from 'react'; import PropTypes from 'prop-types'; import classnames from 'classnames'; @@ -38,6 +38,11 @@ import UserPreferencedCurrencyDisplay from '../../app/user-preferenced-currency- import { SECONDARY, PRIMARY } from '../../../helpers/constants/common'; import { findKeyringForAddress } from '../../../ducks/metamask/metamask'; import Tooltip from '../../ui/tooltip/tooltip'; +import { + MetaMetricsEventCategory, + MetaMetricsEventName, +} from '../../../../shared/constants/metametrics'; +import { MetaMetricsContext } from '../../../contexts/metametrics'; const MAXIMUM_CURRENCY_DECIMALS = 3; const MAXIMUM_CHARACTERS_WITHOUT_TOOLTIP = 17; @@ -82,6 +87,8 @@ export const AccountListItem = ({ const { blockExplorerUrl } = rpcPrefs; const blockExplorerUrlSubTitle = getURLHostName(blockExplorerUrl); + const trackEvent = useContext(MetaMetricsContext); + return ( { e.stopPropagation(); + trackEvent({ + event: MetaMetricsEventName.AccountDetailMenuOpened, + category: MetaMetricsEventCategory.Navigation, + properties: { + location: 'Account Options', + }, + }); setAccountOptionsMenuOpen(true); }} data-testid="account-list-item-menu-button" diff --git a/ui/components/multichain/app-header/app-header.js b/ui/components/multichain/app-header/app-header.js index 70a7631e8..6c19f9094 100644 --- a/ui/components/multichain/app-header/app-header.js +++ b/ui/components/multichain/app-header/app-header.js @@ -1,4 +1,4 @@ -import React, { useContext, useState, useRef } from 'react'; +import React, { useContext, useState, useRef, useCallback } from 'react'; import classnames from 'classnames'; import PropTypes from 'prop-types'; import browser from 'webextension-polyfill'; @@ -30,6 +30,7 @@ import { } from '../../component-library'; import { + getCurrentChainId, getCurrentNetwork, getOnboardedInThisUISession, getOriginOfCurrentTab, @@ -60,6 +61,7 @@ export const AppHeader = ({ onClick }) => { const history = useHistory(); const isUnlocked = useSelector((state) => state.metamask.isUnlocked); const t = useI18nContext(); + const chainId = useSelector(getCurrentChainId); // Used for account picker const identity = useSelector(getSelectedIdentity); @@ -71,7 +73,7 @@ export const AppHeader = ({ onClick }) => { // Used for network icon / dropdown const currentNetwork = useSelector(getCurrentNetwork); - // used to get the environment and connection status + // Used to get the environment and connection status const popupStatus = getEnvironmentType() === ENVIRONMENT_TYPE_POPUP; const showStatus = getEnvironmentType() === ENVIRONMENT_TYPE_POPUP && @@ -83,6 +85,19 @@ export const AppHeader = ({ onClick }) => { .querySelector('[dir]') ?.getAttribute('dir'); + // Callback for network dropdown + const networkOpenCallback = useCallback(() => { + dispatch(toggleNetworkMenu()); + trackEvent({ + event: MetaMetricsEventName.NavNetworkMenuOpened, + category: MetaMetricsEventCategory.Navigation, + properties: { + location: 'App header', + chain_id: chainId, + }, + }); + }, [chainId, dispatch, trackEvent]); + return ( <> {isUnlocked && !popupStatus ? ( @@ -140,14 +155,14 @@ export const AppHeader = ({ onClick }) => { name={currentNetwork?.nickname} src={currentNetwork?.rpcPrefs?.imageUrl} size={Size.SM} - onClick={() => dispatch(toggleNetworkMenu())} + onClick={networkOpenCallback} display={[DISPLAY.FLEX, DISPLAY.NONE]} // show on popover hide on desktop /> dispatch(toggleNetworkMenu())} + onClick={networkOpenCallback} display={[DISPLAY.NONE, DISPLAY.FLEX]} // show on desktop hide on popover /> {showProductTour && @@ -171,7 +186,17 @@ export const AppHeader = ({ onClick }) => { dispatch(toggleAccountMenu())} + onClick={() => { + dispatch(toggleAccountMenu()); + + trackEvent({ + event: MetaMetricsEventName.NavAccountMenuOpened, + category: MetaMetricsEventCategory.Navigation, + properties: { + location: 'Home', + }, + }); + }} /> { {showStatus ? ( history.push(CONNECTED_ACCOUNTS_ROUTE)} + onClick={() => { + history.push(CONNECTED_ACCOUNTS_ROUTE); + trackEvent({ + event: MetaMetricsEventName.NavConnectedSitesOpened, + category: MetaMetricsEventCategory.Navigation, + }); + }} /> ) : null}{' '} diff --git a/ui/components/multichain/detected-token-banner/detected-token-banner.js b/ui/components/multichain/detected-token-banner/detected-token-banner.js index 298f7196a..8851614ec 100644 --- a/ui/components/multichain/detected-token-banner/detected-token-banner.js +++ b/ui/components/multichain/detected-token-banner/detected-token-banner.js @@ -4,7 +4,10 @@ import PropTypes from 'prop-types'; import classNames from 'classnames'; import { useI18nContext } from '../../../hooks/useI18nContext'; -import { getDetectedTokensInCurrentNetwork } from '../../../selectors'; +import { + getCurrentChainId, + getDetectedTokensInCurrentNetwork, +} from '../../../selectors'; import { MetaMetricsContext } from '../../../contexts/metametrics'; import { MetaMetricsEventCategory, @@ -26,14 +29,17 @@ export const DetectedTokensBanner = ({ ({ address, symbol }) => `${symbol} - ${address}`, ); + const chainId = useSelector(getCurrentChainId); + const handleOnClick = () => { actionButtonOnClick(); trackEvent({ event: MetaMetricsEventName.TokenImportClicked, category: MetaMetricsEventCategory.Wallet, properties: { - source: MetaMetricsTokenEventSource.Detected, + source_connection_method: MetaMetricsTokenEventSource.Detected, tokens: detectedTokensDetails, + chain_id: chainId, }, }); }; diff --git a/ui/components/multichain/global-menu/global-menu.js b/ui/components/multichain/global-menu/global-menu.js index 2d89cf688..8b69c1df5 100644 --- a/ui/components/multichain/global-menu/global-menu.js +++ b/ui/components/multichain/global-menu/global-menu.js @@ -1,7 +1,7 @@ import React, { useContext } from 'react'; import PropTypes from 'prop-types'; import { useHistory } from 'react-router-dom'; -import { useDispatch } from 'react-redux'; +import { useDispatch, useSelector } from 'react-redux'; import { CONNECTED_ROUTE, SETTINGS_ROUTE, @@ -21,12 +21,15 @@ import { MetaMetricsEventCategory, MetaMetricsContextProp, } from '../../../../shared/constants/metametrics'; +import { getPortfolioUrl } from '../../../helpers/utils/portfolio'; +import { getMetaMetricsId } from '../../../selectors'; export const GlobalMenu = ({ closeMenu, anchorElement }) => { const t = useI18nContext(); const dispatch = useDispatch(); const trackEvent = useContext(MetaMetricsContext); const history = useHistory(); + const metaMetricsId = useSelector(getMetaMetricsId); return ( @@ -38,7 +41,7 @@ export const GlobalMenu = ({ closeMenu, anchorElement }) => { event: MetaMetricsEventName.NavConnectedSitesOpened, category: MetaMetricsEventCategory.Navigation, properties: { - location: 'Account Options', + location: 'Global Menu', }, }); closeMenu(); @@ -49,9 +52,9 @@ export const GlobalMenu = ({ closeMenu, anchorElement }) => { { - const portfolioUrl = process.env.PORTFOLIO_URL; + const portfolioUrl = getPortfolioUrl('', 'ext', metaMetricsId); global.platform.openTab({ - url: `${portfolioUrl}?metamaskEntry=ext`, + url: portfolioUrl, }); trackEvent( { @@ -59,6 +62,7 @@ export const GlobalMenu = ({ closeMenu, anchorElement }) => { event: MetaMetricsEventName.PortfolioLinkClicked, properties: { url: portfolioUrl, + location: 'Global Menu', }, }, { @@ -82,7 +86,7 @@ export const GlobalMenu = ({ closeMenu, anchorElement }) => { event: MetaMetricsEventName.AppWindowExpanded, category: MetaMetricsEventCategory.Navigation, properties: { - location: 'Account Options', + location: 'Global Menu', }, }); closeMenu(); @@ -102,6 +106,7 @@ export const GlobalMenu = ({ closeMenu, anchorElement }) => { event: MetaMetricsEventName.SupportLinkClicked, properties: { url: SUPPORT_LINK, + location: 'Global Menu', }, }, { @@ -124,7 +129,7 @@ export const GlobalMenu = ({ closeMenu, anchorElement }) => { category: MetaMetricsEventCategory.Navigation, event: MetaMetricsEventName.NavSettingsOpened, properties: { - location: 'Main Menu', + location: 'Global Menu', }, }); closeMenu(); @@ -137,6 +142,13 @@ export const GlobalMenu = ({ closeMenu, anchorElement }) => { onClick={() => { dispatch(lockMetamask()); history.push(DEFAULT_ROUTE); + trackEvent({ + category: MetaMetricsEventCategory.Navigation, + event: MetaMetricsEventName.AppLocked, + properties: { + location: 'Global Menu', + }, + }); closeMenu(); }} data-testid="global-menu-lock" diff --git a/ui/components/multichain/global-menu/global-menu.test.js b/ui/components/multichain/global-menu/global-menu.test.js index f3ed60fd3..aaa47f3ad 100644 --- a/ui/components/multichain/global-menu/global-menu.test.js +++ b/ui/components/multichain/global-menu/global-menu.test.js @@ -38,7 +38,7 @@ describe('AccountListItem', () => { fireEvent.click(getByTestId('global-menu-portfolio')); await waitFor(() => { expect(global.platform.openTab).toHaveBeenCalledWith({ - url: `${process.env.PORTFOLIO_URL}?metamaskEntry=ext`, + url: `/?metamaskEntry=ext&metametricsId=`, }); }); }); diff --git a/ui/components/multichain/multichain-token-list-item/multichain-token-list-item.js b/ui/components/multichain/multichain-token-list-item/multichain-token-list-item.js index 82ed77524..8f502c0da 100644 --- a/ui/components/multichain/multichain-token-list-item/multichain-token-list-item.js +++ b/ui/components/multichain/multichain-token-list-item/multichain-token-list-item.js @@ -1,4 +1,4 @@ -import React from 'react'; +import React, { useContext } from 'react'; import PropTypes from 'prop-types'; import { useSelector } from 'react-redux'; import classnames from 'classnames'; @@ -21,9 +21,14 @@ import { Text, } from '../../component-library'; import Box from '../../ui/box/box'; -import { getNativeCurrencyImage } from '../../../selectors'; +import { getCurrentChainId, getNativeCurrencyImage } from '../../../selectors'; import Tooltip from '../../ui/tooltip'; import { useI18nContext } from '../../../hooks/useI18nContext'; +import { MetaMetricsContext } from '../../../contexts/metametrics'; +import { + MetaMetricsEventCategory, + MetaMetricsEventName, +} from '../../../../shared/constants/metametrics'; export const MultichainTokenListItem = ({ className, @@ -37,6 +42,9 @@ export const MultichainTokenListItem = ({ const t = useI18nContext(); const primaryTokenImage = useSelector(getNativeCurrencyImage); const dataTheme = document.documentElement.getAttribute('data-theme'); + const trackEvent = useContext(MetaMetricsContext); + const chainId = useSelector(getCurrentChainId); + return ( { e.preventDefault(); onClick(); + trackEvent({ + category: MetaMetricsEventCategory.Tokens, + event: MetaMetricsEventName.TokenDetailsOpened, + properties: { + location: 'Home', + chain_id: chainId, + token_symbol: tokenSymbol, + }, + }); }} > { const currentChainId = useSelector(getCurrentChainId); const dispatch = useDispatch(); const history = useHistory(); + const trackEvent = useContext(MetaMetricsContext); const environmentType = getEnvironmentType(); const isFullScreen = environmentType === ENVIRONMENT_TYPE_FULLSCREEN; @@ -65,6 +71,16 @@ export const NetworkListMenu = ({ onClose }) => { } else { dispatch(setActiveNetwork(network.id)); } + trackEvent({ + event: MetaMetricsEventName.NavNetworkSwitched, + category: MetaMetricsEventCategory.Network, + properties: { + location: 'Network Menu', + chain_id: currentChainId, + from_network: currentChainId, + to_network: network.id || network.chainId, + }, + }); }} onDeleteClick={ canDeleteNetwork @@ -92,7 +108,16 @@ export const NetworkListMenu = ({ onClose }) => { {t('showTestnetNetworks')} dispatch(setShowTestNetworks(!value))} + onToggle={(value) => { + const shouldShowTestNetworks = !value; + dispatch(setShowTestNetworks(shouldShowTestNetworks)); + if (shouldShowTestNetworks) { + trackEvent({ + event: MetaMetricsEventName.TestNetworksDisplayed, + category: MetaMetricsEventCategory.Network, + }); + } + }} /> @@ -106,6 +131,10 @@ export const NetworkListMenu = ({ onClose }) => { ADD_POPULAR_CUSTOM_NETWORK, ); dispatch(toggleNetworkMenu()); + trackEvent({ + event: MetaMetricsEventName.AddNetworkButtonClick, + category: MetaMetricsEventCategory.Network, + }); }} > {t('addNetwork')} diff --git a/ui/helpers/utils/metrics.js b/ui/helpers/utils/metrics.js index c085545f2..56cf95873 100644 --- a/ui/helpers/utils/metrics.js +++ b/ui/helpers/utils/metrics.js @@ -8,3 +8,11 @@ export function getMethodName(camelCase) { .replace(/([A-Z])([a-z])/gu, ' $1$2') .replace(/ +/gu, ' '); } + +export function formatAccountType(accountType) { + if (accountType === 'default') { + return 'metamask'; + } + + return accountType; +} diff --git a/ui/helpers/utils/portfolio.js b/ui/helpers/utils/portfolio.js new file mode 100644 index 000000000..06515c95b --- /dev/null +++ b/ui/helpers/utils/portfolio.js @@ -0,0 +1,8 @@ +export function getPortfolioUrl( + endpoint = '', + metamaskEntry = '', + metaMetricsId = '', +) { + const portfolioUrl = process.env.PORTFOLIO_URL || ''; + return `${portfolioUrl}/${endpoint}?metamaskEntry=${metamaskEntry}&metametricsId=${metaMetricsId}`; +} diff --git a/ui/pages/add-nft/add-nft.js b/ui/pages/add-nft/add-nft.js index c4c40895e..9772c9c25 100644 --- a/ui/pages/add-nft/add-nft.js +++ b/ui/pages/add-nft/add-nft.js @@ -112,7 +112,7 @@ export default function AddNft() { tokenId: tokenId.toString(), asset_type: AssetType.NFT, token_standard: tokenDetails?.standard, - source: MetaMetricsTokenEventSource.Custom, + source_connection_method: MetaMetricsTokenEventSource.Custom, }, }); diff --git a/ui/pages/confirm-add-suggested-token/confirm-add-suggested-token.js b/ui/pages/confirm-add-suggested-token/confirm-add-suggested-token.js index bba7aedc5..1e8044af2 100644 --- a/ui/pages/confirm-add-suggested-token/confirm-add-suggested-token.js +++ b/ui/pages/confirm-add-suggested-token/confirm-add-suggested-token.js @@ -129,7 +129,7 @@ const ConfirmAddSuggestedToken = () => { token_contract_address: asset.address, token_decimal_precision: asset.decimals, unlisted: asset.unlisted, - source: MetaMetricsTokenEventSource.Dapp, + source_connection_method: MetaMetricsTokenEventSource.Dapp, token_standard: TokenStandard.ERC20, asset_type: AssetType.token, }, diff --git a/ui/pages/confirm-import-token/confirm-import-token.js b/ui/pages/confirm-import-token/confirm-import-token.js index 61bc41ff4..f0e37909a 100644 --- a/ui/pages/confirm-import-token/confirm-import-token.js +++ b/ui/pages/confirm-import-token/confirm-import-token.js @@ -51,7 +51,7 @@ const ConfirmImportToken = () => { token_contract_address: pendingToken.address, token_decimal_precision: pendingToken.decimals, unlisted: pendingToken.unlisted, - source: pendingToken.isCustom + source_connection_method: pendingToken.isCustom ? MetaMetricsTokenEventSource.Custom : MetaMetricsTokenEventSource.List, token_standard: TokenStandard.ERC20, diff --git a/ui/pages/create-account/import-account/import-account.js b/ui/pages/create-account/import-account/import-account.js index d47e7fc5f..4a6459130 100644 --- a/ui/pages/create-account/import-account/import-account.js +++ b/ui/pages/create-account/import-account/import-account.js @@ -48,10 +48,12 @@ export default function NewAccountImportForm() { navigateToMostRecentOverviewPage(); } else { dispatch(actions.displayWarning(t('importAccountError'))); - trackImportEvent(strategy, false); } }) - .catch((error) => translateWarning(error.message)); + .catch((error) => { + trackImportEvent(strategy, error.message); + translateWarning(error.message); + }); } function trackImportEvent(strategy, wasSuccessful) { diff --git a/ui/pages/create-account/new-account.component.js b/ui/pages/create-account/new-account.component.js index 0c219d7c3..bb01e116c 100644 --- a/ui/pages/create-account/new-account.component.js +++ b/ui/pages/create-account/new-account.component.js @@ -35,6 +35,7 @@ export default class NewAccountCreateForm extends Component { event: MetaMetricsEventName.AccountAdded, properties: { account_type: MetaMetricsEventAccountType.Default, + location: 'Home', }, }); history.push(mostRecentOverviewPage); diff --git a/ui/pages/home/home.component.js b/ui/pages/home/home.component.js index a8faadc76..0bfe5d8ce 100644 --- a/ui/pages/home/home.component.js +++ b/ui/pages/home/home.component.js @@ -674,7 +674,24 @@ export default class Home extends PureComponent { { + onTabClick(tabName); + let event; + switch (tabName) { + case 'nfts': + event = MetaMetricsEventName.NftScreenOpened; + break; + case 'activity': + event = MetaMetricsEventName.ActivityScreenOpened; + break; + default: + event = MetaMetricsEventName.TokenScreenOpened; + } + this.context.trackEvent({ + category: MetaMetricsEventCategory.Home, + event, + }); + }} tabsClassName="home__tabs" > { setNetworkName(selectedNetworkName || ''); setRpcUrl(selectedNetwork.rpcUrl); @@ -543,6 +556,19 @@ const NetworksForm = ({ }), ); + trackEvent({ + event: MetaMetricsEventName.CustomNetworkAdded, + category: MetaMetricsEventCategory.Network, + properties: { + block_explorer_url: blockExplorerUrl, + chain_id: prefixedChainId, + network_name: networkName, + source_connection_method: + MetaMetricsNetworkEventSource.CustomNetworkForm, + token_symbol: ticker, + }, + }); + submitCallback?.(); } } catch (error) { diff --git a/ui/selectors/selectors.js b/ui/selectors/selectors.js index 71d8ead6c..88df4405c 100644 --- a/ui/selectors/selectors.js +++ b/ui/selectors/selectors.js @@ -119,6 +119,11 @@ export function getCurrentChainId(state) { return chainId; } +export function getMetaMetricsId(state) { + const { metaMetricsId } = state.metamask; + return metaMetricsId; +} + export function isCurrentProviderCustom(state) { const provider = getProvider(state); return ( @@ -233,7 +238,15 @@ export function getHardwareWalletType(state) { export function getAccountType(state) { const currentKeyring = getCurrentKeyring(state); - const type = currentKeyring && currentKeyring.type; + return getAccountTypeForKeyring(currentKeyring); +} + +export function getAccountTypeForKeyring(keyring) { + if (!keyring) { + return ''; + } + + const { type } = keyring; ///: BEGIN:ONLY_INCLUDE_IN(build-mmi) if (type.startsWith('Custody')) { @@ -245,6 +258,7 @@ export function getAccountType(state) { case KeyringType.trezor: case KeyringType.ledger: case KeyringType.lattice: + case KeyringType.qr: return 'hardware'; case KeyringType.imported: return 'imported';