Skip to content

Commit 667be81

Browse files
feat: MUSD-71 add segment events to musd conversion workflow (#24457)
<!-- 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** Adds first set of events for the mUSD conversion flow. ### Events Added - `MUSD_CONVERSION_CTA_CLICKED` - `MUSD_FULLSCREEN_ANNOUNCEMENT_DISPLAYED` - `MUSD_FULLSCREEN_ANNOUNCEMENT_BUTTON_CLICKED` - `MUSD_CONVERSION_STATUS_UPDATED` <!-- 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? 3. What is the improvement/solution? --> ## **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` If this PR is End-User-Facing, please write a short User-Facing description in the past tense like: `CHANGELOG entry: Added a new tab for users to see their NFTs` `CHANGELOG entry: Fixed a bug that was causing some NFTs to flicker` (This helps the Release Engineer do their job more quickly and accurately) --> CHANGELOG entry: add first iteration of mUSD conversion segment events ## **Related issues** Fixes: [MUSD-71: Add segment events to mobile mUSD conversion workflow](https://consensyssoftware.atlassian.net/browse/MUSD-71) ## **Manual testing steps** ```gherkin Feature: mUSD conversion analytics tracking Scenario: user initiates mUSD conversion from a wallet CTA Given user is eligible to see a "Get/Convert to mUSD" call-to-action in the wallet When user taps the mUSD conversion call-to-action Then user is routed into the mUSD conversion flow (or the education screen on first view) And the app records the CTA click and subsequent conversion transaction status updates Scenario: user views the mUSD conversion education announcement and continues Given user is shown the mUSD conversion education screen When the user views the screen and taps a primary or secondary button Then the app records the announcement display and the button click And the user continues to the next step (continue) or returns (go back) ``` ## **Screenshots/Recordings** <!-- If applicable, add screenshots and/or recordings to visualize the before and after of your change. --> ### **Before** <!-- [screenshots/recordings] --> N/A ### **After** <!-- [screenshots/recordings] --> 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. <!-- CURSOR_SUMMARY --> --- > [!NOTE] > Introduces first set of analytics for the mUSD conversion experience and wires them across UI and transaction lifecycle. > > - Adds new MetaMetrics events: `MUSD_CONVERSION_CTA_CLICKED`, `MUSD_FULLSCREEN_ANNOUNCEMENT_DISPLAYED`, `MUSD_FULLSCREEN_ANNOUNCEMENT_BUTTON_CLICKED`, `MUSD_CONVERSION_STATUS_UPDATED` > - Implements tracking in `EarnMusdConversionEducationView` (screen viewed, primary/secondary button clicks), `MusdConversionAssetListCta`, `MusdConversionAssetOverviewCta`, and `TokenListItem` CTAs > - Tracks conversion transaction status in `useMusdConversionStatus` (approved/confirmed/failed), including token, amount, chain, and network name > - Introduces `MUSD_EVENTS_CONSTANTS` and centralizes event locations/providers under `earn/constants/events`, updating imports accordingly > - Minor hook updates (`useMusdBalance` memoization) and Earn views import fixes; no UX changes > - Adds extensive unit tests validating event emission across views/hooks/CTAs > > <sup>Written by [Cursor Bugbot](https://cursor.com/dashboard?tab=bugbot) for commit 9857a7e. This will update automatically on new commits. Configure [here](https://cursor.com/dashboard?tab=bugbot).</sup> <!-- /CURSOR_SUMMARY --> --------- Co-authored-by: Nicholas Smith <nick.smith@consensys.net>
1 parent 79d4322 commit 667be81

24 files changed

Lines changed: 1150 additions & 46 deletions

File tree

app/components/UI/Earn/Views/EarnInputView/EarnInputView.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,7 @@ import { getStakingNavbar } from '../../../Navbar';
4242
import ScreenLayout from '../../../Ramp/Aggregator/components/ScreenLayout';
4343
import QuickAmounts from '../../../Stake/components/QuickAmounts';
4444
import { EVENT_PROVIDERS } from '../../../Stake/constants/events';
45-
import { EVENT_LOCATIONS } from '../../constants/events';
45+
import { EVENT_LOCATIONS } from '../../constants/events/earnEvents';
4646
import usePoolStakedDeposit from '../../../Stake/hooks/usePoolStakedDeposit';
4747
import EarnTokenSelector from '../../components/EarnTokenSelector';
4848
import InputDisplay from '../../components/InputDisplay';

app/components/UI/Earn/Views/EarnLendingDepositConfirmationView/index.tsx

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,10 @@ import { EARN_EXPERIENCES } from '../../constants/experiences';
3636
import { RootState } from '../../../../../reducers';
3737
import { selectNetworkConfigurationByChainId } from '../../../../../selectors/networkController';
3838
import { IMetaMetricsEvent } from '../../../../../core/Analytics';
39-
import { EVENT_LOCATIONS, EVENT_PROVIDERS } from '../../constants/events';
39+
import {
40+
EVENT_LOCATIONS,
41+
EVENT_PROVIDERS,
42+
} from '../../constants/events/earnEvents';
4043
import { ProgressStep } from './components/ProgressStepper';
4144
import BN from 'bnjs4';
4245
import { endTrace, trace, TraceName } from '../../../../../util/trace';

app/components/UI/Earn/Views/EarnLendingWithdrawalConfirmationView/index.tsx

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,10 @@ import { getStakingNavbar } from '../../../Navbar';
3535
import AccountTag from '../../../Stake/components/StakingConfirmation/AccountTag/AccountTag';
3636
import ContractTag from '../../../Stake/components/StakingConfirmation/ContractTag/ContractTag';
3737
import { TokenI } from '../../../Tokens/types';
38-
import { EVENT_LOCATIONS, EVENT_PROVIDERS } from '../../constants/events';
38+
import {
39+
EVENT_LOCATIONS,
40+
EVENT_PROVIDERS,
41+
} from '../../constants/events/earnEvents';
3942
import { EARN_EXPERIENCES } from '../../constants/experiences';
4043
import useEarnToken from '../../hooks/useEarnToken';
4144
import { EarnTokenDetails } from '../../types/lending.types';

app/components/UI/Earn/Views/EarnMusdConversionEducationView/index.test.tsx

Lines changed: 152 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,12 @@ import { strings } from '../../../../../../locales/i18n';
1414
import { useMusdConversion } from '../../hooks/useMusdConversion';
1515
import { useParams } from '../../../../../util/navigation/navUtils';
1616

17+
const FIXED_NOW_MS = 1730000000000;
18+
const mockTrackEvent = jest.fn();
19+
const mockCreateEventBuilder = jest.fn();
20+
const mockAddProperties = jest.fn();
21+
const mockBuild = jest.fn();
22+
1723
jest.mock('@react-navigation/native', () => {
1824
const actualNav = jest.requireActual('@react-navigation/native');
1925
return {
@@ -47,6 +53,17 @@ jest.mock('../../hooks/useMusdConversion', () => ({
4753
useMusdConversion: jest.fn(),
4854
}));
4955

56+
jest.mock('../../../../hooks/useMetrics', () => {
57+
const actual = jest.requireActual('../../../../hooks/useMetrics');
58+
return {
59+
...actual,
60+
useMetrics: () => ({
61+
trackEvent: mockTrackEvent,
62+
createEventBuilder: mockCreateEventBuilder,
63+
}),
64+
};
65+
});
66+
5067
jest.mock('../../../../../util/Logger', () => ({
5168
error: jest.fn(),
5269
}));
@@ -88,6 +105,7 @@ describe('EarnMusdConversionEducationView', () => {
88105
setOptions: jest.fn(),
89106
navigate: jest.fn(),
90107
goBack: jest.fn(),
108+
canGoBack: jest.fn(() => true),
91109
};
92110

93111
const mockRouteParams = {
@@ -101,6 +119,8 @@ describe('EarnMusdConversionEducationView', () => {
101119
beforeEach(() => {
102120
jest.clearAllMocks();
103121

122+
jest.spyOn(Date, 'now').mockReturnValue(FIXED_NOW_MS);
123+
104124
mockUseDispatch.mockReturnValue(mockDispatch);
105125
// @ts-expect-error - partial mock of navigation is sufficient for testing
106126
mockUseNavigation.mockReturnValue(mockNavigation);
@@ -119,9 +139,16 @@ describe('EarnMusdConversionEducationView', () => {
119139
seen,
120140
},
121141
}));
142+
143+
mockBuild.mockReturnValue({ name: 'mock-built-event' });
144+
mockAddProperties.mockImplementation(() => ({ build: mockBuild }));
145+
mockCreateEventBuilder.mockImplementation(() => ({
146+
addProperties: mockAddProperties,
147+
}));
122148
});
123149

124150
afterEach(() => {
151+
jest.restoreAllMocks();
125152
jest.resetAllMocks();
126153
});
127154

@@ -249,6 +276,131 @@ describe('EarnMusdConversionEducationView', () => {
249276
});
250277
});
251278

279+
describe('MetaMetrics', () => {
280+
it('tracks fullscreen announcement displayed event once per visit', () => {
281+
const { MetaMetricsEvents } = jest.requireActual(
282+
'../../../../hooks/useMetrics',
283+
);
284+
285+
const { unmount } = renderWithProvider(
286+
<EarnMusdConversionEducationView />,
287+
{ state: {} },
288+
);
289+
290+
expect(mockCreateEventBuilder).toHaveBeenCalledTimes(1);
291+
expect(mockCreateEventBuilder).toHaveBeenCalledWith(
292+
MetaMetricsEvents.MUSD_FULLSCREEN_ANNOUNCEMENT_DISPLAYED,
293+
);
294+
295+
expect(mockAddProperties).toHaveBeenCalledTimes(1);
296+
expect(mockAddProperties).toHaveBeenCalledWith({
297+
location: 'conversion_education_screen',
298+
});
299+
300+
expect(mockTrackEvent).toHaveBeenCalledTimes(1);
301+
expect(mockTrackEvent).toHaveBeenCalledWith({
302+
name: 'mock-built-event',
303+
});
304+
305+
unmount();
306+
307+
renderWithProvider(<EarnMusdConversionEducationView />, { state: {} });
308+
309+
expect(mockCreateEventBuilder).toHaveBeenCalledTimes(2);
310+
expect(mockCreateEventBuilder).toHaveBeenNthCalledWith(
311+
2,
312+
MetaMetricsEvents.MUSD_FULLSCREEN_ANNOUNCEMENT_DISPLAYED,
313+
);
314+
315+
expect(mockAddProperties).toHaveBeenCalledTimes(2);
316+
expect(mockAddProperties).toHaveBeenNthCalledWith(2, {
317+
location: 'conversion_education_screen',
318+
});
319+
320+
expect(mockTrackEvent).toHaveBeenCalledTimes(2);
321+
expect(mockTrackEvent).toHaveBeenNthCalledWith(2, {
322+
name: 'mock-built-event',
323+
});
324+
});
325+
326+
it('tracks fullscreen announcement button clicked event when continue button is pressed', async () => {
327+
const { MetaMetricsEvents } = jest.requireActual(
328+
'../../../../hooks/useMetrics',
329+
);
330+
331+
const { getByText } = renderWithProvider(
332+
<EarnMusdConversionEducationView />,
333+
{ state: {} },
334+
);
335+
336+
mockTrackEvent.mockClear();
337+
mockCreateEventBuilder.mockClear();
338+
mockAddProperties.mockClear();
339+
mockBuild.mockClear();
340+
341+
await act(async () => {
342+
fireEvent.press(
343+
getByText(strings('earn.musd_conversion.education.primary_button')),
344+
);
345+
});
346+
347+
await waitFor(() => {
348+
expect(mockCreateEventBuilder).toHaveBeenCalledTimes(1);
349+
expect(mockCreateEventBuilder).toHaveBeenCalledWith(
350+
MetaMetricsEvents.MUSD_FULLSCREEN_ANNOUNCEMENT_BUTTON_CLICKED,
351+
);
352+
353+
expect(mockAddProperties).toHaveBeenCalledTimes(1);
354+
expect(mockAddProperties).toHaveBeenCalledWith({
355+
location: 'conversion_education_screen',
356+
button_type: 'primary',
357+
button_text: strings('earn.musd_conversion.education.primary_button'),
358+
redirects_to: 'custom_amount_screen',
359+
});
360+
361+
expect(mockTrackEvent).toHaveBeenCalledTimes(1);
362+
expect(mockTrackEvent).toHaveBeenCalledWith({
363+
name: 'mock-built-event',
364+
});
365+
});
366+
});
367+
368+
it('tracks fullscreen announcement button clicked event when go back button is pressed', () => {
369+
const { MetaMetricsEvents } = jest.requireActual(
370+
'../../../../hooks/useMetrics',
371+
);
372+
373+
const { getByText } = renderWithProvider(
374+
<EarnMusdConversionEducationView />,
375+
{ state: {} },
376+
);
377+
378+
mockTrackEvent.mockClear();
379+
mockCreateEventBuilder.mockClear();
380+
mockAddProperties.mockClear();
381+
mockBuild.mockClear();
382+
383+
fireEvent.press(
384+
getByText(strings('earn.musd_conversion.education.secondary_button')),
385+
);
386+
387+
expect(mockCreateEventBuilder).toHaveBeenCalledTimes(1);
388+
expect(mockCreateEventBuilder).toHaveBeenCalledWith(
389+
MetaMetricsEvents.MUSD_FULLSCREEN_ANNOUNCEMENT_BUTTON_CLICKED,
390+
);
391+
392+
expect(mockAddProperties).toHaveBeenCalledTimes(1);
393+
expect(mockAddProperties).toHaveBeenCalledWith({
394+
location: 'conversion_education_screen',
395+
button_type: 'secondary',
396+
button_text: strings('earn.musd_conversion.education.secondary_button'),
397+
});
398+
399+
expect(mockTrackEvent).toHaveBeenCalledTimes(1);
400+
expect(mockTrackEvent).toHaveBeenCalledWith({ name: 'mock-built-event' });
401+
});
402+
});
403+
252404
describe('error handling', () => {
253405
it('logs error when initiateConversion throws error', async () => {
254406
const testError = new Error('Conversion failed');

app/components/UI/Earn/Views/EarnMusdConversionEducationView/index.tsx

Lines changed: 79 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import React, { useCallback, useMemo } from 'react';
1+
import React, { useCallback, useEffect, useMemo, useRef } from 'react';
22
import { Hex } from '@metamask/utils';
33
import { useDispatch } from 'react-redux';
44
import { View, Image, useColorScheme } from 'react-native';
@@ -25,7 +25,8 @@ import {
2525
ButtonVariant as DesignSystemButtonVariant,
2626
} from '@metamask/design-system-react-native';
2727
import { strings } from '../../../../../../locales/i18n';
28-
28+
import { MetaMetricsEvents, useMetrics } from '../../../../hooks/useMetrics';
29+
import { MUSD_EVENTS_CONSTANTS } from '../../constants/events';
2930
interface EarnMusdConversionEducationViewRouteParams {
3031
/**
3132
* The payment token to preselect in the confirmation screen
@@ -58,6 +59,8 @@ const EarnMusdConversionEducationView = () => {
5859

5960
const colorScheme = useColorScheme();
6061

62+
const { trackEvent, createEventBuilder } = useMetrics();
63+
6164
const backgroundImage = useMemo(
6265
() =>
6366
colorScheme === 'dark'
@@ -66,8 +69,74 @@ const EarnMusdConversionEducationView = () => {
6669
[colorScheme],
6770
);
6871

72+
const { BUTTON_TYPES, EVENT_LOCATIONS } = MUSD_EVENTS_CONSTANTS;
73+
74+
const submitScreenViewedEvent = useCallback(() => {
75+
trackEvent(
76+
createEventBuilder(
77+
MetaMetricsEvents.MUSD_FULLSCREEN_ANNOUNCEMENT_DISPLAYED,
78+
)
79+
.addProperties({
80+
location: EVENT_LOCATIONS.CONVERSION_EDUCATION_SCREEN,
81+
})
82+
.build(),
83+
);
84+
}, [
85+
EVENT_LOCATIONS.CONVERSION_EDUCATION_SCREEN,
86+
createEventBuilder,
87+
trackEvent,
88+
]);
89+
90+
const hasSubmittedScreenViewedEventRef = useRef(false);
91+
92+
useEffect(() => {
93+
if (hasSubmittedScreenViewedEventRef.current) {
94+
return;
95+
}
96+
hasSubmittedScreenViewedEventRef.current = true;
97+
submitScreenViewedEvent();
98+
}, [submitScreenViewedEvent]);
99+
100+
const submitContinuePressedEvent = useCallback(() => {
101+
trackEvent(
102+
createEventBuilder(
103+
MetaMetricsEvents.MUSD_FULLSCREEN_ANNOUNCEMENT_BUTTON_CLICKED,
104+
)
105+
.addProperties({
106+
location: EVENT_LOCATIONS.CONVERSION_EDUCATION_SCREEN,
107+
button_type: BUTTON_TYPES.PRIMARY,
108+
button_text: strings('earn.musd_conversion.education.primary_button'),
109+
redirects_to: EVENT_LOCATIONS.CUSTOM_AMOUNT_SCREEN, // Redirects to custom amount screen.
110+
})
111+
.build(),
112+
);
113+
}, [
114+
trackEvent,
115+
createEventBuilder,
116+
EVENT_LOCATIONS.CONVERSION_EDUCATION_SCREEN,
117+
EVENT_LOCATIONS.CUSTOM_AMOUNT_SCREEN,
118+
BUTTON_TYPES.PRIMARY,
119+
]);
120+
121+
const submitGoBackPressedEvent = () => {
122+
trackEvent(
123+
createEventBuilder(
124+
MetaMetricsEvents.MUSD_FULLSCREEN_ANNOUNCEMENT_BUTTON_CLICKED,
125+
)
126+
.addProperties({
127+
location: EVENT_LOCATIONS.CONVERSION_EDUCATION_SCREEN,
128+
button_type: BUTTON_TYPES.SECONDARY,
129+
button_text: strings(
130+
'earn.musd_conversion.education.secondary_button',
131+
),
132+
})
133+
.build(),
134+
);
135+
};
136+
69137
const handleContinue = useCallback(async () => {
70138
try {
139+
submitContinuePressedEvent();
71140
// Mark education as seen so it won't show again
72141
dispatch(setMusdConversionEducationSeen(true));
73142

@@ -91,9 +160,16 @@ const EarnMusdConversionEducationView = () => {
91160
'[mUSD Conversion Education] Failed to initiate conversion',
92161
);
93162
}
94-
}, [dispatch, initiateConversion, outputChainId, preferredPaymentToken]);
163+
}, [
164+
dispatch,
165+
initiateConversion,
166+
outputChainId,
167+
preferredPaymentToken,
168+
submitContinuePressedEvent,
169+
]);
95170

96171
const handleGoBack = () => {
172+
submitGoBackPressedEvent();
97173
if (navigation.canGoBack()) {
98174
navigation.goBack();
99175
}

app/components/UI/Earn/Views/EarnWithdrawInputView/EarnWithdrawInputView.tsx

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,10 @@ import useEarnWithdrawInput from '../../../Earn/hooks/useEarnWithdrawInput';
3434
import { getStakingNavbar } from '../../../Navbar';
3535
import ScreenLayout from '../../../Ramp/Aggregator/components/ScreenLayout';
3636
import QuickAmounts from '../../../Stake/components/QuickAmounts';
37-
import { EVENT_LOCATIONS, EVENT_PROVIDERS } from '../../constants/events';
37+
import {
38+
EVENT_LOCATIONS,
39+
EVENT_PROVIDERS,
40+
} from '../../constants/events/earnEvents';
3841
import usePoolStakedUnstake from '../../../Stake/hooks/usePoolStakedUnstake';
3942
import { StakeNavigationParamsList } from '../../../Stake/types';
4043
import EarnTokenSelector from '../../components/EarnTokenSelector';

app/components/UI/Earn/components/Earnings/index.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ import { useStyles } from '../../../../../component-library/hooks';
1919
import Routes from '../../../../../constants/navigation/Routes';
2020
import { MetaMetricsEvents } from '../../../../hooks/useMetrics';
2121
import EarnMaintenanceBanner from '../../../Earn/components/EarnMaintenanceBanner';
22-
import { EVENT_LOCATIONS } from '../../../Earn/constants/events';
22+
import { EVENT_LOCATIONS } from '../../constants/events/earnEvents';
2323
import useEarnings from '../../../Earn/hooks/useEarnings';
2424
import {
2525
selectPooledStakingServiceInterruptionBannerEnabledFlag,

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

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,10 @@ import { strings } from '../../../../../../locales/i18n';
99
import { act, fireEvent } from '@testing-library/react-native';
1010
import { MetaMetricsEvents, useMetrics } from '../../../../hooks/useMetrics';
1111
import { MetricsEventBuilder } from '../../../../../core/Analytics/MetricsEventBuilder';
12-
import { EVENT_LOCATIONS, EVENT_PROVIDERS } from '../../constants/events';
12+
import {
13+
EVENT_LOCATIONS,
14+
EVENT_PROVIDERS,
15+
} from '../../constants/events/earnEvents';
1316
import {
1417
selectPooledStakingEnabledFlag,
1518
selectStablecoinLendingEnabledFlag,

app/components/UI/Earn/components/EmptyStateCta/index.tsx

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,10 @@ import { getDecimalChainId } from '../../../../../util/networks';
2121
import { MetaMetricsEvents, useMetrics } from '../../../../hooks/useMetrics';
2222
import { useStyles } from '../../../../hooks/useStyles';
2323
import { TokenI } from '../../../Tokens/types';
24-
import { EVENT_LOCATIONS, EVENT_PROVIDERS } from '../../constants/events';
24+
import {
25+
EVENT_LOCATIONS,
26+
EVENT_PROVIDERS,
27+
} from '../../constants/events/earnEvents';
2528
import { selectStablecoinLendingEnabledFlag } from '../../selectors/featureFlags';
2629
import { parseFloatSafe } from '../../utils/number';
2730
import styleSheet from './EmptyStateCta.styles';

0 commit comments

Comments
 (0)