Skip to content

Commit 2b7101d

Browse files
authored
feat: add account_type/snap_id for buy/send metrics (MetaMask#28011)
## **Description** Adds more context/info to the Send/Buy events for both Ethereum and Bitcoin overview screens. [![Open in GitHub Codespaces](https://github.com/codespaces/badge.svg)](https://codespaces.new/MetaMask/metamask-extension/pull/28011?quickstart=1) ## **Related issues** Fixes: MetaMask/accounts-planning#636, MetaMask/accounts-planning#637 ## **Manual testing steps** - Cherry-pick this commit: f938d6d * This will allow you to use the verbose mode for the local segment server - `node development/mock-segment.js -v` - `yarn start:flask` - Create a Bitcoin account (enable support from the experimental settings) - Select a Bitcoin account - Click on "Buy" button using your Bitcoin account - Click on "Send" button using your Bitcoin account - Create an Ethereum account - Click on "Buy" button using your Bitcoin account - Click on "Send" button using your Bitcoin account You should see those events from the `mock-segment.js` server output: ``` ... [mock-segment]: Events received: Buy Button Clicked { "account_type": "bip122:p2wpkh", "location": "Home", "text": "Buy", "chain_id": "bip122:000000000019d6689c085ae165831e93", "snap_id": "npm:@metamask/bitcoin-wallet-snap", "category": "Navigation", "locale": "en", "environment_type": "popup" } ... [mock-segment]: Events received: Send Button Clicked { "account_type": "bip122:p2wpkh", "token_symbol": "BTC", "location": "Home", "text": "Send", "chain_id": "bip122:000000000019d6689c085ae165831e93", "snap_id": "npm:@metamask/bitcoin-wallet-snap", "category": "Navigation", "locale": "en", "environment_type": "popup" } ... [mock-segment]: Events received: Buy Button Clicked { "account_type": "eip155:eoa", "location": "Home", "text": "Buy", "chain_id": "0x1", "token_symbol": { "symbol": "ETH", "name": "Ether", "address": "0x0000000000000000000000000000000000000000", "decimals": 18, "iconUrl": "./images/eth_logo.svg", "balance": "0", "string": "0" }, "category": "Navigation", "locale": "en", "environment_type": "popup" } ... [mock-segment]: Events received: Send Button Clicked { "account_type": "eip155:eoa", "token_symbol": "ETH", "location": "Home", "text": "Send", "chain_id": "0x1", "category": "Navigation", "locale": "en", "environment_type": "popup" } ... ``` ## **Screenshots/Recordings** ### **Before** ### **After** ## **Pre-merge author checklist** - [x] I've followed [MetaMask Contributor Docs](https://github.com/MetaMask/contributor-docs) and [MetaMask Extension Coding Standards](https://github.com/MetaMask/metamask-extension/blob/develop/.github/guidelines/CODING_GUIDELINES.md). - [x] I've completed the PR template to the best of my ability - [x] I’ve included tests if applicable - [x] I’ve documented my code using [JSDoc](https://jsdoc.app/) format if applicable - [x] I’ve applied the right labels on the PR (see [labeling guidelines](https://github.com/MetaMask/metamask-extension/blob/develop/.github/guidelines/LABELING_GUIDELINES.md)). Not required for external contributors. ## **Pre-merge reviewer checklist** - [ ] I've manually tested the PR (e.g. pull and build branch, run the app, test code being changed). - [ ] I confirm that this PR addresses all acceptance criteria described in the ticket it closes and includes the necessary testing evidence such as recordings and or screenshots.
1 parent e54f555 commit 2b7101d

File tree

8 files changed

+323
-76
lines changed

8 files changed

+323
-76
lines changed

ui/components/app/wallet-overview/btc-overview.test.tsx

+90
Original file line numberDiff line numberDiff line change
@@ -11,14 +11,33 @@ import { MultichainNetworks } from '../../../../shared/constants/multichain/netw
1111
import { RampsMetaMaskEntry } from '../../../hooks/ramps/useRamps/useRamps';
1212
import { defaultBuyableChains } from '../../../ducks/ramps/constants';
1313
import { setBackgroundConnection } from '../../../store/background-connection';
14+
import { MetaMetricsContext } from '../../../contexts/metametrics';
15+
import {
16+
MetaMetricsEventCategory,
17+
MetaMetricsEventName,
18+
} from '../../../../shared/constants/metametrics';
1419
import BtcOverview from './btc-overview';
1520

21+
// We need to mock `dispatch` since we use it for `setDefaultHomeActiveTabName`.
22+
const mockDispatch = jest.fn().mockReturnValue(() => jest.fn());
23+
jest.mock('react-redux', () => ({
24+
...jest.requireActual('react-redux'),
25+
useDispatch: () => mockDispatch,
26+
}));
27+
28+
jest.mock('../../../store/actions', () => ({
29+
handleSnapRequest: jest.fn(),
30+
sendMultichainTransaction: jest.fn(),
31+
setDefaultHomeActiveTabName: jest.fn(),
32+
}));
33+
1634
const PORTOFOLIO_URL = 'https://portfolio.test';
1735

1836
const BTC_OVERVIEW_BUY = 'coin-overview-buy';
1937
const BTC_OVERVIEW_BRIDGE = 'coin-overview-bridge';
2038
const BTC_OVERVIEW_RECEIVE = 'coin-overview-receive';
2139
const BTC_OVERVIEW_SWAP = 'token-overview-button-swap';
40+
const BTC_OVERVIEW_SEND = 'coin-overview-send';
2241
const BTC_OVERVIEW_PRIMARY_CURRENCY = 'coin-overview__primary-currency';
2342

2443
const mockMetaMetricsId = 'deadbeef';
@@ -228,6 +247,39 @@ describe('BtcOverview', () => {
228247
});
229248
});
230249

250+
it('sends an event when clicking the Buy button', () => {
251+
const storeWithBtcBuyable = getStore({
252+
ramps: {
253+
buyableChains: mockBuyableChainsWithBtc,
254+
},
255+
});
256+
257+
const mockTrackEvent = jest.fn();
258+
const { queryByTestId } = renderWithProvider(
259+
<MetaMetricsContext.Provider value={mockTrackEvent}>
260+
<BtcOverview />
261+
</MetaMetricsContext.Provider>,
262+
storeWithBtcBuyable,
263+
);
264+
265+
const buyButton = queryByTestId(BTC_OVERVIEW_BUY);
266+
expect(buyButton).toBeInTheDocument();
267+
expect(buyButton).not.toBeDisabled();
268+
fireEvent.click(buyButton as HTMLElement);
269+
270+
expect(mockTrackEvent).toHaveBeenCalledWith({
271+
event: MetaMetricsEventName.NavBuyButtonClicked,
272+
category: MetaMetricsEventCategory.Navigation,
273+
properties: {
274+
account_type: mockNonEvmAccount.type,
275+
chain_id: MultichainNetworks.BITCOIN,
276+
location: 'Home',
277+
snap_id: mockNonEvmAccount.metadata.snap.id,
278+
text: 'Buy',
279+
},
280+
});
281+
});
282+
231283
it('always show the Receive button', () => {
232284
const { queryByTestId } = renderWithProvider(<BtcOverview />, getStore());
233285
const receiveButton = queryByTestId(BTC_OVERVIEW_RECEIVE);
@@ -263,4 +315,42 @@ describe('BtcOverview', () => {
263315
expect(buyButton).toBeInTheDocument();
264316
expect(buyButton).toBeDisabled();
265317
});
318+
319+
it('always show the Send button', () => {
320+
const { queryByTestId } = renderWithProvider(<BtcOverview />, getStore());
321+
const sendButton = queryByTestId(BTC_OVERVIEW_SEND);
322+
expect(sendButton).toBeInTheDocument();
323+
expect(sendButton).not.toBeDisabled();
324+
});
325+
326+
it('sends an event when clicking the Send button', () => {
327+
const mockTrackEvent = jest.fn();
328+
const { queryByTestId } = renderWithProvider(
329+
<MetaMetricsContext.Provider value={mockTrackEvent}>
330+
<BtcOverview />
331+
</MetaMetricsContext.Provider>,
332+
getStore(),
333+
);
334+
335+
const sendButton = queryByTestId(BTC_OVERVIEW_SEND);
336+
expect(sendButton).toBeInTheDocument();
337+
expect(sendButton).not.toBeDisabled();
338+
fireEvent.click(sendButton as HTMLElement);
339+
340+
expect(mockTrackEvent).toHaveBeenCalledWith(
341+
{
342+
event: MetaMetricsEventName.NavSendButtonClicked,
343+
category: MetaMetricsEventCategory.Navigation,
344+
properties: {
345+
account_type: mockNonEvmAccount.type,
346+
chain_id: MultichainNetworks.BITCOIN,
347+
location: 'Home',
348+
snap_id: mockNonEvmAccount.metadata.snap.id,
349+
text: 'Send',
350+
token_symbol: 'BTC',
351+
},
352+
},
353+
expect.any(Object),
354+
);
355+
});
266356
});

ui/components/app/wallet-overview/btc-overview.tsx

+4-3
Original file line numberDiff line numberDiff line change
@@ -9,9 +9,9 @@ import {
99
} from '../../../selectors/multichain';
1010
///: BEGIN:ONLY_INCLUDE_IF(build-main,build-beta,build-flask)
1111
import { getIsBitcoinBuyable } from '../../../ducks/ramps';
12-
import { getSelectedInternalAccount } from '../../../selectors';
1312
import { useMultichainSelector } from '../../../hooks/useMultichainSelector';
1413
///: END:ONLY_INCLUDE_IF
14+
import { getSelectedInternalAccount } from '../../../selectors';
1515
import { CoinOverview } from './coin-overview';
1616

1717
type BtcOverviewProps = {
@@ -21,17 +21,18 @@ type BtcOverviewProps = {
2121
const BtcOverview = ({ className }: BtcOverviewProps) => {
2222
const { chainId } = useSelector(getMultichainProviderConfig);
2323
const balance = useSelector(getMultichainSelectedAccountCachedBalance);
24+
const account = useSelector(getSelectedInternalAccount);
2425
///: BEGIN:ONLY_INCLUDE_IF(build-main,build-beta,build-flask)
25-
const selectedAccount = useSelector(getSelectedInternalAccount);
2626
const isBtcMainnetAccount = useMultichainSelector(
2727
getMultichainIsMainnet,
28-
selectedAccount,
28+
account,
2929
);
3030
const isBtcBuyable = useSelector(getIsBitcoinBuyable);
3131
///: END:ONLY_INCLUDE_IF
3232

3333
return (
3434
<CoinOverview
35+
account={account}
3536
balance={balance}
3637
// We turn this off to avoid having that asterisk + the "Balance maybe be outdated" message for now
3738
balanceIsCached={false}

ui/components/app/wallet-overview/coin-buttons.stories.js

+4
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,13 @@
11
import React from 'react';
2+
import testData from '../../../../.storybook/test-data';
23
import CoinButtons from './coin-buttons';
34

5+
const { accounts, selectedAccount } = testData.metamask.internalAccounts;
6+
47
export default {
58
title: 'Components/App/WalletOverview/CoinButtons',
69
args: {
10+
account: accounts[selectedAccount],
711
chainId: '1',
812
trackingLocation: 'home',
913
isSwapsChain: true,

ui/components/app/wallet-overview/coin-buttons.tsx

+67-29
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ import {
2424
} from '@metamask/utils';
2525

2626
///: BEGIN:ONLY_INCLUDE_IF(build-flask)
27-
import { BtcAccountType } from '@metamask/keyring-api';
27+
import { BtcAccountType, InternalAccount } from '@metamask/keyring-api';
2828
///: END:ONLY_INCLUDE_IF
2929
///: BEGIN:ONLY_INCLUDE_IF(build-main,build-beta,build-flask)
3030
import { ChainId } from '../../../../shared/constants/network';
@@ -51,7 +51,6 @@ import {
5151
getCurrentKeyring,
5252
///: END:ONLY_INCLUDE_IF
5353
getUseExternalServices,
54-
getSelectedAccount,
5554
///: BEGIN:ONLY_INCLUDE_IF(build-flask)
5655
getMemoizedUnapprovedTemplatedConfirmations,
5756
///: END:ONLY_INCLUDE_IF
@@ -90,8 +89,29 @@ import {
9089
} from '../../../store/actions';
9190
import { BITCOIN_WALLET_SNAP_ID } from '../../../../shared/lib/accounts/bitcoin-wallet-snap';
9291
///: END:ONLY_INCLUDE_IF
92+
import {
93+
getMultichainIsEvm,
94+
getMultichainNativeCurrency,
95+
} from '../../../selectors/multichain';
96+
import { useMultichainSelector } from '../../../hooks/useMultichainSelector';
97+
98+
type CoinButtonsProps = {
99+
account: InternalAccount;
100+
chainId: `0x${string}` | CaipChainId | number;
101+
trackingLocation: string;
102+
isSwapsChain: boolean;
103+
isSigningEnabled: boolean;
104+
///: BEGIN:ONLY_INCLUDE_IF(build-main,build-beta,build-flask)
105+
isBridgeChain: boolean;
106+
isBuyableChain: boolean;
107+
defaultSwapsToken?: SwapsEthToken;
108+
///: END:ONLY_INCLUDE_IF
109+
classPrefix?: string;
110+
iconButtonClassName?: string;
111+
};
93112

94113
const CoinButtons = ({
114+
account,
95115
chainId,
96116
trackingLocation,
97117
isSwapsChain,
@@ -103,26 +123,13 @@ const CoinButtons = ({
103123
///: END:ONLY_INCLUDE_IF
104124
classPrefix = 'coin',
105125
iconButtonClassName = '',
106-
}: {
107-
chainId: `0x${string}` | CaipChainId | number;
108-
trackingLocation: string;
109-
isSwapsChain: boolean;
110-
isSigningEnabled: boolean;
111-
///: BEGIN:ONLY_INCLUDE_IF(build-main,build-beta,build-flask)
112-
isBridgeChain: boolean;
113-
isBuyableChain: boolean;
114-
defaultSwapsToken?: SwapsEthToken;
115-
///: END:ONLY_INCLUDE_IF
116-
classPrefix?: string;
117-
iconButtonClassName?: string;
118-
}) => {
126+
}: CoinButtonsProps) => {
119127
const t = useContext(I18nContext);
120128
const dispatch = useDispatch();
121129

122130
const trackEvent = useContext(MetaMetricsContext);
123131
const [showReceiveModal, setShowReceiveModal] = useState(false);
124132

125-
const account = useSelector(getSelectedAccount);
126133
const { address: selectedAddress } = account;
127134
const history = useHistory();
128135
///: BEGIN:ONLY_INCLUDE_IF(build-main,build-beta,build-flask)
@@ -131,6 +138,16 @@ const CoinButtons = ({
131138
const usingHardwareWallet = isHardwareKeyring(keyring?.type);
132139
///: END:ONLY_INCLUDE_IF
133140

141+
// Initially, those events were using a "ETH" as `token_symbol`, so we keep this behavior
142+
// for EVM, no matter the currently selected native token (e.g. SepoliaETH if you are on Sepolia
143+
// network).
144+
const isEvm = useMultichainSelector(getMultichainIsEvm, account);
145+
const multichainNativeToken = useMultichainSelector(
146+
getMultichainNativeCurrency,
147+
account,
148+
);
149+
const nativeToken = isEvm ? 'ETH' : multichainNativeToken;
150+
134151
const isExternalServicesEnabled = useSelector(getUseExternalServices);
135152

136153
const buttonTooltips = {
@@ -180,6 +197,23 @@ const CoinButtons = ({
180197
};
181198
///: END:ONLY_INCLUDE_IF
182199

200+
const getSnapAccountMetaMetricsPropertiesIfAny = (
201+
internalAccount: InternalAccount,
202+
): { snap_id?: string } => {
203+
// Some accounts might be Snap accounts, in this case we add some extra properties
204+
// to the metrics:
205+
const snapId = internalAccount.metadata.snap?.id;
206+
if (snapId) {
207+
return {
208+
snap_id: snapId,
209+
};
210+
}
211+
212+
// If the account is not a Snap account or that we could not get the Snap ID for
213+
// some reason, we don't add any extra property.
214+
return {};
215+
};
216+
183217
///: BEGIN:ONLY_INCLUDE_IF(build-mmi)
184218
const mmiPortfolioEnabled = useSelector(getMmiPortfolioEnabled);
185219
const mmiPortfolioUrl = useSelector(getMmiPortfolioUrl);
@@ -276,6 +310,21 @@ const CoinButtons = ({
276310
///: END:ONLY_INCLUDE_IF
277311

278312
const handleSendOnClick = useCallback(async () => {
313+
trackEvent(
314+
{
315+
event: MetaMetricsEventName.NavSendButtonClicked,
316+
category: MetaMetricsEventCategory.Navigation,
317+
properties: {
318+
account_type: account.type,
319+
token_symbol: nativeToken,
320+
location: 'Home',
321+
text: 'Send',
322+
chain_id: chainId,
323+
...getSnapAccountMetaMetricsPropertiesIfAny(account),
324+
},
325+
},
326+
{ excludeMetaMetricsId: false },
327+
);
279328
switch (account.type) {
280329
///: BEGIN:ONLY_INCLUDE_IF(build-flask)
281330
case BtcAccountType.P2wpkh: {
@@ -291,19 +340,6 @@ const CoinButtons = ({
291340
}
292341
///: END:ONLY_INCLUDE_IF
293342
default: {
294-
trackEvent(
295-
{
296-
event: MetaMetricsEventName.NavSendButtonClicked,
297-
category: MetaMetricsEventCategory.Navigation,
298-
properties: {
299-
token_symbol: 'ETH',
300-
location: 'Home',
301-
text: 'Send',
302-
chain_id: chainId,
303-
},
304-
},
305-
{ excludeMetaMetricsId: false },
306-
);
307343
await dispatch(startNewDraftTransaction({ type: AssetType.native }));
308344
history.push(SEND_ROUTE);
309345
}
@@ -358,10 +394,12 @@ const CoinButtons = ({
358394
event: MetaMetricsEventName.NavBuyButtonClicked,
359395
category: MetaMetricsEventCategory.Navigation,
360396
properties: {
397+
account_type: account.type,
361398
location: 'Home',
362399
text: 'Buy',
363400
chain_id: chainId,
364401
token_symbol: defaultSwapsToken,
402+
...getSnapAccountMetaMetricsPropertiesIfAny(account),
365403
},
366404
});
367405
}, [chainId, defaultSwapsToken]);

ui/components/app/wallet-overview/coin-overview.tsx

+5-4
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import { zeroAddress } from 'ethereumjs-util';
1111
import { CaipChainId } from '@metamask/utils';
1212
import type { Hex } from '@metamask/utils';
1313

14+
import { InternalAccount } from '@metamask/keyring-api';
1415
import {
1516
Box,
1617
ButtonIcon,
@@ -45,7 +46,6 @@ import UserPreferencedCurrencyDisplay from '../user-preferenced-currency-display
4546
import { PRIMARY } from '../../../helpers/constants/common';
4647
import {
4748
getPreferences,
48-
getSelectedAccount,
4949
getShouldHideZeroBalanceTokens,
5050
getTokensMarketData,
5151
getIsTestnet,
@@ -74,6 +74,7 @@ import CoinButtons from './coin-buttons';
7474
import { AggregatedPercentageOverview } from './aggregated-percentage-overview';
7575

7676
export type CoinOverviewProps = {
77+
account: InternalAccount;
7778
balance: string;
7879
balanceIsCached: boolean;
7980
className?: string;
@@ -90,6 +91,7 @@ export type CoinOverviewProps = {
9091
};
9192

9293
export const CoinOverview = ({
94+
account,
9395
balance,
9496
balanceIsCached,
9597
className,
@@ -121,7 +123,6 @@ export const CoinOverview = ({
121123

122124
///: END:ONLY_INCLUDE_IF
123125

124-
const account = useSelector(getSelectedAccount);
125126
const showNativeTokenAsMainBalanceRoute = getSpecificSettingsRoute(
126127
t,
127128
t('general'),
@@ -135,12 +136,11 @@ export const CoinOverview = ({
135136
const { showFiatInTestnets, privacyMode, showNativeTokenAsMainBalance } =
136137
useSelector(getPreferences);
137138

138-
const selectedAccount = useSelector(getSelectedAccount);
139139
const shouldHideZeroBalanceTokens = useSelector(
140140
getShouldHideZeroBalanceTokens,
141141
);
142142
const { totalFiatBalance, loading } = useAccountTotalFiatBalance(
143-
selectedAccount,
143+
account,
144144
shouldHideZeroBalanceTokens,
145145
);
146146

@@ -368,6 +368,7 @@ export const CoinOverview = ({
368368
buttons={
369369
<CoinButtons
370370
{...{
371+
account,
371372
trackingLocation: 'home',
372373
chainId,
373374
isSwapsChain,

0 commit comments

Comments
 (0)