Skip to content

Commit 15f7125

Browse files
authored
perf: Create custom spans for account overview tabs (#28086)
<!-- Please submit this PR as a draft initially. Do not mark it as "Ready for review" until the template has been completely filled out, and PR status checks have passed at least once. --> ## **Description** [![Open in GitHub Codespaces](https://github.com/codespaces/badge.svg)](https://codespaces.new/MetaMask/metamask-extension/pull/28086?quickstart=1) ### Motivation - Creating a Sentry custom span that would capture this performance issue: - #27500 - It was determined that the issue is not captured by the existing "UI Startup" custom span (MetaMask/MetaMask-planning#3398). ### Approach I've decided to widen the scope of this ticket so that it covers performance issues with all accounts in general (not just those created with the `account-watcher` snap). To this end, I've defined three new custom spans: - `'Account Overview - Asset List Tab'` - `'Account Overview - Nfts Tab'` - `'Account Overview - Activity Tab'` #### Behavior - **Triggers** a) when an account list item is clicked in the account list menu component, or b) when an account overview tab (e.g. "Tokens", "NFTs", "Activity") is clicked, or c) (Only for `"Account Overview - Asset List Tab"`) the "Import" button in the detected token selection popover is clicked. - **Terminates** when one of the following happens (race): - A different account is clicked in the account list menu. - An account overview tab is clicked. - The newly selected tab component finishes loading -- including its resources. - If there is no data to display, the loading spinner/message disappears, and the appropriate filler content renders. #### Helpers - Added selector for the `defaultHomeActiveTabName` property of the `metamask` slice. - Added enum `AccountOverviewTabKey` and objects `ACCOUNT_OVERVIEW_TAB_KEY_TO_METAMETRICS_EVENT_NAME_MAP`, `ACCOUNT_OVERVIEW_TAB_KEY_TO_TRACE_NAME_MAP`. ### Results - The custom spans function as expected. - The spans successfully distinguish periods of inactivity with loading time. - The spans continue to capture delays while the UI toggles between accounts and account overview tabs. <img width="1677" alt="Screenshot 2024-11-12 at 8 52 58 AM" src="https://github.com/user-attachments/assets/62eda693-f488-46f5-a808-6f25a1763d52"> - The custom spans capture the performance issues that motivated this ticket - #27500 (see 1.46m long Nfts Tab span and 5.49s Asset List Tab span) <img width="1214" alt="Screenshot 2024-11-13 at 1 24 17 AM" src="https://github.com/user-attachments/assets/9c1f0108-f194-4074-bcc4-1a96f94781d5"> - #28130 (see 1.28m long Nfts Tab span and Error message: "Cannot destructure property 'imageOriginal' ...") <img width="1214" alt="Screenshot 2024-11-13 at 1 30 00 AM" src="https://github.com/user-attachments/assets/9baaf40c-d4da-465b-aef5-cbd684d6f450"> ## **Related issues** - Closes MetaMask/MetaMask-planning#3516 ## **Manual testing steps** 1. Click one or more of the account overview tabs ("Tokens", "NFTs", "Activity"). 2. Click Account Menu 3. Switch account and repeat. - Add accounts as necessary. - For load testing, add whale account(s) using the experimental "Watch Account" feature. ## **Screenshots/Recordings** ## **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 6bb750a commit 15f7125

File tree

16 files changed

+113
-35
lines changed

16 files changed

+113
-35
lines changed

app/scripts/controllers/app-state-controller.test.ts

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import {
55
ORIGIN_METAMASK,
66
POLLING_TOKEN_ENVIRONMENT_TYPES,
77
} from '../../../shared/constants/app';
8+
import { AccountOverviewTabKey } from '../../../shared/constants/app-state';
89
import { AppStateController } from './app-state-controller';
910
import type {
1011
AllowedActions,
@@ -209,9 +210,11 @@ describe('AppStateController', () => {
209210

210211
describe('setDefaultHomeActiveTabName', () => {
211212
it('sets the default home tab name', () => {
212-
appStateController.setDefaultHomeActiveTabName('testTabName');
213+
appStateController.setDefaultHomeActiveTabName(
214+
AccountOverviewTabKey.Activity,
215+
);
213216
expect(appStateController.store.getState().defaultHomeActiveTabName).toBe(
214-
'testTabName',
217+
AccountOverviewTabKey.Activity,
215218
);
216219
});
217220
});

app/scripts/controllers/app-state-controller.ts

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ import {
2626
import { DEFAULT_AUTO_LOCK_TIME_LIMIT } from '../../../shared/constants/preferences';
2727
import { LastInteractedConfirmationInfo } from '../../../shared/types/confirm';
2828
import { SecurityAlertResponse } from '../lib/ppom/types';
29+
import { AccountOverviewTabKey } from '../../../shared/constants/app-state';
2930
import type {
3031
Preferences,
3132
PreferencesControllerGetStateAction,
@@ -35,7 +36,7 @@ import type {
3536
export type AppStateControllerState = {
3637
timeoutMinutes: number;
3738
connectedStatusPopoverHasBeenShown: boolean;
38-
defaultHomeActiveTabName: string | null;
39+
defaultHomeActiveTabName: AccountOverviewTabKey | null;
3940
browserEnvironment: Record<string, string>;
4041
popupGasPollTokens: string[];
4142
notificationGasPollTokens: string[];
@@ -326,7 +327,9 @@ export class AppStateController extends EventEmitter {
326327
*
327328
* @param defaultHomeActiveTabName - the tab name
328329
*/
329-
setDefaultHomeActiveTabName(defaultHomeActiveTabName: string | null): void {
330+
setDefaultHomeActiveTabName(
331+
defaultHomeActiveTabName: AccountOverviewTabKey | null,
332+
): void {
330333
this.store.updateState({
331334
defaultHomeActiveTabName,
332335
});

shared/constants/app-state.ts

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
import { TraceName } from '../lib/trace';
2+
import { MetaMetricsEventName } from './metametrics';
3+
4+
export enum AccountOverviewTabKey {
5+
Tokens = 'tokens',
6+
Nfts = 'nfts',
7+
Activity = 'activity',
8+
}
9+
10+
export const ACCOUNT_OVERVIEW_TAB_KEY_TO_METAMETRICS_EVENT_NAME_MAP = {
11+
[AccountOverviewTabKey.Tokens]: MetaMetricsEventName.TokenScreenOpened,
12+
[AccountOverviewTabKey.Nfts]: MetaMetricsEventName.NftScreenOpened,
13+
[AccountOverviewTabKey.Activity]: MetaMetricsEventName.ActivityScreenOpened,
14+
} as const;
15+
16+
export const ACCOUNT_OVERVIEW_TAB_KEY_TO_TRACE_NAME_MAP = {
17+
[AccountOverviewTabKey.Tokens]: TraceName.AccountOverviewAssetListTab,
18+
[AccountOverviewTabKey.Nfts]: TraceName.AccountOverviewNftsTab,
19+
[AccountOverviewTabKey.Activity]: TraceName.AccountOverviewActivityTab,
20+
} as const;

shared/lib/trace.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,9 @@ import { log as sentryLogger } from '../../app/scripts/lib/setupSentry';
1010
*/
1111
export enum TraceName {
1212
AccountList = 'Account List',
13+
AccountOverviewAssetListTab = 'Account Overview Asset List Tab',
14+
AccountOverviewNftsTab = 'Account Overview Nfts Tab',
15+
AccountOverviewActivityTab = 'Account Overview Activity Tab',
1316
BackgroundConnect = 'Background Connect',
1417
DeveloperTest = 'Developer Test',
1518
FirstRender = 'First Render',

ui/components/app/assets/nfts/nfts-tab/nfts-tab.js

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@ import {
3939
} from '../../../../../../shared/constants/metametrics';
4040
import { getCurrentLocale } from '../../../../../ducks/locale/locale';
4141
import Spinner from '../../../../ui/spinner';
42+
import { endTrace, TraceName } from '../../../../../../shared/lib/trace';
4243

4344
export default function NftsTab() {
4445
const useNftDetection = useSelector(getUseNftDetection);
@@ -93,6 +94,12 @@ export default function NftsTab() {
9394
currentLocale,
9495
]);
9596

97+
useEffect(() => {
98+
if (!nftsLoading && !nftsStillFetchingIndication) {
99+
endTrace({ name: TraceName.AccountOverviewNftsTab });
100+
}
101+
}, [nftsLoading, nftsStillFetchingIndication]);
102+
96103
if (!hasAnyNfts && nftsStillFetchingIndication) {
97104
return (
98105
<Box className="nfts-tab__loading">

ui/components/app/assets/token-list/token-list.tsx

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import React, { ReactNode, useMemo } from 'react';
1+
import React, { ReactNode, useEffect, useMemo } from 'react';
22
import { shallowEqual, useSelector } from 'react-redux';
33
import TokenCell from '../token-cell';
44
import { useI18nContext } from '../../../../hooks/useI18nContext';
@@ -19,6 +19,7 @@ import {
1919
import { useAccountTotalFiatBalance } from '../../../../hooks/useAccountTotalFiatBalance';
2020
import { getConversionRate } from '../../../../ducks/metamask/metamask';
2121
import { useNativeTokenBalance } from '../asset-list/native-token/use-native-token-balance';
22+
import { endTrace, TraceName } from '../../../../../shared/lib/trace';
2223

2324
type TokenListProps = {
2425
onTokenClick: (arg: string) => void;
@@ -66,6 +67,12 @@ export default function TokenList({
6667
contractExchangeRates,
6768
]);
6869

70+
useEffect(() => {
71+
if (!loading) {
72+
endTrace({ name: TraceName.AccountOverviewAssetListTab });
73+
}
74+
}, [loading]);
75+
6976
return loading ? (
7077
<Box
7178
display={Display.Flex}

ui/components/app/detected-token/detected-token-selection-popover/detected-token-selection-popover.js

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ import Popover from '../../../ui/popover';
1818
import Box from '../../../ui/box';
1919
import Button from '../../../ui/button';
2020
import DetectedTokenDetails from '../detected-token-details/detected-token-details';
21+
import { trace, endTrace, TraceName } from '../../../../../shared/lib/trace';
2122

2223
const DetectedTokenSelectionPopover = ({
2324
tokensListDetected,
@@ -64,7 +65,11 @@ const DetectedTokenSelectionPopover = ({
6465
<Button
6566
className="detected-token-selection-popover__import-button"
6667
type="primary"
67-
onClick={onImport}
68+
onClick={() => {
69+
endTrace({ name: TraceName.AccountOverviewAssetListTab });
70+
trace({ name: TraceName.AccountOverviewAssetListTab });
71+
onImport();
72+
}}
6873
disabled={selectedTokens.length === 0}
6974
>
7075
{t('importWithCount', [`(${selectedTokens.length})`])}

ui/components/app/transaction-list/transaction-list.component.js

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import React, {
44
useCallback,
55
Fragment,
66
useContext,
7+
useEffect,
78
} from 'react';
89
import PropTypes from 'prop-types';
910
import { useSelector } from 'react-redux';
@@ -53,6 +54,7 @@ import { getMultichainAccountUrl } from '../../../helpers/utils/multichain/block
5354
import { MetaMetricsContext } from '../../../contexts/metametrics';
5455
import { useMultichainSelector } from '../../../hooks/useMultichainSelector';
5556
import { getMultichainNetwork } from '../../../selectors/multichain';
57+
import { endTrace, TraceName } from '../../../../shared/lib/trace';
5658

5759
const PAGE_INCREMENT = 10;
5860

@@ -258,6 +260,11 @@ export default function TransactionList({
258260
// Check if the current account is a bitcoin account
259261
const isBitcoinAccount = useSelector(isSelectedInternalAccountBtc);
260262
const trackEvent = useContext(MetaMetricsContext);
263+
264+
useEffect(() => {
265+
endTrace({ name: TraceName.AccountOverviewActivityTab });
266+
}, []);
267+
261268
const multichainNetwork = useMultichainSelector(
262269
getMultichainNetwork,
263270
selectedAccount,

ui/components/multichain/account-list-menu/account-list-menu.tsx

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,7 @@ import {
6868
getOriginOfCurrentTab,
6969
getSelectedInternalAccount,
7070
getUpdatedAndSortedAccounts,
71+
getDefaultHomeActiveTabName,
7172
///: BEGIN:ONLY_INCLUDE_IF(solana)
7273
getIsSolanaSupportEnabled,
7374
///: END:ONLY_INCLUDE_IF
@@ -114,7 +115,11 @@ import {
114115
AccountConnections,
115116
MergedInternalAccount,
116117
} from '../../../selectors/selectors.types';
117-
import { endTrace, TraceName } from '../../../../shared/lib/trace';
118+
import { endTrace, trace, TraceName } from '../../../../shared/lib/trace';
119+
import {
120+
ACCOUNT_OVERVIEW_TAB_KEY_TO_TRACE_NAME_MAP,
121+
AccountOverviewTabKey,
122+
} from '../../../../shared/constants/app-state';
118123
///: BEGIN:ONLY_INCLUDE_IF(solana)
119124
import {
120125
SOLANA_WALLET_NAME,
@@ -251,6 +256,9 @@ export const AccountListMenu = ({
251256
),
252257
[updatedAccountsList, allowedAccountTypes],
253258
);
259+
const defaultHomeActiveTabName: AccountOverviewTabKey = useSelector(
260+
getDefaultHomeActiveTabName,
261+
);
254262
///: BEGIN:ONLY_INCLUDE_IF(keyring-snaps)
255263
const addSnapAccountEnabled = useSelector(getIsAddSnapAccountEnabled);
256264
///: END:ONLY_INCLUDE_IF
@@ -340,6 +348,16 @@ export const AccountListMenu = ({
340348
location: 'Main Menu',
341349
},
342350
});
351+
endTrace({
352+
name: ACCOUNT_OVERVIEW_TAB_KEY_TO_TRACE_NAME_MAP[
353+
defaultHomeActiveTabName
354+
],
355+
});
356+
trace({
357+
name: ACCOUNT_OVERVIEW_TAB_KEY_TO_TRACE_NAME_MAP[
358+
defaultHomeActiveTabName
359+
],
360+
});
343361
dispatch(setSelectedAccount(account.address));
344362
};
345363
},

ui/components/multichain/account-overview/account-overview-btc.test.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ import {
99
} from './account-overview-btc';
1010

1111
const defaultProps: AccountOverviewBtcProps = {
12-
defaultHomeActiveTabName: '',
12+
defaultHomeActiveTabName: null,
1313
onTabClick: jest.fn(),
1414
setBasicFunctionalityModalOpen: jest.fn(),
1515
onSupportLinkClick: jest.fn(),

ui/components/multichain/account-overview/account-overview-eth.test.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ describe('AccountOverviewEth', () => {
2222
});
2323
it('shows all tabs', () => {
2424
const { queryByTestId } = render({
25-
defaultHomeActiveTabName: '',
25+
defaultHomeActiveTabName: null,
2626
onTabClick: jest.fn(),
2727
setBasicFunctionalityModalOpen: jest.fn(),
2828
onSupportLinkClick: jest.fn(),

ui/components/multichain/account-overview/account-overview-tabs.tsx

Lines changed: 23 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,15 @@
11
import React, { useCallback, useContext, useMemo } from 'react';
2+
import { useDispatch } from 'react-redux';
23
import { useHistory } from 'react-router-dom';
4+
import { endTrace, trace } from '../../../../shared/lib/trace';
35
import { useI18nContext } from '../../../hooks/useI18nContext';
46
import { ASSET_ROUTE } from '../../../helpers/constants/routes';
57
import {
68
///: BEGIN:ONLY_INCLUDE_IF(build-main)
79
SUPPORT_LINK,
810
///: END:ONLY_INCLUDE_IF
911
} from '../../../../shared/lib/ui-utils';
10-
import {
11-
MetaMetricsEventCategory,
12-
MetaMetricsEventName,
13-
} from '../../../../shared/constants/metametrics';
12+
import { MetaMetricsEventCategory } from '../../../../shared/constants/metametrics';
1413
import { MetaMetricsContext } from '../../../contexts/metametrics';
1514
import NftsTab from '../../app/assets/nfts/nfts-tab';
1615
import AssetList from '../../app/assets/asset-list';
@@ -37,6 +36,12 @@ import {
3736
///: BEGIN:ONLY_INCLUDE_IF(build-mmi)
3837
import InstitutionalHomeFooter from '../../../pages/home/institutional/institutional-home-footer';
3938
///: END:ONLY_INCLUDE_IF
39+
import {
40+
ACCOUNT_OVERVIEW_TAB_KEY_TO_METAMETRICS_EVENT_NAME_MAP,
41+
ACCOUNT_OVERVIEW_TAB_KEY_TO_TRACE_NAME_MAP,
42+
AccountOverviewTabKey,
43+
} from '../../../../shared/constants/app-state';
44+
import { detectNfts } from '../../../store/actions';
4045
import { AccountOverviewCommonProps } from './common';
4146

4247
export type AccountOverviewTabsProps = AccountOverviewCommonProps & {
@@ -60,6 +65,7 @@ export const AccountOverviewTabs = ({
6065
const history = useHistory();
6166
const t = useI18nContext();
6267
const trackEvent = useContext(MetaMetricsContext);
68+
const dispatch = useDispatch();
6369

6470
const tabProps = useMemo(
6571
() => ({
@@ -69,24 +75,24 @@ export const AccountOverviewTabs = ({
6975
[],
7076
);
7177

72-
const getEventFromTabName = (tabName: string) => {
73-
switch (tabName) {
74-
case 'nfts':
75-
return MetaMetricsEventName.NftScreenOpened;
76-
case 'activity':
77-
return MetaMetricsEventName.ActivityScreenOpened;
78-
default:
79-
return MetaMetricsEventName.TokenScreenOpened;
80-
}
81-
};
82-
8378
const handleTabClick = useCallback(
84-
(tabName: string) => {
79+
(tabName: AccountOverviewTabKey) => {
8580
onTabClick(tabName);
81+
if (tabName === AccountOverviewTabKey.Nfts) {
82+
dispatch(detectNfts());
83+
}
8684
trackEvent({
8785
category: MetaMetricsEventCategory.Home,
88-
event: getEventFromTabName(tabName),
86+
event: ACCOUNT_OVERVIEW_TAB_KEY_TO_METAMETRICS_EVENT_NAME_MAP[tabName],
8987
});
88+
if (defaultHomeActiveTabName) {
89+
endTrace({
90+
name: ACCOUNT_OVERVIEW_TAB_KEY_TO_TRACE_NAME_MAP[
91+
defaultHomeActiveTabName
92+
],
93+
});
94+
}
95+
trace({ name: ACCOUNT_OVERVIEW_TAB_KEY_TO_TRACE_NAME_MAP[tabName] });
9096
},
9197
[onTabClick],
9298
);

ui/components/multichain/account-overview/account-overview-unknown.test.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ const render = (props: AccountOverviewUnknownProps) => {
1818
describe('AccountOverviewUnknown', () => {
1919
it('shows only the activity tab', () => {
2020
const { queryByTestId } = render({
21-
defaultHomeActiveTabName: '',
21+
defaultHomeActiveTabName: null,
2222
onTabClick: jest.fn(),
2323
setBasicFunctionalityModalOpen: jest.fn(),
2424
onSupportLinkClick: jest.fn(),
Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,10 @@
1+
import { AccountOverviewTabKey } from '../../../../shared/constants/app-state';
2+
13
export type AccountOverviewCommonProps = {
24
onTabClick: (tabName: string) => void;
35
setBasicFunctionalityModalOpen: () => void;
46
///: BEGIN:ONLY_INCLUDE_IF(build-main)
57
onSupportLinkClick: () => void;
68
///: END:ONLY_INCLUDE_IF
7-
defaultHomeActiveTabName: string;
9+
defaultHomeActiveTabName: AccountOverviewTabKey | null;
810
};

ui/components/ui/tabs/tabs.component.js

Lines changed: 0 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,12 @@
11
import React, { useState } from 'react';
22
import PropTypes from 'prop-types';
33
import classnames from 'classnames';
4-
import { useDispatch } from 'react-redux';
54
import Box from '../box';
65
import {
76
BackgroundColor,
87
DISPLAY,
98
JustifyContent,
109
} from '../../../helpers/constants/design-system';
11-
import { detectNfts } from '../../../store/actions';
1210

1311
const Tabs = ({
1412
defaultActiveTabKey,
@@ -22,7 +20,6 @@ const Tabs = ({
2220
const _getValidChildren = () => {
2321
return React.Children.toArray(children).filter(Boolean);
2422
};
25-
const dispatch = useDispatch();
2623

2724
/**
2825
* Returns the index of the child with the given key
@@ -44,10 +41,6 @@ const Tabs = ({
4441
setActiveTabIndex(tabIndex);
4542
onTabClick?.(tabKey);
4643
}
47-
48-
if (tabKey === 'nfts') {
49-
dispatch(detectNfts());
50-
}
5144
};
5245

5346
const renderTabs = () => {

ui/selectors/selectors.js

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1299,6 +1299,10 @@ export function getOriginOfCurrentTab(state) {
12991299
return state.activeTab.origin;
13001300
}
13011301

1302+
export function getDefaultHomeActiveTabName(state) {
1303+
return state.metamask.defaultHomeActiveTabName;
1304+
}
1305+
13021306
export function getIpfsGateway(state) {
13031307
return state.metamask.ipfsGateway;
13041308
}

0 commit comments

Comments
 (0)