diff --git a/app/scripts/controllers/app-state-controller.test.ts b/app/scripts/controllers/app-state-controller.test.ts index 740c4a7d33f..4bc1cb63e39 100644 --- a/app/scripts/controllers/app-state-controller.test.ts +++ b/app/scripts/controllers/app-state-controller.test.ts @@ -5,6 +5,7 @@ import { ORIGIN_METAMASK, POLLING_TOKEN_ENVIRONMENT_TYPES, } from '../../../shared/constants/app'; +import { AccountOverviewTabKey } from '../../../shared/constants/app-state'; import { AppStateController } from './app-state-controller'; import type { AllowedActions, @@ -209,9 +210,11 @@ describe('AppStateController', () => { describe('setDefaultHomeActiveTabName', () => { it('sets the default home tab name', () => { - appStateController.setDefaultHomeActiveTabName('testTabName'); + appStateController.setDefaultHomeActiveTabName( + AccountOverviewTabKey.Activity, + ); expect(appStateController.store.getState().defaultHomeActiveTabName).toBe( - 'testTabName', + AccountOverviewTabKey.Activity, ); }); }); diff --git a/app/scripts/controllers/app-state-controller.ts b/app/scripts/controllers/app-state-controller.ts index e76b8fe3888..605f307ec0e 100644 --- a/app/scripts/controllers/app-state-controller.ts +++ b/app/scripts/controllers/app-state-controller.ts @@ -26,6 +26,7 @@ import { import { DEFAULT_AUTO_LOCK_TIME_LIMIT } from '../../../shared/constants/preferences'; import { LastInteractedConfirmationInfo } from '../../../shared/types/confirm'; import { SecurityAlertResponse } from '../lib/ppom/types'; +import { AccountOverviewTabKey } from '../../../shared/constants/app-state'; import type { Preferences, PreferencesControllerGetStateAction, @@ -35,7 +36,7 @@ import type { export type AppStateControllerState = { timeoutMinutes: number; connectedStatusPopoverHasBeenShown: boolean; - defaultHomeActiveTabName: string | null; + defaultHomeActiveTabName: AccountOverviewTabKey | null; browserEnvironment: Record; popupGasPollTokens: string[]; notificationGasPollTokens: string[]; @@ -326,7 +327,9 @@ export class AppStateController extends EventEmitter { * * @param defaultHomeActiveTabName - the tab name */ - setDefaultHomeActiveTabName(defaultHomeActiveTabName: string | null): void { + setDefaultHomeActiveTabName( + defaultHomeActiveTabName: AccountOverviewTabKey | null, + ): void { this.store.updateState({ defaultHomeActiveTabName, }); diff --git a/shared/constants/app-state.ts b/shared/constants/app-state.ts new file mode 100644 index 00000000000..559942f31fd --- /dev/null +++ b/shared/constants/app-state.ts @@ -0,0 +1,26 @@ +import { TraceName } from '../lib/trace'; +import { MetaMetricsEventName } from './metametrics'; + +export enum AccountOverviewTabKey { + Tokens = 'tokens', + Nfts = 'nfts', + Activity = 'activity', +} + +export const ACCOUNT_OVERVIEW_TAB_KEY_TO_METAMETRICS_EVENT_NAME_MAP = { + [AccountOverviewTabKey.Tokens]: MetaMetricsEventName.TokenScreenOpened, + [AccountOverviewTabKey.Nfts]: MetaMetricsEventName.NftScreenOpened, + [AccountOverviewTabKey.Activity]: MetaMetricsEventName.ActivityScreenOpened, +} as const; + +export const ACCOUNT_OVERVIEW_TAB_KEY_TO_TRACE_NAMES_ARRAY_MAP = { + [AccountOverviewTabKey.Tokens]: [ + TraceName.AccountOverviewAssetListTab, + TraceName.AccountOverviewAssetListTabFCP, + ], + [AccountOverviewTabKey.Nfts]: [ + TraceName.AccountOverviewNftsTab, + TraceName.AccountOverviewNftsTabFMP, + ], + [AccountOverviewTabKey.Activity]: [TraceName.AccountOverviewActivityTab], +} as const; diff --git a/shared/lib/trace.ts b/shared/lib/trace.ts index 1dd50b73622..dfbbeaf8048 100644 --- a/shared/lib/trace.ts +++ b/shared/lib/trace.ts @@ -9,7 +9,30 @@ import { log as sentryLogger } from '../../app/scripts/lib/setupSentry'; * The supported trace names. */ export enum TraceName { + /** + * `AccountList` component renders (FCP). + */ AccountList = 'Account List', + /** + * `TokenList` component renders (FCP). + */ + AccountOverviewAssetListTabFCP = 'Account Overview - Asset List Tab - First Contentful Paint', + /** + * `TokenList` component loading message disappears (FMP). + */ + AccountOverviewAssetListTab = 'Account Overview - Asset List Tab', + /** + * `NftsTab` component loading spinner disappears or no-Nfts banner renders. + */ + AccountOverviewNftsTabFMP = 'Account Overview - Nfts Tab - First Meaningful Paint', + /** + * `NftsTab` component fully loads, including all resources. + */ + AccountOverviewNftsTab = 'Account Overview - Nfts Tab', + /** + * `TransactionList` component renders (FCP). + */ + AccountOverviewActivityTab = 'Account Overview - Activity Tab', BackgroundConnect = 'Background Connect', DeveloperTest = 'Developer Test', FirstRender = 'First Render', @@ -27,6 +50,10 @@ export enum TraceName { UIStartup = 'UI Startup', } +export enum TraceOperation { + ComponentLoad = 'custom.ui.component_load', +} + const log = createModuleLogger(sentryLogger, 'trace'); const ID_DEFAULT = 'default'; @@ -90,6 +117,11 @@ export type TraceRequest = { * Custom tags to associate with the trace. */ tags?: Record; + + /** + * Custom operation name to associate with the trace. + */ + op?: TraceOperation | typeof OP_DEFAULT; }; /** @@ -111,6 +143,10 @@ export type EndTraceRequest = { * Override the end time of the trace. */ timestamp?: number; + /** + * Custom tags to associate with the trace. + */ + tags?: Record; }; export function trace(request: TraceRequest, fn: TraceCallback): T; @@ -155,6 +191,13 @@ export function endTrace(request: EndTraceRequest) { return; } + if ('tags' in request) { + pendingTrace.request.tags = Object.assign( + pendingTrace.request.tags ?? {}, + request.tags, + ); + } + pendingTrace.end(timestamp); tracesByKey.delete(key); @@ -165,6 +208,17 @@ export function endTrace(request: EndTraceRequest) { logTrace(pendingRequest, startTime, endTime); } +/** + * End multiple pending traces. + * + * @param requests - The data necessary to identify and end the pending traces. + */ +export function endTraces(...requests: EndTraceRequest[]) { + for (const request of requests) { + endTrace(request); + } +} + function traceCallback(request: TraceRequest, fn: TraceCallback): T { const { name } = request; @@ -224,6 +278,17 @@ function startTrace(request: TraceRequest): TraceContext { ); } +/** + * Start multiple traces. + * + * @param requests - The data necessary to identify and start the pending traces. + */ +export function startTraces(...requests: EndTraceRequest[]) { + for (const request of requests) { + startTrace(request); + } +} + function startSpan( request: TraceRequest, callback: (spanOptions: StartSpanOptions) => T, @@ -234,7 +299,7 @@ function startSpan( const spanOptions: StartSpanOptions = { attributes, name, - op: OP_DEFAULT, + op: request.op ?? OP_DEFAULT, parentSpan, startTime, }; diff --git a/ui/components/app/assets/nfts/nfts-tab/nfts-tab.js b/ui/components/app/assets/nfts/nfts-tab/nfts-tab.js index 9a1062e4f17..d8aabe46e46 100644 --- a/ui/components/app/assets/nfts/nfts-tab/nfts-tab.js +++ b/ui/components/app/assets/nfts/nfts-tab/nfts-tab.js @@ -39,6 +39,7 @@ import { } from '../../../../../../shared/constants/metametrics'; import { getCurrentLocale } from '../../../../../ducks/locale/locale'; import Spinner from '../../../../ui/spinner'; +import { endTrace, TraceName } from '../../../../../../shared/lib/trace'; export default function NftsTab() { const useNftDetection = useSelector(getUseNftDetection); @@ -84,6 +85,8 @@ export default function NftsTab() { referrer: ORIGIN_METAMASK, }, }); + endTrace({ name: TraceName.AccountOverviewNftsTabFMP }); + endTrace({ name: TraceName.AccountOverviewNftsTab }); }, [ nftsLoading, showNftBanner, @@ -93,6 +96,17 @@ export default function NftsTab() { currentLocale, ]); + useEffect(() => { + if (nftsLoading) { + return; + } + if (nftsStillFetchingIndication) { + endTrace({ name: TraceName.AccountOverviewNftsTabFMP }); + } else { + endTrace({ name: TraceName.AccountOverviewNftsTab }); + } + }, [nftsLoading, nftsStillFetchingIndication]); + if (!hasAnyNfts && nftsStillFetchingIndication) { return ( diff --git a/ui/components/app/assets/token-list/token-list.tsx b/ui/components/app/assets/token-list/token-list.tsx index f0b17d68602..a6cc3d7ea6c 100644 --- a/ui/components/app/assets/token-list/token-list.tsx +++ b/ui/components/app/assets/token-list/token-list.tsx @@ -1,4 +1,4 @@ -import React, { ReactNode, useMemo } from 'react'; +import React, { ReactNode, useEffect, useMemo } from 'react'; import { shallowEqual, useSelector } from 'react-redux'; import TokenCell from '../token-cell'; import { useI18nContext } from '../../../../hooks/useI18nContext'; @@ -19,6 +19,7 @@ import { import { useAccountTotalFiatBalance } from '../../../../hooks/useAccountTotalFiatBalance'; import { getConversionRate } from '../../../../ducks/metamask/metamask'; import { useNativeTokenBalance } from '../asset-list/native-token/use-native-token-balance'; +import { endTrace, TraceName } from '../../../../../shared/lib/trace'; type TokenListProps = { onTokenClick: (arg: string) => void; @@ -66,6 +67,16 @@ export default function TokenList({ contractExchangeRates, ]); + useEffect(() => { + endTrace({ name: TraceName.AccountOverviewAssetListTabFCP }); + }, []); + + useEffect(() => { + if (!loading) { + endTrace({ name: TraceName.AccountOverviewAssetListTab }); + } + }, [loading]); + return loading ? ( { + for (const traceName of ACCOUNT_OVERVIEW_TAB_KEY_TO_TRACE_NAMES_ARRAY_MAP[ + AccountOverviewTabKey.Tokens + ]) { + endTrace({ name: traceName, tags: { 'ui.event.abort': true } }); + trace({ name: traceName, op: TraceOperation.ComponentLoad }); + } + onImport(); + }} disabled={selectedTokens.length === 0} > {t('importWithCount', [`(${selectedTokens.length})`])} diff --git a/ui/components/app/transaction-list/transaction-list.component.js b/ui/components/app/transaction-list/transaction-list.component.js index 860571fbe0c..2fd7d0bc6d1 100644 --- a/ui/components/app/transaction-list/transaction-list.component.js +++ b/ui/components/app/transaction-list/transaction-list.component.js @@ -4,6 +4,7 @@ import React, { useCallback, Fragment, useContext, + useEffect, } from 'react'; import PropTypes from 'prop-types'; import { useSelector } from 'react-redux'; @@ -53,6 +54,7 @@ import { getMultichainAccountUrl } from '../../../helpers/utils/multichain/block import { MetaMetricsContext } from '../../../contexts/metametrics'; import { useMultichainSelector } from '../../../hooks/useMultichainSelector'; import { getMultichainNetwork } from '../../../selectors/multichain'; +import { endTrace, TraceName } from '../../../../shared/lib/trace'; const PAGE_INCREMENT = 10; @@ -258,6 +260,11 @@ export default function TransactionList({ // Check if the current account is a bitcoin account const isBitcoinAccount = useSelector(isSelectedInternalAccountBtc); const trackEvent = useContext(MetaMetricsContext); + + useEffect(() => { + endTrace({ name: TraceName.AccountOverviewActivityTab }); + }, []); + const multichainNetwork = useMultichainSelector( getMultichainNetwork, selectedAccount, diff --git a/ui/components/multichain/account-list-menu/account-list-menu.tsx b/ui/components/multichain/account-list-menu/account-list-menu.tsx index 75d04e9e60e..a66c94675f4 100644 --- a/ui/components/multichain/account-list-menu/account-list-menu.tsx +++ b/ui/components/multichain/account-list-menu/account-list-menu.tsx @@ -68,6 +68,7 @@ import { getOriginOfCurrentTab, getSelectedInternalAccount, getUpdatedAndSortedAccounts, + getDefaultHomeActiveTabName, ///: BEGIN:ONLY_INCLUDE_IF(solana) getIsSolanaSupportEnabled, ///: END:ONLY_INCLUDE_IF @@ -114,7 +115,16 @@ import { AccountConnections, MergedInternalAccount, } from '../../../selectors/selectors.types'; -import { endTrace, TraceName } from '../../../../shared/lib/trace'; +import { + endTrace, + trace, + TraceName, + TraceOperation, +} from '../../../../shared/lib/trace'; +import { + AccountOverviewTabKey, + ACCOUNT_OVERVIEW_TAB_KEY_TO_TRACE_NAMES_ARRAY_MAP, +} from '../../../../shared/constants/app-state'; ///: BEGIN:ONLY_INCLUDE_IF(solana) import { SOLANA_WALLET_NAME, @@ -251,6 +261,9 @@ export const AccountListMenu = ({ ), [updatedAccountsList, allowedAccountTypes], ); + const defaultHomeActiveTabName: AccountOverviewTabKey = useSelector( + getDefaultHomeActiveTabName, + ); ///: BEGIN:ONLY_INCLUDE_IF(keyring-snaps) const addSnapAccountEnabled = useSelector(getIsAddSnapAccountEnabled); ///: END:ONLY_INCLUDE_IF @@ -340,6 +353,12 @@ export const AccountListMenu = ({ location: 'Main Menu', }, }); + for (const traceName of ACCOUNT_OVERVIEW_TAB_KEY_TO_TRACE_NAMES_ARRAY_MAP[ + defaultHomeActiveTabName + ]) { + endTrace({ name: traceName, tags: { 'ui.event.abort': true } }); + trace({ name: traceName, op: TraceOperation.ComponentLoad }); + } dispatch(setSelectedAccount(account.address)); }; }, diff --git a/ui/components/multichain/account-overview/account-overview-btc.test.tsx b/ui/components/multichain/account-overview/account-overview-btc.test.tsx index 9d265657432..1def72354c8 100644 --- a/ui/components/multichain/account-overview/account-overview-btc.test.tsx +++ b/ui/components/multichain/account-overview/account-overview-btc.test.tsx @@ -9,7 +9,7 @@ import { } from './account-overview-btc'; const defaultProps: AccountOverviewBtcProps = { - defaultHomeActiveTabName: '', + defaultHomeActiveTabName: null, onTabClick: jest.fn(), setBasicFunctionalityModalOpen: jest.fn(), onSupportLinkClick: jest.fn(), diff --git a/ui/components/multichain/account-overview/account-overview-eth.test.tsx b/ui/components/multichain/account-overview/account-overview-eth.test.tsx index f6f67892942..ba2049ffd20 100644 --- a/ui/components/multichain/account-overview/account-overview-eth.test.tsx +++ b/ui/components/multichain/account-overview/account-overview-eth.test.tsx @@ -22,7 +22,7 @@ describe('AccountOverviewEth', () => { }); it('shows all tabs', () => { const { queryByTestId } = render({ - defaultHomeActiveTabName: '', + defaultHomeActiveTabName: null, onTabClick: jest.fn(), setBasicFunctionalityModalOpen: jest.fn(), onSupportLinkClick: jest.fn(), diff --git a/ui/components/multichain/account-overview/account-overview-tabs.tsx b/ui/components/multichain/account-overview/account-overview-tabs.tsx index 554d9fb0acf..184a4437aa3 100644 --- a/ui/components/multichain/account-overview/account-overview-tabs.tsx +++ b/ui/components/multichain/account-overview/account-overview-tabs.tsx @@ -1,5 +1,7 @@ import React, { useCallback, useContext, useMemo } from 'react'; +import { useDispatch } from 'react-redux'; import { useHistory } from 'react-router-dom'; +import { endTrace, trace, TraceOperation } from '../../../../shared/lib/trace'; import { useI18nContext } from '../../../hooks/useI18nContext'; import { ASSET_ROUTE } from '../../../helpers/constants/routes'; import { @@ -7,10 +9,7 @@ import { SUPPORT_LINK, ///: END:ONLY_INCLUDE_IF } from '../../../../shared/lib/ui-utils'; -import { - MetaMetricsEventCategory, - MetaMetricsEventName, -} from '../../../../shared/constants/metametrics'; +import { MetaMetricsEventCategory } from '../../../../shared/constants/metametrics'; import { MetaMetricsContext } from '../../../contexts/metametrics'; import NftsTab from '../../app/assets/nfts/nfts-tab'; import AssetList from '../../app/assets/asset-list'; @@ -37,6 +36,12 @@ import { ///: BEGIN:ONLY_INCLUDE_IF(build-mmi) import InstitutionalHomeFooter from '../../../pages/home/institutional/institutional-home-footer'; ///: END:ONLY_INCLUDE_IF +import { + ACCOUNT_OVERVIEW_TAB_KEY_TO_METAMETRICS_EVENT_NAME_MAP, + AccountOverviewTabKey, + ACCOUNT_OVERVIEW_TAB_KEY_TO_TRACE_NAMES_ARRAY_MAP, +} from '../../../../shared/constants/app-state'; +import { detectNfts } from '../../../store/actions'; import { AccountOverviewCommonProps } from './common'; export type AccountOverviewTabsProps = AccountOverviewCommonProps & { @@ -60,6 +65,7 @@ export const AccountOverviewTabs = ({ const history = useHistory(); const t = useI18nContext(); const trackEvent = useContext(MetaMetricsContext); + const dispatch = useDispatch(); const tabProps = useMemo( () => ({ @@ -69,24 +75,28 @@ export const AccountOverviewTabs = ({ [], ); - const getEventFromTabName = (tabName: string) => { - switch (tabName) { - case 'nfts': - return MetaMetricsEventName.NftScreenOpened; - case 'activity': - return MetaMetricsEventName.ActivityScreenOpened; - default: - return MetaMetricsEventName.TokenScreenOpened; - } - }; - const handleTabClick = useCallback( - (tabName: string) => { + (tabName: AccountOverviewTabKey) => { onTabClick(tabName); + if (tabName === AccountOverviewTabKey.Nfts) { + dispatch(detectNfts()); + } trackEvent({ category: MetaMetricsEventCategory.Home, - event: getEventFromTabName(tabName), + event: ACCOUNT_OVERVIEW_TAB_KEY_TO_METAMETRICS_EVENT_NAME_MAP[tabName], }); + if (defaultHomeActiveTabName) { + for (const traceName of ACCOUNT_OVERVIEW_TAB_KEY_TO_TRACE_NAMES_ARRAY_MAP[ + defaultHomeActiveTabName + ]) { + endTrace({ name: traceName, tags: { 'ui.event.abort': true } }); + } + } + for (const traceName of ACCOUNT_OVERVIEW_TAB_KEY_TO_TRACE_NAMES_ARRAY_MAP[ + tabName + ]) { + trace({ name: traceName, op: TraceOperation.ComponentLoad }); + } }, [onTabClick], ); diff --git a/ui/components/multichain/account-overview/account-overview-unknown.test.tsx b/ui/components/multichain/account-overview/account-overview-unknown.test.tsx index 59fe2a0c23b..0d46ff86275 100644 --- a/ui/components/multichain/account-overview/account-overview-unknown.test.tsx +++ b/ui/components/multichain/account-overview/account-overview-unknown.test.tsx @@ -18,7 +18,7 @@ const render = (props: AccountOverviewUnknownProps) => { describe('AccountOverviewUnknown', () => { it('shows only the activity tab', () => { const { queryByTestId } = render({ - defaultHomeActiveTabName: '', + defaultHomeActiveTabName: null, onTabClick: jest.fn(), setBasicFunctionalityModalOpen: jest.fn(), onSupportLinkClick: jest.fn(), diff --git a/ui/components/multichain/account-overview/common.ts b/ui/components/multichain/account-overview/common.ts index 7bee1928706..05957cb37cf 100644 --- a/ui/components/multichain/account-overview/common.ts +++ b/ui/components/multichain/account-overview/common.ts @@ -1,8 +1,10 @@ +import { AccountOverviewTabKey } from '../../../../shared/constants/app-state'; + export type AccountOverviewCommonProps = { onTabClick: (tabName: string) => void; setBasicFunctionalityModalOpen: () => void; ///: BEGIN:ONLY_INCLUDE_IF(build-main) onSupportLinkClick: () => void; ///: END:ONLY_INCLUDE_IF - defaultHomeActiveTabName: string; + defaultHomeActiveTabName: AccountOverviewTabKey | null; }; diff --git a/ui/components/multichain/account-picker/account-picker.js b/ui/components/multichain/account-picker/account-picker.js index 20d38292319..2f231dbd624 100644 --- a/ui/components/multichain/account-picker/account-picker.js +++ b/ui/components/multichain/account-picker/account-picker.js @@ -31,7 +31,7 @@ import { shortenAddress } from '../../../helpers/utils/util'; ///: BEGIN:ONLY_INCLUDE_IF(build-mmi) import { getCustodianIconForAddress } from '../../../selectors/institutional/selectors'; ///: END:ONLY_INCLUDE_IF -import { trace, TraceName } from '../../../../shared/lib/trace'; +import { trace, TraceName, TraceOperation } from '../../../../shared/lib/trace'; export const AccountPicker = ({ address, @@ -60,7 +60,10 @@ export const AccountPicker = ({ className={classnames('multichain-account-picker', className)} data-testid="account-menu-icon" onClick={() => { - trace({ name: TraceName.AccountList }); + trace({ + name: TraceName.AccountList, + op: TraceOperation.ComponentLoad, + }); onClick(); }} backgroundColor={BackgroundColor.transparent} diff --git a/ui/components/ui/tabs/tabs.component.js b/ui/components/ui/tabs/tabs.component.js index 9795f697fce..e96576711ce 100644 --- a/ui/components/ui/tabs/tabs.component.js +++ b/ui/components/ui/tabs/tabs.component.js @@ -1,14 +1,12 @@ import React, { useState } from 'react'; import PropTypes from 'prop-types'; import classnames from 'classnames'; -import { useDispatch } from 'react-redux'; import Box from '../box'; import { BackgroundColor, DISPLAY, JustifyContent, } from '../../../helpers/constants/design-system'; -import { detectNfts } from '../../../store/actions'; const Tabs = ({ defaultActiveTabKey, @@ -22,7 +20,6 @@ const Tabs = ({ const _getValidChildren = () => { return React.Children.toArray(children).filter(Boolean); }; - const dispatch = useDispatch(); /** * Returns the index of the child with the given key @@ -44,10 +41,6 @@ const Tabs = ({ setActiveTabIndex(tabIndex); onTabClick?.(tabKey); } - - if (tabKey === 'nfts') { - dispatch(detectNfts()); - } }; const renderTabs = () => { diff --git a/ui/selectors/selectors.js b/ui/selectors/selectors.js index c4f2928d8ef..7c407eae4dc 100644 --- a/ui/selectors/selectors.js +++ b/ui/selectors/selectors.js @@ -1299,6 +1299,10 @@ export function getOriginOfCurrentTab(state) { return state.activeTab.origin; } +export function getDefaultHomeActiveTabName(state) { + return state.metamask.defaultHomeActiveTabName; +} + export function getIpfsGateway(state) { return state.metamask.ipfsGateway; }