Skip to content

Commit

Permalink
feat: Adding a new controller for Metametrics Data Deletion (#24503)
Browse files Browse the repository at this point in the history
  • Loading branch information
NiranjanaBinoy authored Sep 16, 2024
1 parent 67cf369 commit d8b99ea
Show file tree
Hide file tree
Showing 24 changed files with 2,047 additions and 58 deletions.
2 changes: 2 additions & 0 deletions .metamaskrc.dist
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@ INFURA_PROJECT_ID=00000000000
;PASSWORD=METAMASK PASSWORD
;SEGMENT_WRITE_KEY=
;BRIDGE_USE_DEV_APIS=
;ANALYTICS_DATA_DELETION_SOURCE_ID=
;ANALYTICS_DATA_DELETION_ENDPOINT=
;SWAPS_USE_DEV_APIS=
;PORTFOLIO_URL=
;TRANSACTION_SECURITY_PROVIDER=
Expand Down
5 changes: 5 additions & 0 deletions app/scripts/constants/sentry-state.ts
Original file line number Diff line number Diff line change
Expand Up @@ -159,6 +159,11 @@ export const SENTRY_BACKGROUND_STATE = {
traits: false,
dataCollectionForMarketing: false,
marketingCampaignCookieId: true,
latestNonAnonymousEventTimestamp: true,
},
MetaMetricsDataDeletionController: {
metaMetricsDataDeletionId: true,
metaMetricsDataDeletionTimestamp: true,
},
NameController: {
names: false,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,181 @@
import { ControllerMessenger } from '@metamask/base-controller';
import {
MetaMetricsDataDeletionController,
type MetaMetricsDataDeletionControllerMessengerActions,
} from './metametrics-data-deletion';

describe('MetaMetricsDataDeletionController', () => {
describe('createMetaMetricsDataDeletionTask', () => {
it('creates a data deletion task and stores ID when user is participating in metrics tracking', async () => {
const mockMetaMetricsId = 'mockId';
const mockTaskId = 'mockTaskId';
const { controller, dataDeletionService } = setupController({
options: {
getMetaMetricsId: jest.fn().mockReturnValue(mockMetaMetricsId),
dataDeletionService: {
createDataDeletionRegulationTask: jest
.fn()
.mockResolvedValue(mockTaskId),
fetchDeletionRegulationStatus: jest
.fn()
.mockResolvedValue('UNKNOWN'),
},
},
});

await controller.createMetaMetricsDataDeletionTask();
expect(
dataDeletionService.createDataDeletionRegulationTask,
).toHaveBeenCalledWith(mockMetaMetricsId);
expect(
dataDeletionService.createDataDeletionRegulationTask,
).toHaveBeenCalledTimes(1);
expect(
dataDeletionService.fetchDeletionRegulationStatus,
).toHaveBeenCalledTimes(1);
expect(controller.state).toStrictEqual({
metaMetricsDataDeletionId: mockTaskId,
metaMetricsDataDeletionTimestamp: expect.any(Number),
metaMetricsDataDeletionStatus: 'UNKNOWN',
});
});
it('creates a data deletion task and stores ID when user is not currently participating in metrics tracking', async () => {
const mockMetaMetricsId = 'mockId';
const mockTaskId = 'mockTaskId';
const { controller, dataDeletionService } = setupController({
options: {
getMetaMetricsId: jest.fn().mockReturnValue(mockMetaMetricsId),
dataDeletionService: {
createDataDeletionRegulationTask: jest
.fn()
.mockResolvedValue(mockTaskId),
fetchDeletionRegulationStatus: jest
.fn()
.mockResolvedValue('UNKNOWN'),
},
},
});

await controller.createMetaMetricsDataDeletionTask();

expect(
dataDeletionService.createDataDeletionRegulationTask,
).toHaveBeenCalledTimes(1);
expect(
dataDeletionService.fetchDeletionRegulationStatus,
).toHaveBeenCalledTimes(1);
expect(
dataDeletionService.createDataDeletionRegulationTask,
).toHaveBeenCalledWith(mockMetaMetricsId);
expect(controller.state).toStrictEqual({
metaMetricsDataDeletionId: mockTaskId,
metaMetricsDataDeletionTimestamp: expect.any(Number),
metaMetricsDataDeletionStatus: 'UNKNOWN',
});
});

it('fails to creates a data deletion task when user has never participating in metrics tracking', async () => {
const { controller } = setupController({
options: {
getMetaMetricsId: jest.fn().mockReturnValue(null),
},
});
await expect(
controller.createMetaMetricsDataDeletionTask(),
).rejects.toThrow();
expect(controller.state).toStrictEqual({
metaMetricsDataDeletionId: null,
metaMetricsDataDeletionTimestamp: expect.any(Number),
});
});
});
describe('updateDataDeletionTaskStatus', () => {
it('fetches and stores status of the delete regulation using delete regulation ID', async () => {
const mockMetaMetricsId = 'mockId';
const mockTaskId = 'mockTaskId';
const { controller, dataDeletionService } = setupController({
options: {
getMetaMetricsId: jest.fn().mockReturnValue(mockMetaMetricsId),
dataDeletionService: {
createDataDeletionRegulationTask: jest
.fn()
.mockResolvedValue(mockTaskId),
fetchDeletionRegulationStatus: jest
.fn()
.mockResolvedValue('UNKNOWN'),
},
},
});
await controller.createMetaMetricsDataDeletionTask();
await controller.updateDataDeletionTaskStatus();
expect(
dataDeletionService.fetchDeletionRegulationStatus,
).toHaveBeenCalledTimes(2);
expect(
dataDeletionService.fetchDeletionRegulationStatus,
).toHaveBeenCalledWith(mockTaskId);
expect(controller.state).toStrictEqual({
metaMetricsDataDeletionId: mockTaskId,
metaMetricsDataDeletionTimestamp: expect.any(Number),
metaMetricsDataDeletionStatus: 'UNKNOWN',
});
});
});
});

/**
* Setup a test controller instance.
*
* @param options - Setup options.
* @param options.options - Controller constructor options.
* @returns The test controller, a messenger instance, and related mocks.
*/
function setupController({
options,
}: {
options?: Partial<
ConstructorParameters<typeof MetaMetricsDataDeletionController>[0]
>;
} = {}): {
controller: MetaMetricsDataDeletionController;
dataDeletionService: ConstructorParameters<
typeof MetaMetricsDataDeletionController
>[0]['dataDeletionService'];
messenger: ControllerMessenger<
MetaMetricsDataDeletionControllerMessengerActions,
never
>;
} {
const messenger = new ControllerMessenger<
MetaMetricsDataDeletionControllerMessengerActions,
never
>();
const mockCreateDataDeletionRegulationTaskResponse = 'mockRegulateId';
const mockFetchDeletionRegulationStatusResponse = 'UNKNOWN';
const mockDataDeletionService = {
createDataDeletionRegulationTask: jest
.fn()
.mockResolvedValue(mockCreateDataDeletionRegulationTaskResponse),
fetchDeletionRegulationStatus: jest
.fn()
.mockResolvedValue(mockFetchDeletionRegulationStatusResponse),
...options?.dataDeletionService,
};
const constructorOptions = {
dataDeletionService: mockDataDeletionService,
getMetaMetricsId: jest.fn().mockReturnValue('mockMetaMetricsId'),
messenger: messenger.getRestricted({
name: 'MetaMetricsDataDeletionController',
allowedActions: [],
allowedEvents: [],
}),
...options,
};
const controller = new MetaMetricsDataDeletionController(constructorOptions);

return {
controller,
dataDeletionService: constructorOptions.dataDeletionService,
messenger,
};
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,183 @@
import {
BaseController,
RestrictedControllerMessenger,
} from '@metamask/base-controller';
import { PublicInterface } from '@metamask/utils';
import type { DataDeletionService } from '../../services/data-deletion-service';
import { DeleteRegulationStatus } from '../../../../shared/constants/metametrics';

// Unique name for the controller
const controllerName = 'MetaMetricsDataDeletionController';

/**
* Timestamp at which regulation response is returned.
*/
export type DataDeleteTimestamp = number;
/**
* Regulation Id retuned while creating a delete regulation.
*/
export type DataDeleteRegulationId = string | null;

/**
* MetaMetricsDataDeletionController controller state
* metaMetricsDataDeletionId - Regulation Id retuned while creating a delete regulation.
* metaMetricsDataDeletionTimestamp - Timestamp at which the most recent regulation is created/requested for.
* metaMetricsDataDeletionStatus - Status of the current delete regulation.
*/
export type MetaMetricsDataDeletionState = {
metaMetricsDataDeletionId: DataDeleteRegulationId;
metaMetricsDataDeletionTimestamp: DataDeleteTimestamp;
metaMetricsDataDeletionStatus?: DeleteRegulationStatus;
};

const getDefaultState = (): MetaMetricsDataDeletionState => {
return {
metaMetricsDataDeletionId: null,
metaMetricsDataDeletionTimestamp: 0,
};
};

// Metadata for the controller state
const metadata = {
metaMetricsDataDeletionId: {
persist: true,
anonymous: true,
},
metaMetricsDataDeletionTimestamp: {
persist: true,
anonymous: true,
},
metaMetricsDataDeletionStatus: {
persist: true,
anonymous: true,
},
};

// Describes the action creating the delete regulation task
export type CreateMetaMetricsDataDeletionTaskAction = {
type: `${typeof controllerName}:createMetaMetricsDataDeletionTask`;
handler: MetaMetricsDataDeletionController['createMetaMetricsDataDeletionTask'];
};

// Describes the action to check the existing regulation status
export type UpdateDataDeletionTaskStatusAction = {
type: `${typeof controllerName}:updateDataDeletionTaskStatus`;
handler: MetaMetricsDataDeletionController['updateDataDeletionTaskStatus'];
};

// Union of all possible actions for the messenger
export type MetaMetricsDataDeletionControllerMessengerActions =
| CreateMetaMetricsDataDeletionTaskAction
| UpdateDataDeletionTaskStatusAction;

// Type for the messenger of MetaMetricsDataDeletionController
export type MetaMetricsDataDeletionControllerMessenger =
RestrictedControllerMessenger<
typeof controllerName,
MetaMetricsDataDeletionControllerMessengerActions,
never,
never,
never
>;

/**
* Controller responsible for maintaining
* state related to Metametrics data deletion
*/
export class MetaMetricsDataDeletionController extends BaseController<
typeof controllerName,
MetaMetricsDataDeletionState,
MetaMetricsDataDeletionControllerMessenger
> {
#dataDeletionService: PublicInterface<DataDeletionService>;

#getMetaMetricsId: () => string | null;

/**
* Creates a MetaMetricsDataDeletionController instance.
*
* @param args - The arguments to this function.
* @param args.dataDeletionService - The service used for deleting data.
* @param args.messenger - Messenger used to communicate with BaseV2 controller.
* @param args.state - Initial state to set on this controller.
* @param args.getMetaMetricsId - A function that returns the current MetaMetrics ID.
*/
constructor({
dataDeletionService,
messenger,
state,
getMetaMetricsId,
}: {
dataDeletionService: PublicInterface<DataDeletionService>;
messenger: MetaMetricsDataDeletionControllerMessenger;
state?: Partial<MetaMetricsDataDeletionState>;
getMetaMetricsId: () => string | null;
}) {
// Call the constructor of BaseControllerV2
super({
messenger,
metadata,
name: controllerName,
state: { ...getDefaultState(), ...state },
});
this.#getMetaMetricsId = getMetaMetricsId;
this.#dataDeletionService = dataDeletionService;
this.#registerMessageHandlers();
}

/**
* Constructor helper for registering this controller's messaging system
* actions.
*/
#registerMessageHandlers(): void {
this.messagingSystem.registerActionHandler(
`${controllerName}:createMetaMetricsDataDeletionTask`,
this.createMetaMetricsDataDeletionTask.bind(this),
);

this.messagingSystem.registerActionHandler(
`${controllerName}:updateDataDeletionTaskStatus`,
this.updateDataDeletionTaskStatus.bind(this),
);
}

/**
* Creating the delete regulation using source regulation
*
*/
async createMetaMetricsDataDeletionTask(): Promise<void> {
const metaMetricsId = this.#getMetaMetricsId();
if (!metaMetricsId) {
throw new Error('MetaMetrics ID not found');
}

const deleteRegulateId =
await this.#dataDeletionService.createDataDeletionRegulationTask(
metaMetricsId,
);
this.update((state) => {
state.metaMetricsDataDeletionId = deleteRegulateId ?? null;
state.metaMetricsDataDeletionTimestamp = Date.now();
});
await this.updateDataDeletionTaskStatus();
}

/**
* To check the status of the current delete regulation.
*/
async updateDataDeletionTaskStatus(): Promise<void> {
const deleteRegulationId = this.state.metaMetricsDataDeletionId;
if (!deleteRegulationId) {
throw new Error('Delete Regulation id not found');
}

const deletionStatus =
await this.#dataDeletionService.fetchDeletionRegulationStatus(
deleteRegulationId,
);

this.update((state) => {
state.metaMetricsDataDeletionStatus = deletionStatus ?? undefined;
});
}
}
Loading

0 comments on commit d8b99ea

Please sign in to comment.