Skip to content

Commit 3708dbf

Browse files
refactor(analytics): D2 migrate useMetrics to useAnalytics in Stake and Earn components (#28263)
> [!IMPORTANT] > Depends on D1 #28262 for createMockEventBuilder added in `app/util/test/analyticsMock.ts`. > if D1 revert is ever needed, you have to also revert this one ## **Description** <!-- Write a short description of the changes included in this pull request, also include relevant motivation and context. Have in mind the following questions: 1. What is the reason for the change? 2. What is the improvement/solution? --> Part of the analytics system migration from the legacy `useMetrics` hook to the new `useAnalytics` hook across Stake and Earn components. **What changed:** - Replaced `useMetrics` with `useAnalytics` in 8 production components: `GasImpactModal`, `ClaimBanner`, `StakingCta`, `FooterButtonGroup`, `RewardsCard`, `UnstakeTimeCard`, and related views (`StakeConfirmationView`, `UnstakeConfirmationView`). - Moved `MetaMetricsEvents` imports from `hooks/useMetrics` to `core/Analytics` (the canonical source) in all affected files. - Replaced `MetricsEventBuilder.createEventBuilder` with `AnalyticsEventBuilder.createEventBuilder` in `tooltipMetaMetricsUtils.ts`. - Updated 5 test files to use the `createMockUseAnalyticsHook` factory + `jest.mocked(useAnalytics)` pattern instead of manual inline mock objects, reducing boilerplate and improving type safety. ## **Changelog** <!-- If this PR is not End-User-Facing and should not show up in the CHANGELOG, you can choose to either: 1. Write `CHANGELOG entry: null` 2. Label with `no-changelog` --> CHANGELOG entry: null ## **Related issues** Fixes: #26817 ## **Manual testing steps** N/A ## **Screenshots/Recordings** ### **Before** N/A ### **After** N/A ## **Pre-merge author checklist** - [x] I've followed [MetaMask Contributor Docs](https://github.com/MetaMask/contributor-docs) and [MetaMask Mobile Coding Standards](https://github.com/MetaMask/metamask-mobile/blob/main/.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-mobile/blob/main/.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. <!-- Generated with the help of the pr-description AI skill --> <!-- CURSOR_SUMMARY --> --- > [!NOTE] > **Medium Risk** > Moderate risk because it changes analytics instrumentation in staking/earn user flows; functional behavior should be unchanged but event building/tracking could regress or break silently. > > **Overview** > Migrates Stake/Earn UI analytics from the legacy `useMetrics` hook to the newer `useAnalytics` hook across staking modals/cards/confirmation flows, and standardizes `MetaMetricsEvents` imports to come from `core/Analytics`. > > Updates tooltip event construction to use `AnalyticsEventBuilder` instead of `MetricsEventBuilder`, and refactors affected unit tests to mock `useAnalytics` via shared helpers (`createMockUseAnalyticsHook` / `createMockEventBuilder`) to avoid async Engine readiness polling and reduce inline mock boilerplate. > > <sup>Reviewed by [Cursor Bugbot](https://cursor.com/bugbot) for commit ca3af27. Bugbot is set up for automated code reviews on this repo. Configure [here](https://www.cursor.com/dashboard/bugbot).</sup> <!-- /CURSOR_SUMMARY -->
1 parent 202d3e8 commit 3708dbf

File tree

18 files changed

+82
-114
lines changed

18 files changed

+82
-114
lines changed

app/components/UI/Earn/components/EarnTokenList/EarnTokenList.test.tsx

Lines changed: 6 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -7,20 +7,11 @@ import { Metrics, SafeAreaProvider } from 'react-native-safe-area-context';
77
import EarnTokenList from '.';
88
import { strings } from '../../../../../../locales/i18n';
99
import Engine from '../../../../../core/Engine';
10-
// Prevent `useMetrics` from triggering async Engine readiness polling (`whenEngineReady`)
10+
import { useAnalytics } from '../../../../hooks/useAnalytics/useAnalytics';
11+
import { createMockUseAnalyticsHook } from '../../../../../util/test/analyticsMock';
12+
// Prevent `useAnalytics` from triggering async Engine readiness polling (`whenEngineReady`)
1113
// which can cause Jest timeouts / "import after environment torn down" errors.
12-
jest.mock('../../../../hooks/useMetrics', () => ({
13-
MetaMetricsEvents: {
14-
EARN_TOKEN_LIST_ITEM_CLICKED: 'EARN_TOKEN_LIST_ITEM_CLICKED',
15-
},
16-
useMetrics: () => ({
17-
trackEvent: jest.fn(),
18-
createEventBuilder: () => ({
19-
addProperties: jest.fn().mockReturnThis(),
20-
build: jest.fn().mockReturnValue({}),
21-
}),
22-
}),
23-
}));
14+
jest.mock('../../../../hooks/useAnalytics/useAnalytics');
2415
import { MOCK_ACCOUNTS_CONTROLLER_STATE } from '../../../../../util/test/accountsControllerTestUtils';
2516
import initialRootState from '../../../../../util/test/initial-root-state';
2617
import renderWithProvider from '../../../../../util/test/renderWithProvider';
@@ -154,6 +145,8 @@ describe('EarnTokenList', () => {
154145
beforeEach(() => {
155146
jest.clearAllMocks();
156147

148+
jest.mocked(useAnalytics).mockReturnValue(createMockUseAnalyticsHook());
149+
157150
jest.spyOn(useStakingEligibilityHook, 'default').mockReturnValue({
158151
isEligible: true,
159152
isLoadingEligibility: false,

app/components/UI/Earn/components/Tron/TronStakingLearnMoreModal/TronStakingLearnMoreModal.test.tsx

Lines changed: 14 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,11 @@ import TronStakingLearnMoreModal from '.';
44
import { MetaMetricsEvents } from '../../../../../../core/Analytics';
55
import renderWithProvider from '../../../../../../util/test/renderWithProvider';
66
import { Metrics, SafeAreaProvider } from 'react-native-safe-area-context';
7+
import { useAnalytics } from '../../../../../hooks/useAnalytics/useAnalytics';
8+
import {
9+
createMockEventBuilder,
10+
createMockUseAnalyticsHook,
11+
} from '../../../../../../util/test/analyticsMock';
712

813
const mockNavigate = jest.fn();
914

@@ -18,29 +23,9 @@ jest.mock('@react-navigation/native', () => {
1823
});
1924

2025
const mockTrackEvent = jest.fn();
21-
const mockCreateEventBuilder = jest.fn(() => ({
22-
addProperties: jest.fn().mockReturnThis(),
23-
build: jest.fn().mockReturnValue({}),
24-
}));
26+
const mockCreateEventBuilder = jest.fn(() => createMockEventBuilder());
2527

26-
jest.mock('../../../../../hooks/useAnalytics/useAnalytics', () => ({
27-
useAnalytics: () => ({
28-
trackEvent: mockTrackEvent,
29-
createEventBuilder: mockCreateEventBuilder,
30-
}),
31-
}));
32-
33-
// LearnMoreModalFooter (child component) still uses useMetrics - mock it
34-
jest.mock('../../../../../hooks/useMetrics', () => {
35-
const actual = jest.requireActual('../../../../../../core/Analytics');
36-
return {
37-
...actual,
38-
useMetrics: () => ({
39-
trackEvent: mockTrackEvent,
40-
createEventBuilder: mockCreateEventBuilder,
41-
}),
42-
};
43-
});
28+
jest.mock('../../../../../hooks/useAnalytics/useAnalytics');
4429

4530
const mockTrace = jest.fn();
4631
const mockEndTrace = jest.fn();
@@ -102,8 +87,15 @@ const renderModal = () =>
10287
);
10388

10489
describe('TronStakingLearnMoreModal', () => {
90+
let mockHook: ReturnType<typeof createMockUseAnalyticsHook>;
91+
10592
beforeEach(() => {
10693
jest.clearAllMocks();
94+
mockHook = createMockUseAnalyticsHook({
95+
trackEvent: mockTrackEvent,
96+
createEventBuilder: mockCreateEventBuilder,
97+
});
98+
jest.mocked(useAnalytics).mockReturnValue(mockHook);
10799
mockUseTronStakeApy.mockReturnValue({
108100
fetchStatus: 'fetched',
109101
apyPercent: '4.5%',

app/components/UI/Stake/Views/StakeConfirmationView/StakeConfirmationView.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ import { strings } from '../../../../../../locales/i18n';
1313
import { FooterButtonGroupActions } from '../../components/StakingConfirmation/ConfirmationFooter/FooterButtonGroup/FooterButtonGroup.types';
1414
import UnstakingTimeCard from '../../components/StakingConfirmation/UnstakeTimeCard/UnstakeTimeCard';
1515
import { ScrollView } from 'react-native-gesture-handler';
16-
import { MetaMetricsEvents } from '../../../../hooks/useMetrics';
16+
import { MetaMetricsEvents } from '../../../../../core/Analytics';
1717
import { EVENT_LOCATIONS, EVENT_PROVIDERS } from '../../constants/events';
1818
import { getDecimalChainId } from '../../../../../util/networks';
1919

app/components/UI/Stake/Views/UnstakeConfirmationView/UnstakeConfirmationView.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ import TokenValueStack from '../../components/StakingConfirmation/TokenValueStac
1111
import AccountCard from '../../components/StakingConfirmation/AccountCard/AccountCard';
1212
import ConfirmationFooter from '../../components/StakingConfirmation/ConfirmationFooter/ConfirmationFooter';
1313
import { FooterButtonGroupActions } from '../../components/StakingConfirmation/ConfirmationFooter/FooterButtonGroup/FooterButtonGroup.types';
14-
import { MetaMetricsEvents } from '../../../../hooks/useMetrics';
14+
import { MetaMetricsEvents } from '../../../../../core/Analytics';
1515
import { EVENT_LOCATIONS, EVENT_PROVIDERS } from '../../constants/events';
1616
import { getDecimalChainId } from '../../../../../util/networks';
1717
import { useSelector } from 'react-redux';

app/components/UI/Stake/components/GasImpactModal/GasImpactModal.test.tsx

Lines changed: 5 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,8 @@ import renderWithProvider from '../../../../../util/test/renderWithProvider';
88
import Routes from '../../../../../constants/navigation/Routes';
99
import { strings } from '../../../../../../locales/i18n';
1010
import { flushPromises } from '../../../../../util/test/utils';
11+
import { useAnalytics } from '../../../../hooks/useAnalytics/useAnalytics';
12+
import { createMockUseAnalyticsHook } from '../../../../../util/test/analyticsMock';
1113

1214
import usePoolStakedDeposit from '../../hooks/usePoolStakedDeposit';
1315
import { GasImpactModalRouteParams } from './GasImpactModal.types';
@@ -59,26 +61,7 @@ jest.mock('../../hooks/usePoolStakedDeposit', () => ({
5961
default: jest.fn(),
6062
}));
6163

62-
jest.mock('../../../../hooks/useMetrics', () => ({
63-
useMetrics: () => ({
64-
trackEvent: jest.fn(),
65-
createEventBuilder: jest.fn().mockReturnValue({
66-
addProperties: jest.fn().mockReturnThis(),
67-
build: jest.fn().mockReturnValue({}),
68-
}),
69-
}),
70-
MetaMetricsEvents: {
71-
STAKE_TRANSACTION_APPROVED: 'STAKE_TRANSACTION_APPROVED',
72-
STAKE_TRANSACTION_REJECTED: 'STAKE_TRANSACTION_REJECTED',
73-
STAKE_TRANSACTION_CONFIRMED: 'STAKE_TRANSACTION_CONFIRMED',
74-
STAKE_TRANSACTION_FAILED: 'STAKE_TRANSACTION_FAILED',
75-
STAKE_TRANSACTION_SUBMITTED: 'STAKE_TRANSACTION_SUBMITTED',
76-
STAKE_GAS_COST_IMPACT_CANCEL_CLICKED:
77-
'STAKE_GAS_COST_IMPACT_CANCEL_CLICKED',
78-
STAKE_GAS_COST_IMPACT_PROCEEDED_CLICKED:
79-
'STAKE_GAS_COST_IMPACT_PROCEEDED_CLICKED',
80-
},
81-
}));
64+
jest.mock('../../../../hooks/useAnalytics/useAnalytics');
8265

8366
const initialMetrics: Metrics = {
8467
frame: { x: 0, y: 0, width: 320, height: 640 },
@@ -104,6 +87,8 @@ describe('GasImpactModal', () => {
10487
beforeEach(() => {
10588
jest.clearAllMocks();
10689

90+
jest.mocked(useAnalytics).mockReturnValue(createMockUseAnalyticsHook());
91+
10792
usePoolStakedDepositMock.mockReturnValue({
10893
attemptDepositTransaction: jest.fn(),
10994
});

app/components/UI/Stake/components/GasImpactModal/index.tsx

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,8 @@ import { useStyles } from '../../../../hooks/useStyles';
2626
import Routes from '../../../../../constants/navigation/Routes';
2727
import { GasImpactModalRouteParams } from './GasImpactModal.types';
2828
import { strings } from '../../../../../../locales/i18n';
29-
import { MetaMetricsEvents, useMetrics } from '../../../../hooks/useMetrics';
29+
import { useAnalytics } from '../../../../hooks/useAnalytics/useAnalytics';
30+
import { MetaMetricsEvents } from '../../../../../core/Analytics';
3031
import { EVENT_LOCATIONS, EVENT_PROVIDERS } from '../../constants/events';
3132
import usePoolStakedDeposit from '../../hooks/usePoolStakedDeposit';
3233
import { EVM_SCOPE } from '../../../Earn/constants/networks';
@@ -43,7 +44,7 @@ const GasImpactModal = () => {
4344
);
4445
const { navigate } = useNavigation();
4546

46-
const { trackEvent, createEventBuilder } = useMetrics();
47+
const { trackEvent, createEventBuilder } = useAnalytics();
4748

4849
const sheetRef = useRef<BottomSheetRef>(null);
4950

app/components/UI/Stake/components/StakingBalance/StakingBanners/ClaimBanner/ClaimBanner.test.tsx

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,9 @@ import { act, fireEvent } from '@testing-library/react-native';
44
import React from 'react';
55
import Engine from '../../../../../../../core/Engine';
66
import { createMockAccountsControllerState } from '../../../../../../../util/test/accountsControllerTestUtils';
7+
import { useAnalytics } from '../../../../../../hooks/useAnalytics/useAnalytics';
78
import { backgroundState } from '../../../../../../../util/test/initial-root-state';
9+
import { createMockUseAnalyticsHook } from '../../../../../../../util/test/analyticsMock';
810
import { mockNetworkState } from '../../../../../../../util/test/network';
911
import renderWithProvider, {
1012
DeepPartial,
@@ -86,6 +88,7 @@ jest.mock('../../../../hooks/usePoolStakedClaim', () => ({
8688
attemptPoolStakedClaimTransaction: mockAttemptPoolStakedClaimTransaction,
8789
}),
8890
}));
91+
jest.mock('../../../../../../hooks/useAnalytics/useAnalytics');
8992

9093
const mockNavigate = jest.fn();
9194
const noop = () => undefined;
@@ -103,6 +106,7 @@ jest.mock('@react-navigation/native', () => ({
103106
describe('ClaimBanner', () => {
104107
beforeEach(() => {
105108
jest.clearAllMocks();
109+
jest.mocked(useAnalytics).mockReturnValue(createMockUseAnalyticsHook());
106110
mockNavigate.mockClear();
107111
(useStakingChain as jest.Mock).mockReturnValue({
108112
isStakingSupportedChain: true,

app/components/UI/Stake/components/StakingBalance/StakingBanners/ClaimBanner/ClaimBanner.tsx

Lines changed: 3 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -19,10 +19,8 @@ import Routes from '../../../../../../../constants/navigation/Routes';
1919
import Engine from '../../../../../../../core/Engine';
2020
import { selectSelectedInternalAccountByScope } from '../../../../../../../selectors/multichainAccounts/accounts';
2121

22-
import {
23-
MetaMetricsEvents,
24-
useMetrics,
25-
} from '../../../../../../hooks/useMetrics';
22+
import { useAnalytics } from '../../../../../../hooks/useAnalytics/useAnalytics';
23+
import { MetaMetricsEvents } from '../../../../../../../core/Analytics';
2624
import { EVENT_LOCATIONS } from '../../../../constants/events';
2725
import usePooledStakes from '../../../../hooks/usePooledStakes';
2826
import usePoolStakedClaim from '../../../../hooks/usePoolStakedClaim';
@@ -42,7 +40,7 @@ type StakeBannerProps = Pick<BannerProps, 'style'> & {
4240

4341
const ClaimBanner = ({ claimableAmount, asset, style }: StakeBannerProps) => {
4442
const { styles } = useStyles(styleSheet, {});
45-
const { trackEvent, createEventBuilder } = useMetrics();
43+
const { trackEvent, createEventBuilder } = useAnalytics();
4644
const [isSubmittingClaimTransaction, setIsSubmittingClaimTransaction] =
4745
useState(false);
4846
const { MultichainNetworkController } = Engine.context;

app/components/UI/Stake/components/StakingBalance/StakingCta/StakingCta.test.tsx

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
import React from 'react';
22
import { screen, render } from '@testing-library/react-native';
3+
import { useAnalytics } from '../../../../../hooks/useAnalytics/useAnalytics';
4+
import { createMockUseAnalyticsHook } from '../../../../../../util/test/analyticsMock';
35
import StakingCta from './StakingCta';
46

57
const mockNavigate = jest.fn();
@@ -13,7 +15,14 @@ jest.mock('@react-navigation/native', () => {
1315
}),
1416
};
1517
});
18+
jest.mock('../../../../../hooks/useAnalytics/useAnalytics');
19+
1620
describe('StakingCta', () => {
21+
beforeEach(() => {
22+
jest.clearAllMocks();
23+
jest.mocked(useAnalytics).mockReturnValue(createMockUseAnalyticsHook());
24+
});
25+
1726
it('render matches snapshot', () => {
1827
render(<StakingCta chainId="0x1" estimatedRewardRate="2.6%" />);
1928
expect(screen.toJSON()).toMatchSnapshot();

app/components/UI/Stake/components/StakingBalance/StakingCta/StakingCta.tsx

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,8 @@ import Text, {
88
import { strings } from '../../../../../../../locales/i18n';
99
import { useNavigation } from '@react-navigation/native';
1010
import Routes from '../../../../../../constants/navigation/Routes';
11-
import { MetaMetricsEvents, useMetrics } from '../../../../../hooks/useMetrics';
11+
import { useAnalytics } from '../../../../../hooks/useAnalytics/useAnalytics';
12+
import { MetaMetricsEvents } from '../../../../../../core/Analytics';
1213
import { EVENT_LOCATIONS, EVENT_PROVIDERS } from '../../../constants/events';
1314
import { Hex } from 'viem/_types/types/misc';
1415
import { trace, TraceName } from '../../../../../../util/trace';
@@ -26,7 +27,7 @@ const StakingCta = ({
2627
}: StakingCtaProps) => {
2728
const { styles } = useStyles(styleSheet, {});
2829
const { navigate } = useNavigation();
29-
const { trackEvent, createEventBuilder } = useMetrics();
30+
const { trackEvent, createEventBuilder } = useAnalytics();
3031

3132
const navigateToLearnMoreModal = () => {
3233
trace({

0 commit comments

Comments
 (0)