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

Event tracking for Token Detection V2 (#14441)

This commit is contained in:
Niranjana Binoy 2022-05-11 16:27:58 -04:00 committed by GitHub
parent 4b2cd0ef7a
commit 6c757ab5e0
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
17 changed files with 232 additions and 39 deletions

View File

@ -6,6 +6,9 @@ import { MINUTE } from '../../../shared/constants/time';
import { MAINNET_CHAIN_ID } from '../../../shared/constants/network';
import { isTokenDetectionEnabledForNetwork } from '../../../shared/modules/network.utils';
import { isEqualCaseInsensitive } from '../../../shared/modules/string-utils';
import { TOKEN_STANDARDS } from '../../../ui/helpers/constants/common';
import { ASSET_TYPES } from '../../../shared/constants/transaction';
import { EVENT, EVENT_NAMES } from '../../../shared/constants/metametrics';
// By default, poll every 3 minutes
const DEFAULT_INTERVAL = MINUTE * 3;
@ -26,6 +29,7 @@ export default class DetectTokensController {
* @param config.tokenList
* @param config.tokensController
* @param config.assetsContractController
* @param config.trackMetaMetricsEvent
*/
constructor({
interval = DEFAULT_INTERVAL,
@ -35,6 +39,7 @@ export default class DetectTokensController {
tokenList,
tokensController,
assetsContractController = null,
trackMetaMetricsEvent,
} = {}) {
this.assetsContractController = assetsContractController;
this.tokensController = tokensController;
@ -51,6 +56,7 @@ export default class DetectTokensController {
this.detectedTokens = process.env.TOKEN_DETECTION_V2
? this.tokensController?.state.detectedTokens
: [];
this._trackMetaMetricsEvent = trackMetaMetricsEvent;
preferences?.store.subscribe(({ selectedAddress, useTokenDetection }) => {
if (
@ -162,6 +168,7 @@ export default class DetectTokensController {
let tokensWithBalance = [];
if (process.env.TOKEN_DETECTION_V2) {
const eventTokensDetails = [];
if (result) {
const nonZeroTokenAddresses = Object.keys(result);
for (const nonZeroTokenAddress of nonZeroTokenAddresses) {
@ -172,6 +179,9 @@ export default class DetectTokensController {
iconUrl,
aggregators,
} = tokenList[nonZeroTokenAddress];
eventTokensDetails.push(`${symbol} - ${address}`);
tokensWithBalance.push({
address,
symbol,
@ -180,7 +190,17 @@ export default class DetectTokensController {
aggregators,
});
}
if (tokensWithBalance.length > 0) {
this._trackMetaMetricsEvent({
event: EVENT_NAMES.TOKEN_DETECTED,
category: EVENT.CATEGORIES.WALLET,
properties: {
tokens: eventTokensDetails,
token_standard: TOKEN_STANDARDS.ERC20,
asset_type: ASSET_TYPES.TOKEN,
},
});
await this.tokensController.addDetectedTokens(tokensWithBalance);
}
}

View File

@ -571,6 +571,7 @@ export default class MetaMetricsController {
[TRAITS.OPENSEA_API_ENABLED]: metamaskState.openSeaEnabled,
[TRAITS.THREE_BOX_ENABLED]: metamaskState.threeBoxSyncingAllowed,
[TRAITS.THEME]: metamaskState.theme || 'default',
[TRAITS.TOKEN_DETECTION_ENABLED]: metamaskState.useTokenDetection,
};
if (!this.previousTraits) {

View File

@ -682,6 +682,7 @@ describe('MetaMetricsController', function () {
threeBoxSyncingAllowed: false,
useCollectibleDetection: false,
theme: 'default',
useTokenDetection: true,
});
assert.deepEqual(traits, {
@ -696,6 +697,7 @@ describe('MetaMetricsController', function () {
[TRAITS.OPENSEA_API_ENABLED]: true,
[TRAITS.THREE_BOX_ENABLED]: false,
[TRAITS.THEME]: 'default',
[TRAITS.TOKEN_DETECTION_ENABLED]: true,
});
});
@ -717,6 +719,7 @@ describe('MetaMetricsController', function () {
threeBoxSyncingAllowed: false,
useCollectibleDetection: false,
theme: 'default',
useTokenDetection: true,
});
const updatedTraits = metaMetricsController._buildUserTraitsObject({
@ -737,6 +740,7 @@ describe('MetaMetricsController', function () {
threeBoxSyncingAllowed: false,
useCollectibleDetection: false,
theme: 'default',
useTokenDetection: true,
});
assert.deepEqual(updatedTraits, {
@ -765,6 +769,7 @@ describe('MetaMetricsController', function () {
threeBoxSyncingAllowed: false,
useCollectibleDetection: true,
theme: 'default',
useTokenDetection: true,
});
const updatedTraits = metaMetricsController._buildUserTraitsObject({
@ -783,6 +788,7 @@ describe('MetaMetricsController', function () {
threeBoxSyncingAllowed: false,
useCollectibleDetection: true,
theme: 'default',
useTokenDetection: true,
});
assert.equal(updatedTraits, null);

View File

@ -1470,7 +1470,7 @@ describe('Transaction Controller', function () {
gas_edit_type: 'none',
network: '42',
referrer: ORIGIN_METAMASK,
source: 'user',
source: EVENT.SOURCE.TRANSACTION.USER,
type: TRANSACTION_TYPES.SIMPLE_SEND,
account_type: 'MetaMask',
asset_type: ASSET_TYPES.NATIVE,
@ -1549,7 +1549,7 @@ describe('Transaction Controller', function () {
gas_edit_type: 'none',
network: '42',
referrer: ORIGIN_METAMASK,
source: 'user',
source: EVENT.SOURCE.TRANSACTION.USER,
type: TRANSACTION_TYPES.SIMPLE_SEND,
account_type: 'MetaMask',
asset_type: ASSET_TYPES.NATIVE,
@ -1638,7 +1638,7 @@ describe('Transaction Controller', function () {
gas_edit_type: 'none',
network: '42',
referrer: 'other',
source: 'dapp',
source: EVENT.SOURCE.TRANSACTION.DAPP,
type: TRANSACTION_TYPES.SIMPLE_SEND,
account_type: 'MetaMask',
asset_type: ASSET_TYPES.NATIVE,
@ -1719,7 +1719,7 @@ describe('Transaction Controller', function () {
gas_edit_type: 'none',
network: '42',
referrer: 'other',
source: 'dapp',
source: EVENT.SOURCE.TRANSACTION.DAPP,
type: TRANSACTION_TYPES.SIMPLE_SEND,
account_type: 'MetaMask',
asset_type: ASSET_TYPES.NATIVE,
@ -1800,7 +1800,7 @@ describe('Transaction Controller', function () {
gas_edit_type: 'none',
network: '42',
referrer: 'other',
source: 'dapp',
source: EVENT.SOURCE.TRANSACTION.DAPP,
type: TRANSACTION_TYPES.SIMPLE_SEND,
account_type: 'MetaMask',
asset_type: ASSET_TYPES.NATIVE,
@ -1859,7 +1859,7 @@ describe('Transaction Controller', function () {
properties: {
network: '42',
referrer: 'other',
source: 'dapp',
source: EVENT.SOURCE.TRANSACTION.DAPP,
type: TRANSACTION_TYPES.SIMPLE_SEND,
chain_id: '0x2a',
eip_1559_version: '0',
@ -1936,7 +1936,7 @@ describe('Transaction Controller', function () {
gas_edit_type: 'none',
network: '42',
referrer: 'other',
source: 'dapp',
source: EVENT.SOURCE.TRANSACTION.DAPP,
type: TRANSACTION_TYPES.SIMPLE_SEND,
account_type: 'MetaMask',
asset_type: ASSET_TYPES.NATIVE,

View File

@ -265,7 +265,7 @@ async function addEthereumChainHandler(
network: firstValidRPCUrl,
symbol: ticker,
block_explorer_url: firstValidBlockExplorerUrl,
source: 'dapp',
source: EVENT.SOURCE.TRANSACTION.DAPP,
},
});

View File

@ -713,6 +713,9 @@ export default class MetamaskController extends EventEmitter {
network: this.networkController,
keyringMemStore: this.keyringController.memStore,
tokenList: this.tokenListController,
trackMetaMetricsEvent: this.metaMetricsController.trackEvent.bind(
this.metaMetricsController,
),
}))
: (this.detectTokensController = new DetectTokensController({
preferences: this.preferencesController,

View File

@ -178,6 +178,8 @@
* @property {'three_box_enabled'} THREE_BOX_ENABLED - when 3box feature is
* toggled we identify the 3box_enabled trait
* @property {'theme'} THEME - when the user's theme changes we identify the theme trait
* @property {'token_detection_enabled'} TOKEN_DETECTION_ENABLED - when token detection feature is toggled we
* identify the token_detection_enabled trait
*/
/**
@ -197,6 +199,7 @@ export const TRAITS = {
OPENSEA_API_ENABLED: 'opensea_api_enabled',
THREE_BOX_ENABLED: 'three_box_enabled',
THEME: 'theme',
TOKEN_DETECTION_ENABLED: 'token_detection_enabled',
};
/**
@ -222,6 +225,7 @@ export const TRAITS = {
* @property {boolean} [three_box_enabled] - does the user have 3box sync
* enabled?
* @property {string} [theme] - which theme the user has selected
* @property {boolean} [token_detection_enabled] - does the user have token detection is enabled?
*/
// Mixpanel converts the zero address value to a truly anonymous event, which
@ -265,10 +269,15 @@ export const REJECT_NOTFICIATION_CLOSE_SIG =
*/
export const EVENT_NAMES = {
SIGNATURE_REQUESTED: 'Signature Requested',
ENCRYPTION_PUBLIC_KEY_REQUESTED: 'Encryption Public Key Requested',
DECRYPTION_REQUESTED: 'Decryption Requested',
PERMISSIONS_REQUESTED: 'Permissions Requested',
SIGNATURE_REQUESTED: 'Signature Requested',
TOKEN_ADDED: 'Token Added',
TOKEN_DETECTED: 'Token Detected',
TOKEN_HIDDEN: 'Token Hidden',
TOKEN_IMPORT_CANCELED: 'Token Import Canceled',
TOKEN_IMPORT_CLICKED: 'Token Import Clicked',
};
export const EVENT = {
@ -288,4 +297,25 @@ export const EVENT = {
TRANSACTIONS: 'Transactions',
WALLET: 'Wallet',
},
SOURCE: {
SWAPS: {
MAIN_VIEW: 'Main View',
TOKEN_VIEW: 'Token View',
},
TRANSACTION: {
USER: 'user',
DAPP: 'dapp',
},
TOKEN: {
CUSTOM: 'custom',
DETECTED: 'detected',
DAPP: 'dapp',
LIST: 'list',
},
},
LOCATION: {
TOKEN_DETECTION: 'token_detection',
TOKEN_MENU: 'token_menu',
TOKEN_DETAILS: 'token_details',
},
};

View File

@ -1,4 +1,4 @@
import React from 'react';
import React, { useContext } from 'react';
import { useSelector } from 'react-redux';
import PropTypes from 'prop-types';
import classNames from 'classnames';
@ -7,11 +7,32 @@ import Box from '../../../ui/box/box';
import Button from '../../../ui/button';
import { useI18nContext } from '../../../../hooks/useI18nContext';
import { getDetectedTokensInCurrentNetwork } from '../../../../selectors';
import { MetaMetricsContext } from '../../../../contexts/metametrics';
import {
EVENT,
EVENT_NAMES,
} from '../../../../../shared/constants/metametrics';
const DetectedTokensLink = ({ className = '', setShowDetectedTokens }) => {
const t = useI18nContext();
const detectedTokens = useSelector(getDetectedTokensInCurrentNetwork);
const trackEvent = useContext(MetaMetricsContext);
const detectedTokens = useSelector(getDetectedTokensInCurrentNetwork);
const detectedTokensDetails = detectedTokens.map(
({ address, symbol }) => `${symbol} - ${address}`,
);
const onClick = () => {
setShowDetectedTokens(true);
trackEvent({
event: EVENT_NAMES.TOKEN_IMPORT_CLICKED,
category: EVENT.CATEGORIES.WALLET,
properties: {
source: EVENT.SOURCE.TOKEN.DETECTED,
tokens: detectedTokensDetails,
},
});
};
return (
<Box
className={classNames('detected-tokens-link', className)}
@ -20,7 +41,7 @@ const DetectedTokensLink = ({ className = '', setShowDetectedTokens }) => {
<Button
type="link"
className="detected-tokens-link__link"
onClick={() => setShowDetectedTokens(true)}
onClick={onClick}
>
{t('numberOfNewTokensDetected', [detectedTokens.length])}
</Button>

View File

@ -0,0 +1,28 @@
import React from 'react';
import { Provider } from 'react-redux';
import testData from '../../../../../.storybook/test-data';
import configureStore from '../../../../store/store';
import DetectedTokensLink from './detected-tokens-link';
const store = configureStore(testData);
export default {
title: 'Components/App/AssetList/DetectedTokensLink',
decorators: [(story) => <Provider store={store}>{story()}</Provider>],
id: __filename,
argTypes: {
setShowDetectedTokens: { control: 'func' },
},
args: {
setShowDetectedTokens: 'setShowDetectedTokensSpy',
},
};
const Template = (args) => {
return <DetectedTokensLink {...args} />;
};
export const DefaultStory = Template.bind({});
DefaultStory.storyName = 'Default';

View File

@ -0,0 +1,32 @@
import * as React from 'react';
import {
renderWithProvider,
screen,
fireEvent,
} from '../../../../../test/jest';
import configureStore from '../../../../store/store';
import testData from '../../../../../.storybook/test-data';
import DetectedTokensLink from './detected-tokens-link';
describe('DetectedTokensLink', () => {
let setShowDetectedTokensSpy;
const args = {};
beforeEach(() => {
setShowDetectedTokensSpy = jest.fn();
args.setShowDetectedTokens = setShowDetectedTokensSpy;
});
it('should render number of tokens detected link', () => {
const store = configureStore(testData);
renderWithProvider(<DetectedTokensLink {...args} />, store);
expect(
screen.getByText('3 new tokens found in this account'),
).toBeInTheDocument();
fireEvent.click(screen.getByText('3 new tokens found in this account'));
expect(setShowDetectedTokensSpy).toHaveBeenCalled();
});
});

View File

@ -1,8 +1,13 @@
import React from 'react';
import React, { useContext } from 'react';
import PropTypes from 'prop-types';
import { useSelector } from 'react-redux';
import { useI18nContext } from '../../../../hooks/useI18nContext';
import { MetaMetricsContext } from '../../../../contexts/metametrics';
import {
EVENT,
EVENT_NAMES,
} from '../../../../../shared/constants/metametrics';
import { getDetectedTokensInCurrentNetwork } from '../../../../selectors';
import Popover from '../../../ui/popover';
@ -19,6 +24,7 @@ const DetectedTokenSelectionPopover = ({
sortingBasedOnTokenSelection,
}) => {
const t = useI18nContext();
const trackEvent = useContext(MetaMetricsContext);
const detectedTokens = useSelector(getDetectedTokensInCurrentNetwork);
const { selected: selectedTokens = [] } = sortingBasedOnTokenSelection(
@ -31,6 +37,17 @@ const DetectedTokenSelectionPopover = ({
const onClose = () => {
setShowDetectedTokens(false);
const eventTokensDetails = detectedTokens.map(
({ address, symbol }) => `${symbol} - ${address}`,
);
trackEvent({
event: EVENT_NAMES.TOKEN_IMPORT_CANCELED,
category: EVENT.CATEGORIES.WALLET,
properties: {
source: EVENT.SOURCE.TOKEN.DETECTED,
tokens: eventTokensDetails,
},
});
};
const footer = (

View File

@ -1,4 +1,4 @@
import React, { useState } from 'react';
import React, { useState, useContext } from 'react';
import PropTypes from 'prop-types';
import { useSelector, useDispatch } from 'react-redux';
import { chain } from 'lodash';
@ -9,7 +9,11 @@ import {
setNewTokensImported,
} from '../../../store/actions';
import { getDetectedTokensInCurrentNetwork } from '../../../selectors';
import { MetaMetricsContext } from '../../../contexts/metametrics';
import { TOKEN_STANDARDS } from '../../../helpers/constants/common';
import { ASSET_TYPES } from '../../../../shared/constants/transaction';
import { EVENT, EVENT_NAMES } from '../../../../shared/constants/metametrics';
import DetectedTokenSelectionPopover from './detected-token-selection-popover/detected-token-selection-popover';
import DetectedTokenIgnoredPopover from './detected-token-ignored-popover/detected-token-ignored-popover';
@ -26,8 +30,10 @@ const sortingBasedOnTokenSelection = (tokensDetected) => {
.value()
);
};
const DetectedToken = ({ setShowDetectedTokens }) => {
const dispatch = useDispatch();
const trackEvent = useContext(MetaMetricsContext);
const detectedTokens = useSelector(getDetectedTokensInCurrentNetwork);
@ -42,21 +48,49 @@ const DetectedToken = ({ setShowDetectedTokens }) => {
setShowDetectedTokenIgnoredPopover,
] = useState(false);
const importSelectedTokens = async (selectedTokens) => {
selectedTokens.forEach((importedToken) => {
trackEvent({
event: EVENT_NAMES.TOKEN_ADDED,
category: EVENT.CATEGORIES.WALLET,
sensitiveProperties: {
token_symbol: importedToken.symbol,
token_contract_address: importedToken.address,
token_decimal_precision: importedToken.decimals,
source: EVENT.SOURCE.TOKEN.DETECTED,
token_standard: TOKEN_STANDARDS.ERC20,
asset_type: ASSET_TYPES.TOKEN,
},
});
});
await dispatch(importTokens(selectedTokens));
const tokenSymbols = selectedTokens.map(({ symbol }) => symbol);
dispatch(setNewTokensImported(tokenSymbols.join(', ')));
};
const handleClearTokensSelection = async () => {
// create a lodash chain on this object
const {
selected: selectedTokens,
deselected: deSelectedTokens,
selected: selectedTokens = [],
deselected: deSelectedTokens = [],
} = sortingBasedOnTokenSelection(tokensListDetected);
if (deSelectedTokens.length < detectedTokens.length) {
await dispatch(ignoreTokens(deSelectedTokens));
await dispatch(importTokens(selectedTokens));
const tokenSymbols = selectedTokens.map(({ symbol }) => symbol);
dispatch(setNewTokensImported(tokenSymbols.join(', ')));
} else {
await dispatch(ignoreTokens(deSelectedTokens));
await importSelectedTokens(selectedTokens);
}
const tokensDetailsList = deSelectedTokens.map(
({ symbol, address }) => `${symbol} - ${address}`,
);
trackEvent({
event: EVENT_NAMES.TOKEN_HIDDEN,
category: EVENT.CATEGORIES.WALLET,
sensitiveProperties: {
tokens: tokensDetailsList,
location: EVENT.LOCATION.TOKEN_DETECTION,
token_standard: TOKEN_STANDARDS.ERC20,
asset_type: ASSET_TYPES.TOKEN,
},
});
await dispatch(ignoreTokens(deSelectedTokens));
setShowDetectedTokens(false);
};
@ -71,17 +105,14 @@ const DetectedToken = ({ setShowDetectedTokens }) => {
};
const onImport = async () => {
// create a lodash chain on this object
const { selected: selectedTokens } = sortingBasedOnTokenSelection(
const { selected: selectedTokens = [] } = sortingBasedOnTokenSelection(
tokensListDetected,
);
if (selectedTokens.length < detectedTokens.length) {
setShowDetectedTokenIgnoredPopover(true);
} else {
const tokenSymbols = selectedTokens.map(({ symbol }) => symbol);
await dispatch(importTokens(selectedTokens));
dispatch(setNewTokensImported(tokenSymbols.join(', ')));
await importSelectedTokens(selectedTokens);
setShowDetectedTokens(false);
}
};

View File

@ -136,7 +136,7 @@ const EthOverview = ({ className }) => {
event: 'Swaps Opened',
category: EVENT.CATEGORIES.SWAPS,
properties: {
source: 'Main View',
source: EVENT.SOURCE.SWAPS.MAIN_VIEW,
active_currency: 'ETH',
},
});

View File

@ -120,7 +120,7 @@ const TokenOverview = ({ className, token }) => {
event: 'Swaps Opened',
category: EVENT.CATEGORIES.SWAPS,
properties: {
source: 'Token View',
source: EVENT.SOURCE.SWAPS.TOKEN_VIEW,
active_currency: token.symbol,
},
});

View File

@ -27,6 +27,7 @@ import { getCollectiblesDetectionNoticeDismissed } from '../../ducks/metamask/me
import CollectiblesDetectionNotice from '../../components/app/collectibles-detection-notice';
import { MetaMetricsContext } from '../../contexts/metametrics';
import { ASSET_TYPES } from '../../../shared/constants/transaction';
import { EVENT, EVENT_NAMES } from '../../../shared/constants/metametrics';
export default function AddCollectible() {
const t = useI18nContext();
@ -77,7 +78,7 @@ export default function AddCollectible() {
);
trackEvent({
event: 'Token Added',
event: EVENT_NAMES.TOKEN_ADDED,
category: 'Wallet',
sensitiveProperties: {
token_contract_address: address,
@ -85,7 +86,7 @@ export default function AddCollectible() {
tokenId: tokenId.toString(),
asset_type: ASSET_TYPES.COLLECTIBLE,
token_standard: tokenDetails?.standard,
source: 'custom',
source: EVENT.SOURCE.TOKEN.CUSTOM,
},
});

View File

@ -14,7 +14,7 @@ import { isEqualCaseInsensitive } from '../../../shared/modules/string-utils';
import { getSuggestedAssets } from '../../selectors';
import { rejectWatchAsset, acceptWatchAsset } from '../../store/actions';
import { TOKEN_STANDARDS } from '../../helpers/constants/common';
import { EVENT } from '../../../shared/constants/metametrics';
import { EVENT, EVENT_NAMES } from '../../../shared/constants/metametrics';
import { ASSET_TYPES } from '../../../shared/constants/transaction';
function getTokenName(name, symbol) {
@ -115,14 +115,14 @@ const ConfirmAddSuggestedToken = () => {
await dispatch(acceptWatchAsset(id));
trackEvent({
event: 'Token Added',
event: EVENT_NAMES.TOKEN_ADDED,
category: EVENT.CATEGORIES.WALLET,
sensitiveProperties: {
token_symbol: asset.symbol,
token_contract_address: asset.address,
token_decimal_precision: asset.decimals,
unlisted: asset.unlisted,
source: 'dapp',
source: EVENT.SOURCE.TOKEN.DAPP,
token_standard: TOKEN_STANDARDS.ERC20,
asset_type: ASSET_TYPES.TOKEN,
},

View File

@ -13,7 +13,8 @@ import { MetaMetricsContext } from '../../contexts/metametrics';
import { getMostRecentOverviewPage } from '../../ducks/history/history';
import { getPendingTokens } from '../../ducks/metamask/metamask';
import { addTokens, clearPendingTokens } from '../../store/actions';
import { EVENT } from '../../../shared/constants/metametrics';
import { TOKEN_STANDARDS } from '../../helpers/constants/common';
import { EVENT, EVENT_NAMES } from '../../../shared/constants/metametrics';
import { ASSET_TYPES } from '../../../shared/constants/transaction';
const getTokenName = (name, symbol) => {
@ -37,15 +38,17 @@ const ConfirmImportToken = () => {
addedTokenValues.forEach((pendingToken) => {
trackEvent({
event: 'Token Added',
event: EVENT_NAMES.TOKEN_ADDED,
category: EVENT.CATEGORIES.WALLET,
sensitiveProperties: {
token_symbol: pendingToken.symbol,
token_contract_address: pendingToken.address,
token_decimal_precision: pendingToken.decimals,
unlisted: pendingToken.unlisted,
source: pendingToken.isCustom ? 'custom' : 'list',
token_standard: pendingToken.standard,
source: pendingToken.isCustom
? EVENT.SOURCE.TOKEN.CUSTOM
: EVENT.SOURCE.TOKEN.LIST,
token_standard: TOKEN_STANDARDS.ERC20,
asset_type: ASSET_TYPES.TOKEN,
},
});