Skip to content

Commit 25288a6

Browse files
authored
feat: cp-7.61.0 add @metamask/profile-metrics-controller (MetaMask#23247)
<!-- 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** <!-- 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? --> The `@metamask/profile-metrics-controller` package is being added to the Mobile client. The package ships two new components and their messengers: - `ProfileMetricsController` - `ProfileMetricsService` Preview build coming from MetaMask/core#7196 ## **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: null ## **Related issues** Related to https://consensyssoftware.atlassian.net/browse/WPC-179 ## **Pre-merge author checklist** - [ ] 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). - [ ] I've completed the PR template to the best of my ability - [ ] I’ve included tests if applicable - [ ] I’ve documented my code using [JSDoc](https://jsdoc.app/) format if applicable - [ ] 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] > Integrates `ProfileMetricsController` and `ProfileMetricsService` across Engine init, messengers, types, state, and tests, and adds `@metamask/profile-metrics-controller` dependency. > > - **Engine**: > - Initialize and register `ProfileMetricsController` and `ProfileMetricsService` in `Engine` init, add to `context`, and expose `ProfileMetricsController` in `state`. > - **Controllers/Services**: > - Add `profile-metrics-controller-init` (uses `RemoteFeatureFlagController` flag `extensionUxPna25` + `MetaMetrics.isEnabled` and `metaMetricsId`). > - Add `profile-metrics-service-init` (binds `fetch`, uses `SDK.Env.PRD`). > - **Messengers**: > - New restricted messengers `profile-metrics-controller-messenger` and `profile-metrics-service-messenger`; register both in `CONTROLLER_MESSENGERS`. > - **Types/Constants**: > - Extend global actions/events, controller names, `EngineState`, `ControllersToInitialize` for profile metrics; add `ProfileMetricsService` to `STATELESS_NON_CONTROLLER_NAMES`; add `ProfileMetricsController:stateChange` to background events. > - **State/Fixtures**: > - Include `ProfileMetricsController` default state in snapshots and initial background state. > - **Tests**: > - Add init and messenger tests for controller/service. > - **Dependencies**: > - Add `@metamask/profile-metrics-controller` to `package.json`. > > <sup>Written by [Cursor Bugbot](https://cursor.com/dashboard?tab=bugbot) for commit d0e0e4a. This will update automatically on new commits. Configure [here](https://cursor.com/dashboard?tab=bugbot).</sup> <!-- /CURSOR_SUMMARY -->
1 parent 509b61d commit 25288a6

16 files changed

Lines changed: 447 additions & 3 deletions

app/core/Engine/Engine.ts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -173,6 +173,8 @@ import { loggingControllerInit } from './controllers/logging-controller-init';
173173
import { phishingControllerInit } from './controllers/phishing-controller-init';
174174
import { addressBookControllerInit } from './controllers/address-book-controller-init';
175175
import { multichainRouterInit } from './controllers/multichain-router-init';
176+
import { profileMetricsControllerInit } from './controllers/profile-metrics-controller-init';
177+
import { profileMetricsServiceInit } from './controllers/profile-metrics-service-init';
176178
import { Messenger, MessengerEvents } from '@metamask/messenger';
177179

178180
// TODO: Replace "any" with type
@@ -361,6 +363,8 @@ export class Engine {
361363
RewardsDataService: rewardsDataServiceInit,
362364
DelegationController: DelegationControllerInit,
363365
AddressBookController: addressBookControllerInit,
366+
ProfileMetricsController: profileMetricsControllerInit,
367+
ProfileMetricsService: profileMetricsServiceInit,
364368
},
365369
persistedState: initialState as EngineState,
366370
baseControllerMessenger: this.controllerMessenger,
@@ -393,6 +397,8 @@ export class Engine {
393397
const preferencesController = controllersByName.PreferencesController;
394398
const delegationController = controllersByName.DelegationController;
395399
const addressBookController = controllersByName.AddressBookController;
400+
const profileMetricsController = controllersByName.ProfileMetricsController;
401+
const profileMetricsService = controllersByName.ProfileMetricsService;
396402

397403
// Backwards compatibility for existing references
398404
this.accountsController = accountsController;
@@ -539,6 +545,8 @@ export class Engine {
539545
PredictController: predictController,
540546
RewardsController: rewardsController,
541547
DelegationController: delegationController,
548+
ProfileMetricsController: profileMetricsController,
549+
ProfileMetricsService: profileMetricsService,
542550
};
543551

544552
const childControllers = Object.assign({}, this.context);
@@ -1329,6 +1337,7 @@ export default {
13291337
MultichainBalancesController,
13301338
MultichainTransactionsController,
13311339
///: END:ONLY_INCLUDE_IF
1340+
ProfileMetricsController,
13321341
} = instance.datamodel.state;
13331342

13341343
return {
@@ -1390,6 +1399,7 @@ export default {
13901399
MultichainBalancesController,
13911400
MultichainTransactionsController,
13921401
///: END:ONLY_INCLUDE_IF
1402+
ProfileMetricsController,
13931403
};
13941404
},
13951405

app/core/Engine/constants.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ export const STATELESS_NON_CONTROLLER_NAMES = [
1515
'BackendWebSocketService',
1616
'AccountActivityService',
1717
'MultichainAccountService',
18+
'ProfileMetricsService',
1819
] as const;
1920

2021
export const BACKGROUND_STATE_CHANGE_EVENT_NAMES = [
@@ -78,6 +79,7 @@ export const BACKGROUND_STATE_CHANGE_EVENT_NAMES = [
7879
'NetworkEnablementController:stateChange',
7980
'PredictController:stateChange',
8081
'DelegationController:stateChange',
82+
'ProfileMetricsController:stateChange',
8183
] as const;
8284

8385
export const swapsSupportedChainIds = [
Lines changed: 116 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,116 @@
1+
import {
2+
ProfileMetricsController,
3+
ProfileMetricsControllerMessenger,
4+
} from '@metamask/profile-metrics-controller';
5+
import { ControllerInitRequest } from '../types';
6+
import { profileMetricsControllerInit } from './profile-metrics-controller-init';
7+
import { ExtendedMessenger } from '../../ExtendedMessenger';
8+
import { MOCK_ANY_NAMESPACE, MockAnyNamespace } from '@metamask/messenger';
9+
import { getProfileMetricsControllerMessenger } from '../messengers/profile-metrics-controller-messenger';
10+
import { buildControllerInitRequestMock } from '../utils/test-utils';
11+
import { MetaMetrics } from '../../Analytics';
12+
13+
jest.mock('@metamask/profile-metrics-controller');
14+
15+
function getInitRequestMock({
16+
metaMetricsId,
17+
remoteFeatureFlag,
18+
metaMetricsEnabled,
19+
}: {
20+
metaMetricsId: string;
21+
remoteFeatureFlag: boolean;
22+
metaMetricsEnabled: boolean;
23+
}): jest.Mocked<ControllerInitRequest<ProfileMetricsControllerMessenger>> {
24+
const baseMessenger = new ExtendedMessenger<MockAnyNamespace, never, never>({
25+
namespace: MOCK_ANY_NAMESPACE,
26+
});
27+
28+
jest.spyOn(MetaMetrics, 'getInstance').mockReturnValue({
29+
isEnabled: () => metaMetricsEnabled,
30+
} as MetaMetrics);
31+
32+
const mockGetController = jest.fn().mockReturnValue({
33+
state: {
34+
remoteFeatureFlags: { extensionUxPna25: remoteFeatureFlag },
35+
},
36+
});
37+
38+
const requestMock = {
39+
...buildControllerInitRequestMock(baseMessenger),
40+
controllerMessenger: getProfileMetricsControllerMessenger(baseMessenger),
41+
initMessenger: undefined,
42+
metaMetricsId,
43+
getController: mockGetController,
44+
};
45+
46+
return requestMock;
47+
}
48+
49+
describe.each([
50+
{
51+
metaMetricsId: 'dd6395a5-7a84-47b8-8bc3-713170c2f3e8',
52+
remoteFeatureFlag: true,
53+
metaMetricsEnabled: true,
54+
},
55+
{
56+
metaMetricsId: '898cbad5-7a5e-4ea1-8ca0-822bb4804665',
57+
remoteFeatureFlag: false,
58+
metaMetricsEnabled: false,
59+
},
60+
{
61+
metaMetricsId: '9c9fe89c-76c3-4ad6-89f8-b76061159458',
62+
remoteFeatureFlag: true,
63+
metaMetricsEnabled: false,
64+
},
65+
{
66+
metaMetricsId: '5aed4107-f430-4bb0-84c9-1e7031599cc2',
67+
remoteFeatureFlag: false,
68+
metaMetricsEnabled: true,
69+
},
70+
])(
71+
'profileMetricsControllerInit',
72+
({ metaMetricsId, remoteFeatureFlag, metaMetricsEnabled }) => {
73+
beforeEach(() => {
74+
jest.clearAllMocks();
75+
});
76+
77+
describe(`when metaMetricsId is ${metaMetricsId}, the feature flag value is ${remoteFeatureFlag} and MetaMetrics is ${metaMetricsEnabled ? 'enabled' : 'disabled'}`, () => {
78+
it('initializes the controller', () => {
79+
const { controller } = profileMetricsControllerInit(
80+
getInitRequestMock({
81+
metaMetricsId,
82+
remoteFeatureFlag,
83+
metaMetricsEnabled,
84+
}),
85+
);
86+
87+
expect(controller).toBeInstanceOf(ProfileMetricsController);
88+
});
89+
90+
it('passes the proper arguments to the controller', () => {
91+
profileMetricsControllerInit(
92+
getInitRequestMock({
93+
metaMetricsId,
94+
remoteFeatureFlag,
95+
metaMetricsEnabled,
96+
}),
97+
);
98+
99+
const controllerMock = jest.mocked(ProfileMetricsController);
100+
101+
expect(controllerMock).toHaveBeenCalledWith({
102+
messenger: expect.any(Object),
103+
state: undefined,
104+
assertUserOptedIn: expect.any(Function),
105+
getMetaMetricsId: expect.any(Function),
106+
});
107+
expect(controllerMock.mock.calls[0][0].assertUserOptedIn()).toBe(
108+
metaMetricsEnabled && remoteFeatureFlag,
109+
);
110+
expect(controllerMock.mock.calls[0][0].getMetaMetricsId()).toBe(
111+
metaMetricsId,
112+
);
113+
});
114+
});
115+
},
116+
);
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
import {
2+
ProfileMetricsController,
3+
ProfileMetricsControllerMessenger,
4+
} from '@metamask/profile-metrics-controller';
5+
import { ControllerInitFunction } from '../types';
6+
import { MetaMetrics } from '../../Analytics';
7+
8+
/**
9+
* Initialize the profile metrics controller.
10+
*
11+
* @param request - The request object.
12+
* @param request.controllerMessenger - The messenger to use for the controller.
13+
* @param request.persistedState - The persisted state to use for the
14+
* controller.
15+
* @param request.getController - A function to get other initialized controllers.
16+
* @returns The initialized controller.
17+
*/
18+
export const profileMetricsControllerInit: ControllerInitFunction<
19+
ProfileMetricsController,
20+
ProfileMetricsControllerMessenger
21+
> = ({ controllerMessenger, persistedState, getController, metaMetricsId }) => {
22+
const remoteFeatureFlagController = getController(
23+
'RemoteFeatureFlagController',
24+
);
25+
const assertUserOptedIn = () =>
26+
remoteFeatureFlagController.state.remoteFeatureFlags.extensionUxPna25 ===
27+
true && MetaMetrics.getInstance().isEnabled() === true;
28+
29+
const controller = new ProfileMetricsController({
30+
messenger: controllerMessenger,
31+
state: persistedState.ProfileMetricsController,
32+
assertUserOptedIn,
33+
getMetaMetricsId: () => metaMetricsId,
34+
});
35+
36+
return {
37+
controller,
38+
};
39+
};
Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
import {
2+
ProfileMetricsService,
3+
ProfileMetricsServiceMessenger,
4+
} from '@metamask/profile-metrics-controller';
5+
import { SDK } from '@metamask/profile-sync-controller';
6+
import { ControllerInitRequest } from '../types';
7+
import { buildControllerInitRequestMock } from '../utils/test-utils';
8+
import { profileMetricsServiceInit } from './profile-metrics-service-init';
9+
import { ExtendedMessenger } from '../../ExtendedMessenger';
10+
import { MOCK_ANY_NAMESPACE, MockAnyNamespace } from '@metamask/messenger';
11+
import { getProfileMetricsServiceMessenger } from '../messengers/profile-metrics-service-messenger';
12+
13+
jest.mock('@metamask/profile-metrics-controller');
14+
15+
function getInitRequestMock(): jest.Mocked<
16+
ControllerInitRequest<ProfileMetricsServiceMessenger>
17+
> {
18+
const baseMessenger = new ExtendedMessenger<MockAnyNamespace, never, never>({
19+
namespace: MOCK_ANY_NAMESPACE,
20+
});
21+
22+
const requestMock = {
23+
...buildControllerInitRequestMock(baseMessenger),
24+
controllerMessenger: getProfileMetricsServiceMessenger(baseMessenger),
25+
initMessenger: undefined,
26+
};
27+
28+
return requestMock;
29+
}
30+
31+
describe('profileMetricsServiceInit', () => {
32+
it('initializes the service', () => {
33+
const { controller } = profileMetricsServiceInit(getInitRequestMock());
34+
expect(controller).toBeInstanceOf(ProfileMetricsService);
35+
});
36+
37+
it('passes the proper arguments to the controller', () => {
38+
profileMetricsServiceInit(getInitRequestMock());
39+
40+
const controllerMock = jest.mocked(ProfileMetricsService);
41+
expect(controllerMock).toHaveBeenCalledWith({
42+
messenger: expect.any(Object),
43+
fetch: expect.any(Function),
44+
env: SDK.Env.PRD,
45+
});
46+
});
47+
});
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
import {
2+
ProfileMetricsService,
3+
ProfileMetricsServiceMessenger,
4+
} from '@metamask/profile-metrics-controller';
5+
import { ControllerInitFunction } from '../types';
6+
import { SDK } from '@metamask/profile-sync-controller';
7+
8+
/**
9+
* Initialize the profile metrics service.
10+
*
11+
* @param request - The request object.
12+
* @param request.controllerMessenger - The messenger to use for the service.
13+
* @returns The initialized controller.
14+
*/
15+
export const profileMetricsServiceInit: ControllerInitFunction<
16+
ProfileMetricsService,
17+
ProfileMetricsServiceMessenger
18+
> = ({ controllerMessenger }) => {
19+
// The environment must be the same used by AuthenticationController.
20+
const env = SDK.Env.PRD;
21+
22+
const controller = new ProfileMetricsService({
23+
messenger: controllerMessenger,
24+
fetch: fetch.bind(globalThis),
25+
env,
26+
});
27+
28+
return {
29+
controller,
30+
};
31+
};

app/core/Engine/messengers/index.ts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -122,6 +122,8 @@ import {
122122
getTransactionPayControllerInitMessenger,
123123
getTransactionPayControllerMessenger,
124124
} from './transaction-pay-controller-messenger';
125+
import { getProfileMetricsControllerMessenger } from './profile-metrics-controller-messenger';
126+
import { getProfileMetricsServiceMessenger } from './profile-metrics-service-messenger';
125127

126128
/**
127129
* The messengers for the controllers that have been.
@@ -397,4 +399,12 @@ export const CONTROLLER_MESSENGERS = {
397399
getMessenger: getAccountActivityServiceMessenger,
398400
getInitMessenger: noop,
399401
},
402+
ProfileMetricsController: {
403+
getMessenger: getProfileMetricsControllerMessenger,
404+
getInitMessenger: noop,
405+
},
406+
ProfileMetricsService: {
407+
getMessenger: getProfileMetricsServiceMessenger,
408+
getInitMessenger: noop,
409+
},
400410
} as const;
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
import {
2+
MOCK_ANY_NAMESPACE,
3+
Messenger,
4+
MessengerActions,
5+
MessengerEvents,
6+
MockAnyNamespace,
7+
} from '@metamask/messenger';
8+
import { getProfileMetricsControllerMessenger } from './profile-metrics-controller-messenger';
9+
import { ProfileMetricsControllerMessenger } from '@metamask/profile-metrics-controller';
10+
11+
type RootMessenger = Messenger<
12+
MockAnyNamespace,
13+
MessengerActions<ProfileMetricsControllerMessenger>,
14+
MessengerEvents<ProfileMetricsControllerMessenger>
15+
>;
16+
17+
const getRootMessenger = (): RootMessenger =>
18+
new Messenger({
19+
namespace: MOCK_ANY_NAMESPACE,
20+
});
21+
22+
describe('getProfileMetricsControllerMessenger', () => {
23+
it('returns a restricted messenger', () => {
24+
const messenger = getRootMessenger();
25+
const profileMetricsControllerMessenger =
26+
getProfileMetricsControllerMessenger(messenger);
27+
28+
expect(profileMetricsControllerMessenger).toBeInstanceOf(Messenger);
29+
});
30+
});

0 commit comments

Comments
 (0)