Skip to content

Commit 7604bc8

Browse files
mikespositometamaskbotGudahttmcmire
authored
feat: add @metamask/profile-metrics-controller (#38177)
<!-- 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 extension. The package ships two new components and their messengers: - `ProfileMetricsController` - `ProfileMetricsService` Preview build coming from MetaMask/core#7196 [![Open in GitHub Codespaces](https://github.com/codespaces/badge.svg)](https://codespaces.new/MetaMask/metamask-extension/pull/38177?quickstart=1) ## **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 ## **Manual testing steps** 1. Go to this page... 2. 3. ## **Screenshots/Recordings** <!-- If applicable, add screenshots and/or recordings to visualize the before and after of your change. --> ### **Before** <!-- [screenshots/recordings] --> ### **After** <!-- [screenshots/recordings] --> ## **Pre-merge author checklist** - [ ] I've followed [MetaMask Contributor Docs](https://github.com/MetaMask/contributor-docs) and [MetaMask Extension Coding Standards](https://github.com/MetaMask/metamask-extension/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-extension/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 `@metamask/profile-metrics-controller` (controller + service) with messengers, initialization, state wiring, LavaMoat policies, and adds E2E/unit tests and fixtures. > > - **Controllers/Services** > - Add `ProfileMetricsController` and `ProfileMetricsService` to controller list/types and initialization (`app/scripts/controller-init/*`, `metamask-controller.js`). > - Wire controller state into persisted/mem stores and `ControllerFlatState`. > - **Messengers** > - Introduce restricted messengers: `getProfileMetricsControllerMessenger` and `getProfileMetricsServiceMessenger`, plus registry entries (`messengers/index.ts`). > - **Initialization** > - New inits: `profile-metrics-controller-init.ts` (feature-flag + MetaMetrics gated, interval config) and `profile-metrics-service-init.ts` (bind `fetch`, set SDK env). > - **Security/Build** > - Update LavaMoat Browserify/Webpack policies to allow profile-metrics modules and deps. > - Add dependency `@metamask/profile-metrics-controller` in `package.json`. > - **Tests/Fixtures** > - Add unit tests for messengers and inits. > - Add E2E tests validating API calls based on feature flag and MetaMetrics opt-in. > - Update E2E fixtures/state snapshots to include `ProfileMetricsController` state. > > <sup>Written by [Cursor Bugbot](https://cursor.com/dashboard?tab=bugbot) for commit c162dab. This will update automatically on new commits. Configure [here](https://cursor.com/dashboard?tab=bugbot).</sup> <!-- /CURSOR_SUMMARY --> --------- Co-authored-by: MetaMask Bot <[email protected]> Co-authored-by: Mark Stacey <[email protected]> Co-authored-by: Elliot Winkler <[email protected]>
1 parent d27ff99 commit 7604bc8

26 files changed

+737
-3
lines changed

app/scripts/controller-init/controller-list.ts

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -83,6 +83,10 @@ import {
8383
BackendWebSocketService,
8484
} from '@metamask/core-backend';
8585
import { ClaimsController, ClaimsService } from '@metamask/claims-controller';
86+
import {
87+
ProfileMetricsController,
88+
ProfileMetricsService,
89+
} from '@metamask/profile-metrics-controller';
8690
import OnboardingController from '../controllers/onboarding';
8791
import { PreferencesController } from '../controllers/preferences-controller';
8892
import SwapsController from '../controllers/swaps';
@@ -197,7 +201,9 @@ export type Controller =
197201
| AccountActivityService
198202
| MultichainAccountService
199203
| NetworkEnablementController
200-
| ClaimsService;
204+
| ClaimsService
205+
| ProfileMetricsController
206+
| ProfileMetricsService;
201207

202208
/**
203209
* Flat state object for all controllers supporting or required by modular initialization.
@@ -270,4 +276,5 @@ export type ControllerFlatState = AccountOrderController['state'] &
270276
NftController['state'] &
271277
NftDetectionController['state'] &
272278
NetworkEnablementController['state'] &
273-
AccountTrackerController['state'];
279+
AccountTrackerController['state'] &
280+
ProfileMetricsController['state'];

app/scripts/controller-init/messengers/index.ts

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -204,6 +204,8 @@ import {
204204
getClaimsControllerMessenger,
205205
} from './claims/claims-controller-messenger';
206206
import { getClaimsServiceMessenger } from './claims/claims-service-messenger';
207+
import { getProfileMetricsControllerMessenger } from './profile-metrics-controller-messenger';
208+
import { getProfileMetricsServiceMessenger } from './profile-metrics-service-messenger';
207209

208210
export type { AccountOrderControllerMessenger } from './account-order-controller-messenger';
209211
export { getAccountOrderControllerMessenger } from './account-order-controller-messenger';
@@ -409,6 +411,8 @@ export {
409411
getUserOperationControllerMessenger,
410412
getUserOperationControllerInitMessenger,
411413
} from './user-operation-controller-messenger';
414+
export { getProfileMetricsControllerMessenger } from './profile-metrics-controller-messenger';
415+
export { getProfileMetricsServiceMessenger } from './profile-metrics-service-messenger';
412416

413417
export const CONTROLLER_MESSENGERS = {
414418
AccountOrderController: {
@@ -765,4 +769,12 @@ export const CONTROLLER_MESSENGERS = {
765769
getMessenger: getNetworkEnablementControllerMessenger,
766770
getInitMessenger: getNetworkEnablementControllerInitMessenger,
767771
},
772+
ProfileMetricsController: {
773+
getMessenger: getProfileMetricsControllerMessenger,
774+
getInitMessenger: noop,
775+
},
776+
ProfileMetricsService: {
777+
getMessenger: getProfileMetricsServiceMessenger,
778+
getInitMessenger: noop,
779+
},
768780
} as const;
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
import { Messenger } from '@metamask/messenger';
2+
import { getRootMessenger } from '../../lib/messenger';
3+
import { getProfileMetricsControllerMessenger } from './profile-metrics-controller-messenger';
4+
5+
describe('getProfileMetricsControllerMessenger', () => {
6+
it('returns a restricted messenger', () => {
7+
const messenger = getRootMessenger<never, never>();
8+
const profileMetricsControllerMessenger =
9+
getProfileMetricsControllerMessenger(messenger);
10+
11+
expect(profileMetricsControllerMessenger).toBeInstanceOf(Messenger);
12+
});
13+
});
Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
import { ProfileMetricsControllerMessenger } from '@metamask/profile-metrics-controller';
2+
import {
3+
Messenger,
4+
MessengerActions,
5+
MessengerEvents,
6+
} from '@metamask/messenger';
7+
import { RootMessenger } from '../../lib/messenger';
8+
9+
type AllowedActions = MessengerActions<ProfileMetricsControllerMessenger>;
10+
11+
type AllowedEvents = MessengerEvents<ProfileMetricsControllerMessenger>;
12+
13+
/**
14+
* Create a messenger restricted to the allowed actions and events of the
15+
* accounts controller.
16+
*
17+
* @param messenger - The base messenger used to create the restricted
18+
* messenger.
19+
*/
20+
export function getProfileMetricsControllerMessenger(
21+
messenger: RootMessenger<AllowedActions, AllowedEvents>,
22+
) {
23+
const controllerMessenger = new Messenger<
24+
'ProfileMetricsController',
25+
AllowedActions,
26+
AllowedEvents,
27+
typeof messenger
28+
>({
29+
namespace: 'ProfileMetricsController',
30+
parent: messenger,
31+
});
32+
messenger.delegate({
33+
messenger: controllerMessenger,
34+
actions: [
35+
'AccountsController:listAccounts',
36+
'ProfileMetricsService:submitMetrics',
37+
],
38+
events: [
39+
'AccountsController:accountAdded',
40+
'AccountsController:accountRemoved',
41+
'KeyringController:lock',
42+
'KeyringController:unlock',
43+
],
44+
});
45+
return controllerMessenger;
46+
}
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
import { Messenger } from '@metamask/messenger';
2+
import { getRootMessenger } from '../../lib/messenger';
3+
import { getProfileMetricsServiceMessenger } from './profile-metrics-service-messenger';
4+
5+
describe('getProfileMetricsServiceMessenger', () => {
6+
it('returns a restricted messenger', () => {
7+
const messenger = getRootMessenger<never, never>();
8+
const profileMetricsServiceMessenger =
9+
getProfileMetricsServiceMessenger(messenger);
10+
11+
expect(profileMetricsServiceMessenger).toBeInstanceOf(Messenger);
12+
});
13+
});
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
import { ProfileMetricsServiceMessenger } from '@metamask/profile-metrics-controller';
2+
import {
3+
Messenger,
4+
MessengerActions,
5+
MessengerEvents,
6+
} from '@metamask/messenger';
7+
import { RootMessenger } from '../../lib/messenger';
8+
9+
type AllowedActions = MessengerActions<ProfileMetricsServiceMessenger>;
10+
11+
type AllowedEvents = MessengerEvents<ProfileMetricsServiceMessenger>;
12+
13+
/**
14+
* Create a messenger restricted to the allowed actions and events of the
15+
* accounts controller.
16+
*
17+
* @param messenger - The base messenger used to create the restricted
18+
* messenger.
19+
*/
20+
export function getProfileMetricsServiceMessenger(
21+
messenger: RootMessenger<AllowedActions, AllowedEvents>,
22+
): ProfileMetricsServiceMessenger {
23+
const serviceMessenger = new Messenger<
24+
'ProfileMetricsService',
25+
AllowedActions,
26+
AllowedEvents,
27+
typeof messenger
28+
>({
29+
namespace: 'ProfileMetricsService',
30+
parent: messenger,
31+
});
32+
messenger.delegate({
33+
messenger: serviceMessenger,
34+
actions: ['AuthenticationController:getBearerToken'],
35+
});
36+
return serviceMessenger;
37+
}
Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
import {
2+
ProfileMetricsController,
3+
ProfileMetricsControllerMessenger,
4+
} from '@metamask/profile-metrics-controller';
5+
import { getRootMessenger } from '../lib/messenger';
6+
import { ControllerInitRequest } from './types';
7+
import { buildControllerInitRequestMock } from './test/utils';
8+
import { getProfileMetricsControllerMessenger } from './messengers';
9+
import { ProfileMetricsControllerInit } from './profile-metrics-controller-init';
10+
11+
jest.mock('@metamask/profile-metrics-controller');
12+
13+
function getInitRequestMock(): jest.Mocked<
14+
ControllerInitRequest<ProfileMetricsControllerMessenger>
15+
> {
16+
const baseMessenger = getRootMessenger<never, never>();
17+
18+
const requestMock = {
19+
...buildControllerInitRequestMock(),
20+
controllerMessenger: getProfileMetricsControllerMessenger(baseMessenger),
21+
initMessenger: undefined,
22+
};
23+
24+
return requestMock;
25+
}
26+
27+
describe('ProfileMetricsControllerInit', () => {
28+
it('initializes the controller', () => {
29+
const { controller } = ProfileMetricsControllerInit(getInitRequestMock());
30+
expect(controller).toBeInstanceOf(ProfileMetricsController);
31+
});
32+
33+
it('passes the proper arguments to the controller', () => {
34+
ProfileMetricsControllerInit(getInitRequestMock());
35+
36+
const controllerMock = jest.mocked(ProfileMetricsController);
37+
expect(controllerMock).toHaveBeenCalledWith({
38+
messenger: expect.any(Object),
39+
state: undefined,
40+
interval: expect.any(Number),
41+
assertUserOptedIn: expect.any(Function),
42+
getMetaMetricsId: expect.any(Function),
43+
});
44+
});
45+
});
Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
import {
2+
ProfileMetricsController,
3+
ProfileMetricsControllerMessenger,
4+
} from '@metamask/profile-metrics-controller';
5+
import type { ControllerInitFunction } from './types';
6+
7+
const isTestEnvironment = Boolean(process.env.IN_TEST);
8+
9+
/**
10+
* Initialize the profile metrics controller.
11+
*
12+
* @param request - The request object.
13+
* @param request.controllerMessenger - The messenger to use for the controller.
14+
* @param request.persistedState - The persisted state to use for the
15+
* controller.
16+
* @param request.getController - A function to get other initialized controllers.
17+
* @returns The initialized controller.
18+
*/
19+
export const ProfileMetricsControllerInit: ControllerInitFunction<
20+
ProfileMetricsController,
21+
ProfileMetricsControllerMessenger
22+
> = ({ controllerMessenger, persistedState, getController }) => {
23+
const remoteFeatureFlagController = getController(
24+
'RemoteFeatureFlagController',
25+
);
26+
const metaMetricsController = getController('MetaMetricsController');
27+
const assertUserOptedIn = () =>
28+
remoteFeatureFlagController.state.remoteFeatureFlags.extensionUxPna25 ===
29+
true && metaMetricsController.state.participateInMetaMetrics === true;
30+
31+
const controller = new ProfileMetricsController({
32+
messenger: controllerMessenger,
33+
state: persistedState.ProfileMetricsController,
34+
interval: isTestEnvironment ? 1000 : 10 * 1000,
35+
assertUserOptedIn,
36+
getMetaMetricsId: () => metaMetricsController.getMetaMetricsId(),
37+
});
38+
39+
return {
40+
controller,
41+
};
42+
};
Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
import {
2+
ProfileMetricsService,
3+
ProfileMetricsServiceMessenger,
4+
} from '@metamask/profile-metrics-controller';
5+
import { SDK } from '@metamask/profile-sync-controller';
6+
import { getRootMessenger } from '../lib/messenger';
7+
import { ControllerInitRequest } from './types';
8+
import { buildControllerInitRequestMock } from './test/utils';
9+
import { getProfileMetricsServiceMessenger } from './messengers';
10+
import { ProfileMetricsServiceInit } from './profile-metrics-service-init';
11+
12+
jest.mock('@metamask/profile-metrics-controller');
13+
14+
function getInitRequestMock(): jest.Mocked<
15+
ControllerInitRequest<ProfileMetricsServiceMessenger>
16+
> {
17+
const baseMessenger = getRootMessenger<never, never>();
18+
19+
const requestMock = {
20+
...buildControllerInitRequestMock(),
21+
controllerMessenger: getProfileMetricsServiceMessenger(baseMessenger),
22+
initMessenger: undefined,
23+
};
24+
25+
return requestMock;
26+
}
27+
28+
describe('ProfileMetricsServiceInit', () => {
29+
it('initializes the service', () => {
30+
const { controller } = ProfileMetricsServiceInit(getInitRequestMock());
31+
expect(controller).toBeInstanceOf(ProfileMetricsService);
32+
});
33+
34+
it('passes the proper arguments to the controller', () => {
35+
ProfileMetricsServiceInit(getInitRequestMock());
36+
37+
const controllerMock = jest.mocked(ProfileMetricsService);
38+
expect(controllerMock).toHaveBeenCalledWith({
39+
messenger: expect.any(Object),
40+
fetch: expect.any(Function),
41+
env: SDK.Env.PRD,
42+
});
43+
});
44+
});
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
import {
2+
ProfileMetricsService,
3+
ProfileMetricsServiceMessenger,
4+
} from '@metamask/profile-metrics-controller';
5+
import { SDK } from '@metamask/profile-sync-controller';
6+
import { ControllerInitFunction } from './types';
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+
persistedStateKey: null,
30+
memStateKey: null,
31+
controller,
32+
};
33+
};

0 commit comments

Comments
 (0)