1
0
mirror of https://github.com/kremalicious/metamask-extension.git synced 2024-11-22 01:47:00 +01:00

UX: Multichain: Analytics (#18674)

This commit is contained in:
David Walsh 2023-04-27 09:28:08 -05:00 committed by GitHub
parent bbb35dbe8d
commit e339afce7a
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
29 changed files with 379 additions and 57 deletions

View File

@ -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 {

View File

@ -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',
);
},
);

View File

@ -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,
},
});

View File

@ -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`}

View File

@ -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 || (
<ConfirmPageContainerContent
metaMetricsId={metaMetricsId}
action={action}
title={title}
image={image}

View File

@ -9,7 +9,10 @@ import {
MetaMetricsEventName,
MetaMetricsTokenEventSource,
} from '../../../../../shared/constants/metametrics';
import { getDetectedTokensInCurrentNetwork } from '../../../../selectors';
import {
getCurrentChainId,
getDetectedTokensInCurrentNetwork,
} from '../../../../selectors';
import Popover from '../../../ui/popover';
import Box from '../../../ui/box';
@ -27,6 +30,8 @@ const DetectedTokenSelectionPopover = ({
const t = useI18nContext();
const trackEvent = useContext(MetaMetricsContext);
const chainId = useSelector(getCurrentChainId);
const detectedTokens = useSelector(getDetectedTokensInCurrentNetwork);
const { selected: selectedTokens = [] } =
sortingBasedOnTokenSelection(tokensListDetected);
@ -44,7 +49,8 @@ const DetectedTokenSelectionPopover = ({
event: MetaMetricsEventName.TokenImportCanceled,
category: MetaMetricsEventCategory.Wallet,
properties: {
source: MetaMetricsTokenEventSource.Detected,
source_connection_method: MetaMetricsTokenEventSource.Detected,
chain_id: chainId,
tokens: eventTokensDetails,
},
});

View File

@ -8,7 +8,10 @@ import {
ignoreTokens,
setNewTokensImported,
} from '../../../store/actions';
import { getDetectedTokensInCurrentNetwork } from '../../../selectors';
import {
getCurrentChainId,
getDetectedTokensInCurrentNetwork,
} from '../../../selectors';
import { MetaMetricsContext } from '../../../contexts/metametrics';
import {
@ -47,6 +50,7 @@ const DetectedToken = ({ setShowDetectedTokens }) => {
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,
},
});
});

View File

@ -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',
},
});
}

View File

@ -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,
},
});
}}

View File

@ -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',
),
}),
);

View File

@ -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 && (
<img
className="whats-new-popup__notification-image"
@ -144,7 +156,13 @@ const renderFirstNotification = (notification, idRefMap, history, isLast) => {
<Button
type="primary"
className="whats-new-popup__button"
onClick={actionFunction}
onClick={() => {
actionFunction();
trackEvent({
category: MetaMetricsEventCategory.Home,
event: MetaMetricsEventName.WhatsNewClicked,
});
}}
>
{actionText}
</Button>
@ -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,

View File

@ -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}
>
<MenuItem
onClick={
onClick={() => {
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}

View File

@ -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 (
<Box
display={DISPLAY.FLEX}
@ -207,6 +214,13 @@ export const AccountListItem = ({
size={IconSize.Sm}
onClick={(e) => {
e.stopPropagation();
trackEvent({
event: MetaMetricsEventName.AccountDetailMenuOpened,
category: MetaMetricsEventCategory.Navigation,
properties: {
location: 'Account Options',
},
});
setAccountOptionsMenuOpen(true);
}}
data-testid="account-list-item-menu-button"

View File

@ -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
/>
<PickerNetwork
margin={2}
label={currentNetwork?.nickname}
src={currentNetwork?.rpcPrefs?.imageUrl}
onClick={() => dispatch(toggleNetworkMenu())}
onClick={networkOpenCallback}
display={[DISPLAY.NONE, DISPLAY.FLEX]} // show on desktop hide on popover
/>
{showProductTour &&
@ -171,7 +186,17 @@ export const AppHeader = ({ onClick }) => {
<AccountPicker
address={identity.address}
name={identity.name}
onClick={() => dispatch(toggleAccountMenu())}
onClick={() => {
dispatch(toggleAccountMenu());
trackEvent({
event: MetaMetricsEventName.NavAccountMenuOpened,
category: MetaMetricsEventCategory.Navigation,
properties: {
location: 'Home',
},
});
}}
/>
<Box
display={DISPLAY.FLEX}
@ -181,7 +206,13 @@ export const AppHeader = ({ onClick }) => {
{showStatus ? (
<Box ref={menuRef}>
<ConnectedStatusIndicator
onClick={() => history.push(CONNECTED_ACCOUNTS_ROUTE)}
onClick={() => {
history.push(CONNECTED_ACCOUNTS_ROUTE);
trackEvent({
event: MetaMetricsEventName.NavConnectedSitesOpened,
category: MetaMetricsEventCategory.Navigation,
});
}}
/>
</Box>
) : null}{' '}

View File

@ -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,
},
});
};

View File

@ -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 (
<Menu anchorElement={anchorElement} onHide={closeMenu}>
@ -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 }) => {
<MenuItem
iconName={IconName.Diagram}
onClick={() => {
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"

View File

@ -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=`,
});
});
});

View File

@ -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 (
<Box
className={classnames('multichain-token-list-item', className)}
@ -56,6 +64,15 @@ export const MultichainTokenListItem = ({
onClick={(e) => {
e.preventDefault();
onClick();
trackEvent({
category: MetaMetricsEventCategory.Tokens,
event: MetaMetricsEventName.TokenDetailsOpened,
properties: {
location: 'Home',
chain_id: chainId,
token_symbol: tokenSymbol,
},
});
}}
>
<BadgeWrapper

View File

@ -1,4 +1,4 @@
import React from 'react';
import React, { useContext } from 'react';
import PropTypes from 'prop-types';
import { useDispatch, useSelector } from 'react-redux';
import { useHistory } from 'react-router-dom';
@ -28,6 +28,11 @@ import { Button, BUTTON_VARIANT, Text } from '../../component-library';
import { ADD_POPULAR_CUSTOM_NETWORK } from '../../../helpers/constants/routes';
import { getEnvironmentType } from '../../../../app/scripts/lib/util';
import { ENVIRONMENT_TYPE_FULLSCREEN } from '../../../../shared/constants/app';
import { MetaMetricsContext } from '../../../contexts/metametrics';
import {
MetaMetricsEventCategory,
MetaMetricsEventName,
} from '../../../../shared/constants/metametrics';
const UNREMOVABLE_CHAIN_IDS = [CHAIN_IDS.MAINNET, ...TEST_CHAINS];
@ -38,6 +43,7 @@ export const NetworkListMenu = ({ onClose }) => {
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 }) => {
<Text>{t('showTestnetNetworks')}</Text>
<ToggleButton
value={showTestNetworks}
onToggle={(value) => dispatch(setShowTestNetworks(!value))}
onToggle={(value) => {
const shouldShowTestNetworks = !value;
dispatch(setShowTestNetworks(shouldShowTestNetworks));
if (shouldShowTestNetworks) {
trackEvent({
event: MetaMetricsEventName.TestNetworksDisplayed,
category: MetaMetricsEventCategory.Network,
});
}
}}
/>
</Box>
<Box padding={4}>
@ -106,6 +131,10 @@ export const NetworkListMenu = ({ onClose }) => {
ADD_POPULAR_CUSTOM_NETWORK,
);
dispatch(toggleNetworkMenu());
trackEvent({
event: MetaMetricsEventName.AddNetworkButtonClick,
category: MetaMetricsEventCategory.Network,
});
}}
>
{t('addNetwork')}

View File

@ -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;
}

View File

@ -0,0 +1,8 @@
export function getPortfolioUrl(
endpoint = '',
metamaskEntry = '',
metaMetricsId = '',
) {
const portfolioUrl = process.env.PORTFOLIO_URL || '';
return `${portfolioUrl}/${endpoint}?metamaskEntry=${metamaskEntry}&metametricsId=${metaMetricsId}`;
}

View File

@ -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,
},
});

View File

@ -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,
},

View File

@ -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,

View File

@ -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) {

View File

@ -35,6 +35,7 @@ export default class NewAccountCreateForm extends Component {
event: MetaMetricsEventName.AccountAdded,
properties: {
account_type: MetaMetricsEventAccountType.Default,
location: 'Home',
},
});
history.push(mostRecentOverviewPage);

View File

@ -674,7 +674,24 @@ export default class Home extends PureComponent {
<Tabs
t={this.context.t}
defaultActiveTabKey={defaultHomeActiveTabName}
onTabClick={onTabClick}
onTabClick={(tabName) => {
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"
>
<Tab

View File

@ -1,4 +1,10 @@
import React, { useCallback, useEffect, useRef, useState } from 'react';
import React, {
useCallback,
useContext,
useEffect,
useRef,
useState,
} from 'react';
import { useDispatch } from 'react-redux';
import PropTypes from 'prop-types';
import validUrl from 'valid-url';
@ -23,12 +29,17 @@ import {
} from '../../../../store/actions';
import fetchWithCache from '../../../../../shared/lib/fetch-with-cache';
import { usePrevious } from '../../../../hooks/usePrevious';
import { MetaMetricsNetworkEventSource } from '../../../../../shared/constants/metametrics';
import {
MetaMetricsEventCategory,
MetaMetricsEventName,
MetaMetricsNetworkEventSource,
} from '../../../../../shared/constants/metametrics';
import {
infuraProjectId,
FEATURED_RPCS,
} from '../../../../../shared/constants/network';
import { decimalToHex } from '../../../../../shared/modules/conversion.utils';
import { MetaMetricsContext } from '../../../../contexts/metametrics';
/**
* Attempts to convert the given chainId to a decimal string, for display
@ -96,6 +107,8 @@ const NetworksForm = ({
const [isEditing, setIsEditing] = useState(Boolean(addNewNetwork));
const [previousNetwork, setPreviousNetwork] = useState(selectedNetwork);
const trackEvent = useContext(MetaMetricsContext);
const resetForm = useCallback(() => {
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) {

View File

@ -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';