Skip to content

Commit cc1f719

Browse files
refactor(analytics): migrate Batch 3-2: mobile-platform (MetaMask#26315)
## **Description** Phase 3 analytics migration (Batch 3-2): migrate Security's SecureKeychain, ProtectYourWalletModal, and RevealPrivateCredential from MetaMetrics to the new analytics system. **Reason**: Deprecate MetaMetrics in favour of the shared analytics utility and AnalyticsController. **Changes**: - `SecureKeychain.ts` now uses `analytics.trackEvent()` and `analytics.identify()` from `app/util/analytics` instead of `MetaMetrics.getInstance().trackEvent()` and `MetaMetrics.getInstance().addTraitsToUser()`; uses `AnalyticsEventBuilder` instead of `MetricsEventBuilder`. - `ProtectYourWalletModal` now uses `withAnalyticsAwareness` HOC from `app/components/hooks/useAnalytics` instead of `withMetricsAwareness` from `useMetrics`; prop renamed from `metrics` to `analytics`. - `RevealPrivateCredential` now uses `useAnalytics` hook instead of `useMetrics`. - Test mocks updated to mock the analytics utility and `useAnalytics` instead of MetaMetrics and `useMetrics`. ## **Changelog** CHANGELOG entry: null ## **Related issues** Fixes: https://consensyssoftware.atlassian.net/browse/MCWP-301 (Batch 3-2) ## **Manual testing steps** ```gherkin Feature: Security analytics Scenario: user triggers a security flow event Given app is open and user is in a security flow When user performs an action that triggers analytics (e.g. reveal SRP, protect wallet modal interaction, biometric setup) Then the event is tracked on Mixpanel ``` ## **Screenshots/Recordings** N/A – analytics migration, no UI change. ## **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] > **Medium Risk** > Touches security-adjacent flows (keychain credential storage and SRP/wallet-protection tracking) and changes analytics plumbing, so regressions could silently drop events or mis-set user traits, though app behavior should remain unchanged. > > **Overview** > Migrates security analytics instrumentation away from legacy `MetaMetrics`/`useMetrics` to the shared analytics stack. > > `SecureKeychain` now uses `analytics.trackEvent()`/`analytics.identify()` plus `AnalyticsEventBuilder` (replacing `MetaMetrics.getInstance()` and `MetricsEventBuilder`) for hardware-keystore event tracking and authentication-type user trait updates. `ProtectYourWalletModal` and `RevealPrivateCredential` swap `withMetricsAwareness`/`useMetrics` for `withAnalyticsAwareness`/`useAnalytics`, including the prop rename from `metrics` to `analytics`. > > Unit tests are updated to mock the new analytics utility/hooks and to assert calls against `analytics.identify`/event builder usage instead of MetaMetrics mocks. > > <sup>Written by [Cursor Bugbot](https://cursor.com/dashboard?tab=bugbot) for commit eb1430f. This will update automatically on new commits. Configure [here](https://cursor.com/dashboard?tab=bugbot).</sup> <!-- /CURSOR_SUMMARY -->
1 parent f4cedca commit cc1f719

6 files changed

Lines changed: 60 additions & 50 deletions

File tree

app/components/UI/ProtectYourWalletModal/index.js

Lines changed: 9 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -8,11 +8,11 @@ import { protectWalletModalNotVisible } from '../../../actions/user';
88
import Icon from 'react-native-vector-icons/FontAwesome';
99
import { strings } from '../../../../locales/i18n';
1010
import scaling from '../../../util/scaling';
11-
import { MetaMetricsEvents } from '../../../core/Analytics';
11+
import { MetaMetricsEvents } from '../../../core/Analytics/MetaMetrics.events';
1212

1313
import { ThemeContext, mockTheme } from '../../../util/theme';
1414
import { ProtectWalletModalSelectorsIDs } from './ProtectWalletModal.testIds';
15-
import { withMetricsAwareness } from '../../../components/hooks/useMetrics';
15+
import { withAnalyticsAwareness } from '../../../components/hooks/useAnalytics/withAnalyticsAwareness';
1616
import { selectSeedlessOnboardingLoginFlow } from '../../../selectors/seedlessOnboardingController';
1717

1818
const protectWalletImage = require('../../../images/explain-backup-seedphrase.png'); // eslint-disable-line
@@ -91,9 +91,9 @@ class ProtectYourWalletModal extends PureComponent {
9191
*/
9292
passwordSet: PropTypes.bool,
9393
/**
94-
* Metrics injected by withMetricsAwareness HOC
94+
* Analytics injected by withAnalyticsAwareness HOC
9595
*/
96-
metrics: PropTypes.object,
96+
analytics: PropTypes.object,
9797
/**
9898
* A boolean representing if the user is in the seedless onboarding login flow
9999
*/
@@ -106,8 +106,8 @@ class ProtectYourWalletModal extends PureComponent {
106106
'SetPasswordFlow',
107107
this.props.passwordSet ? { screen: 'AccountBackupStep1' } : undefined,
108108
);
109-
this.props.metrics.trackEvent(
110-
this.props.metrics
109+
this.props.analytics.trackEvent(
110+
this.props.analytics
111111
.createEventBuilder(MetaMetricsEvents.WALLET_SECURITY_PROTECT_ENGAGED)
112112
.addProperties({
113113
wallet_protection_required: false,
@@ -130,8 +130,8 @@ class ProtectYourWalletModal extends PureComponent {
130130

131131
onDismiss = () => {
132132
this.props.protectWalletModalNotVisible();
133-
this.props.metrics.trackEvent(
134-
this.props.metrics
133+
this.props.analytics.trackEvent(
134+
this.props.analytics
135135
.createEventBuilder(MetaMetricsEvents.WALLET_SECURITY_PROTECT_DISMISSED)
136136
.addProperties({
137137
wallet_protection_required: false,
@@ -222,4 +222,4 @@ ProtectYourWalletModal.contextType = ThemeContext;
222222
export default connect(
223223
mapStateToProps,
224224
mapDispatchToProps,
225-
)(withMetricsAwareness(ProtectYourWalletModal));
225+
)(withAnalyticsAwareness(ProtectYourWalletModal));

app/components/UI/ProtectYourWalletModal/index.test.tsx

Lines changed: 22 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -39,28 +39,34 @@ jest.mock('../../../util/analytics/analytics', () => ({
3939
},
4040
}));
4141

42-
// Mock useMetrics hook which is used by withMetricsAwareness HOC
43-
jest.mock('../../../components/hooks/useMetrics', () => ({
44-
useMetrics: () => ({
42+
// Mock useAnalytics hook which is used by withAnalyticsAwareness HOC
43+
jest.mock('../../../components/hooks/useAnalytics/useAnalytics', () => ({
44+
useAnalytics: () => ({
4545
trackEvent: mockTrackEvent,
4646
createEventBuilder: mockCreateEventBuilder,
4747
isEnabled: mockMetricsIsEnabled,
4848
}),
49-
withMetricsAwareness:
50-
(Component: React.ComponentType) => (props: Record<string, unknown>) => (
51-
<Component
52-
{...props}
53-
{...({
54-
metrics: {
55-
trackEvent: mockTrackEvent,
56-
createEventBuilder: mockCreateEventBuilder,
57-
isEnabled: mockMetricsIsEnabled,
58-
},
59-
} as Record<string, unknown>)}
60-
/>
61-
),
6249
}));
6350

51+
jest.mock(
52+
'../../../components/hooks/useAnalytics/withAnalyticsAwareness',
53+
() => ({
54+
withAnalyticsAwareness:
55+
(Component: React.ComponentType) => (props: Record<string, unknown>) => (
56+
<Component
57+
{...props}
58+
{...({
59+
analytics: {
60+
trackEvent: mockTrackEvent,
61+
createEventBuilder: mockCreateEventBuilder,
62+
isEnabled: mockMetricsIsEnabled,
63+
},
64+
} as Record<string, unknown>)}
65+
/>
66+
),
67+
}),
68+
);
69+
6470
const mockStore = configureMockStore();
6571

6672
const initialState = {

app/components/Views/RevealPrivateCredential/RevealPrivateCredential.test.tsx

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ import { KeyringTypes } from '@metamask/keyring-controller';
1212
import { WRONG_PASSWORD_ERROR } from '../../../constants/error';
1313
import { ReauthenticateErrorType } from '../../../core/Authentication/types';
1414
import ClipboardManager from '../../../core/ClipboardManager';
15-
import { MetaMetricsEvents } from '../../../core/Analytics';
15+
import { MetaMetricsEvents } from '../../../core/Analytics/MetaMetrics.events';
1616
import Device from '../../../util/device';
1717

1818
const MOCK_PASSWORD = 'word1 word2 word3 word4';
@@ -109,12 +109,11 @@ jest.mock('react-redux', () => ({
109109
useDispatch: () => mockDispatch,
110110
}));
111111

112-
jest.mock('../../../components/hooks/useMetrics', () => ({
113-
useMetrics: () => ({
112+
jest.mock('../../../components/hooks/useAnalytics/useAnalytics', () => ({
113+
useAnalytics: () => ({
114114
trackEvent: mockTrackEvent,
115115
createEventBuilder: mockCreateEventBuilder,
116116
}),
117-
withMetricsAwareness: jest.fn((Component) => Component),
118117
}));
119118

120119
// Mock ClipboardManager - necessary for testing clipboard functionality

app/components/Views/RevealPrivateCredential/RevealPrivateCredential.tsx

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,7 @@ import { WRONG_PASSWORD_ERROR } from '../../../constants/error';
3535
import { KEEP_SRP_SAFE_URL, SRP_GUIDE_URL } from '../../../constants/urls';
3636
import ClipboardManager from '../../../core/ClipboardManager';
3737
import { useTheme } from '../../../util/theme';
38-
import { MetaMetricsEvents } from '../../../core/Analytics';
38+
import { MetaMetricsEvents } from '../../../core/Analytics/MetaMetrics.events';
3939
import { passwordRequirementsMet } from '../../../util/password';
4040
import useAuthentication from '../../../core/Authentication/hooks/useAuthentication';
4141
import { ReauthenticateErrorType } from '../../../core/Authentication/types';
@@ -50,7 +50,7 @@ import { getNavigationOptionsTitle } from '../../../components/UI/Navbar';
5050
import { RevealSeedViewSelectorsIDs } from './RevealSeedView.testIds';
5151

5252
import { selectSelectedInternalAccountFormattedAddress } from '../../../selectors/accountsController';
53-
import { useMetrics } from '../../../components/hooks/useMetrics';
53+
import { useAnalytics } from '../../../components/hooks/useAnalytics/useAnalytics';
5454
import {
5555
endTrace,
5656
trace,
@@ -114,7 +114,7 @@ const RevealPrivateCredential = ({
114114
const dispatch = useDispatch();
115115

116116
const theme = useTheme();
117-
const { trackEvent, createEventBuilder } = useMetrics();
117+
const { trackEvent, createEventBuilder } = useAnalytics();
118118
const { colors, themeAppearance } = theme;
119119
const styles = createStyles(theme);
120120

app/core/SecureKeychain.test.ts

Lines changed: 16 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import * as Keychain from 'react-native-keychain'; // eslint-disable-line import
33
import { UserProfileProperty } from '../util/metrics/UserSettingsAnalyticsMetaData/UserProfileAnalyticsMetaData.types';
44
import AUTHENTICATION_TYPE from '../constants/userProperties';
55
import QuickCrypto from 'react-native-quick-crypto';
6+
import { analytics } from '../util/analytics/analytics';
67

78
jest.mock('../../locales/i18n', () => ({
89
strings: jest.fn((key) => key),
@@ -42,13 +43,18 @@ jest.mock('../store/storage-wrapper', () => ({
4243
},
4344
}));
4445

45-
const mockAddTraitsToUser = jest.fn();
46-
jest.mock('../core/Analytics', () => ({
47-
MetaMetrics: {
48-
getInstance: jest.fn(() => ({
49-
addTraitsToUser: mockAddTraitsToUser,
50-
trackEvent: jest.fn(),
51-
updateDataRecordingFlag: jest.fn(),
46+
jest.mock('../util/analytics/analytics', () => ({
47+
analytics: {
48+
identify: jest.fn(),
49+
trackEvent: jest.fn(),
50+
},
51+
}));
52+
53+
jest.mock('../util/analytics/AnalyticsEventBuilder', () => ({
54+
AnalyticsEventBuilder: {
55+
createEventBuilder: jest.fn(() => ({
56+
addProperties: jest.fn().mockReturnThis(),
57+
build: jest.fn(),
5258
})),
5359
},
5460
}));
@@ -76,7 +82,7 @@ describe('SecureKeychain - setGenericPassword', () => {
7682
}),
7783
);
7884

79-
expect(mockAddTraitsToUser).toHaveBeenCalledWith(
85+
expect(analytics.identify).toHaveBeenCalledWith(
8086
expect.objectContaining({
8187
[UserProfileProperty.AUTHENTICATION_TYPE]:
8288
AUTHENTICATION_TYPE.DEVICE_AUTHENTICATION,
@@ -99,7 +105,7 @@ describe('SecureKeychain - setGenericPassword', () => {
99105
}),
100106
);
101107

102-
expect(mockAddTraitsToUser).toHaveBeenCalledWith(
108+
expect(analytics.identify).toHaveBeenCalledWith(
103109
expect.objectContaining({
104110
[UserProfileProperty.AUTHENTICATION_TYPE]:
105111
AUTHENTICATION_TYPE.DEVICE_AUTHENTICATION,
@@ -122,7 +128,7 @@ describe('SecureKeychain - setGenericPassword', () => {
122128
}),
123129
);
124130

125-
expect(mockAddTraitsToUser).toHaveBeenCalledWith(
131+
expect(analytics.identify).toHaveBeenCalledWith(
126132
expect.objectContaining({
127133
[UserProfileProperty.AUTHENTICATION_TYPE]:
128134
AUTHENTICATION_TYPE.DEVICE_AUTHENTICATION,

app/core/SecureKeychain.ts

Lines changed: 7 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -2,12 +2,12 @@ import { Platform } from 'react-native';
22
import * as Keychain from 'react-native-keychain'; // eslint-disable-line import/no-namespace
33
import { Encryptor, LEGACY_DERIVATION_OPTIONS } from './Encryptor';
44
import { strings } from '../../locales/i18n';
5-
import { MetaMetricsEvents, MetaMetrics } from './Analytics';
5+
import { MetaMetricsEvents } from './Analytics/MetaMetrics.events';
6+
import { analytics } from '../util/analytics/analytics';
7+
import { AnalyticsEventBuilder } from '../util/analytics/AnalyticsEventBuilder';
68
import Device from '../util/device';
79
import AUTHENTICATION_TYPE from '../constants/userProperties';
810
import { UserProfileProperty } from '../util/metrics/UserSettingsAnalyticsMetaData/UserProfileAnalyticsMetaData.types';
9-
import { MetricsEventBuilder } from './Analytics/MetricsEventBuilder';
10-
1111
const privates = new WeakMap();
1212
const encryptor = new Encryptor({
1313
keyDerivationOptions: LEGACY_DERIVATION_OPTIONS,
@@ -64,8 +64,8 @@ const SecureKeychain = {
6464
instance = SecureKeychainEncryptor.getInstance(salt);
6565

6666
if (Device.isAndroid() && Keychain.SECURITY_LEVEL?.SECURE_HARDWARE)
67-
MetaMetrics.getInstance().trackEvent(
68-
MetricsEventBuilder.createEventBuilder(
67+
analytics.trackEvent(
68+
AnalyticsEventBuilder.createEventBuilder(
6969
MetaMetricsEvents.ANDROID_HARDWARE_KEYSTORE,
7070
).build(),
7171
);
@@ -139,7 +139,7 @@ const SecureKeychain = {
139139
async resetGenericPassword() {
140140
const options = { service: defaultCredentialsOptions.service };
141141
// This is called to remove other auth types and set the user back to the default password login
142-
await MetaMetrics.getInstance().addTraitsToUser({
142+
analytics.identify({
143143
[UserProfileProperty.AUTHENTICATION_TYPE]: AUTHENTICATION_TYPE.PASSWORD,
144144
});
145145
return Keychain.resetGenericPassword(options);
@@ -181,7 +181,6 @@ const SecureKeychain = {
181181
accessible: Keychain.ACCESSIBLE.WHEN_UNLOCKED_THIS_DEVICE_ONLY,
182182
};
183183

184-
const metrics = MetaMetrics.getInstance();
185184
// TODO: Remove biometric and passcode types once we have removed the legacy authentication types
186185
if (
187186
type === AUTHENTICATION_TYPE.DEVICE_AUTHENTICATION ||
@@ -191,7 +190,7 @@ const SecureKeychain = {
191190
authOptions.accessControl =
192191
Keychain.ACCESS_CONTROL.BIOMETRY_ANY_OR_DEVICE_PASSCODE;
193192

194-
await metrics.addTraitsToUser({
193+
analytics.identify({
195194
[UserProfileProperty.AUTHENTICATION_TYPE]:
196195
AUTHENTICATION_TYPE.DEVICE_AUTHENTICATION,
197196
});

0 commit comments

Comments
 (0)