Feat: new analytics data regulation controller#7643
Conversation
…ad of dates - Changed `deleteRegulationDate` to `deleteRegulationTimestamp` in the state and related methods to store timestamps in milliseconds since epoch. - Updated relevant methods and tests to reflect the new timestamp format. - Removed date formatting logic and adjusted selectors accordingly. - Added new dependencies for testing and updated the test suite to ensure proper functionality with the new timestamp format. This change enhances consistency in handling date-related data within the analytics privacy controller.
|
No dependency changes detected. Learn more about Socket for GitHub. 👍 No dependency changes detected in pull request |
…e PascalCase - Refactored enum values in `DataDeleteResponseStatus` and `DataDeleteStatus` to follow PascalCase naming convention. - Updated all references in the codebase and tests to ensure consistency with the new enum values. - This change enhances code readability and aligns with common TypeScript practices.
…st title - Rename Error to Failure (and Ok to Success) for clearer naming - Fix duplicate test title in AnalyticsPrivacyController.test.ts - Update all references across the codebase
…tead of type-only
packages/analytics-data-regulation-controller/tsconfig.build.json
Outdated
Show resolved
Hide resolved
…nd improve state handling - Removed references to the `analytics-controller` in `tsconfig` files to streamline project structure. - Enhanced `checkDataDeleteStatus` method by capturing state values before async calls to ensure consistency during concurrent operations. - Simplified `IDeleteRegulationResponse` type to only represent the success case, removing unnecessary complexity. - Cleaned up unused imports in `logger.ts` and removed the `IDeleteRegulationStatusResponse` type from `types.ts` to reduce clutter. These changes improve code clarity and maintainability while ensuring the functionality remains intact.
packages/analytics-data-regulation-controller/src/AnalyticsDataRegulationController.ts
Outdated
Show resolved
Hide resolved
packages/analytics-data-regulation-controller/src/AnalyticsDataRegulationService.ts
Show resolved
Hide resolved
…ns for data deletion responses - Replaced `IDeleteRegulationResponse` and `IDeleteRegulationStatus` with `DeleteRegulationResponse` and `DeleteRegulationStatus` for improved clarity and consistency. - Updated the `checkDataDeleteStatus` method to utilize the new type definitions, ensuring type safety and better alignment with the API response structure. - Cleaned up imports in `index.ts` to reflect the new type names. These changes enhance code readability and maintainability while ensuring accurate type representation.
packages/analytics-data-regulation-controller/src/AnalyticsDataRegulationController.ts
Outdated
Show resolved
Hide resolved
… status handling - Updated `deletionRequestTimestamp` in `DeleteRegulationStatus` to remove the nullish coalescing operator, ensuring it directly reflects the provided timestamp. - Added `AnalyticsDataRegulationServiceMessenger` type export in `index.ts` for better type accessibility. - Removed the `DeleteRegulationStatusResponse` type from `types.ts` to streamline type definitions and reduce redundancy. These changes enhance clarity and maintainability of the data regulation controller's type definitions and state management.
…etaMask/core into feat/7618_analytics_privacy_controller
|
@metamaskbot publish-preview |
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 23 out of 25 changed files in this pull request and generated no new comments.
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
|
Preview builds have been published. See these instructions for more information about preview builds. Expand for full list of packages and versions. |
| * Type union for data deletion response status values. | ||
| */ | ||
| export type DataDeleteResponseStatus = | ||
| (typeof DATA_DELETE_RESPONSE_STATUSES)[keyof typeof DATA_DELETE_RESPONSE_STATUSES]; |
There was a problem hiding this comment.
Unused DataDeleteResponseStatus type is never referenced
Low Severity
The DataDeleteResponseStatus type is defined and exported but never used as a type annotation anywhere in the codebase. The service methods explicitly return typeof DATA_DELETE_RESPONSE_STATUSES.Success in their return types rather than using this union type. This type exists only because it's derived from the status constants, but it provides no value.
Additional Locations (1)
mcmire
left a comment
There was a problem hiding this comment.
Had some more comments, but they are fairly minor at this point, implementation looks pretty good to me. I'll go ahead and approve.
| actions: ['AnalyticsDataRegulationService:createDataDeletionTask'], | ||
| }); | ||
|
|
||
| const controller = new AnalyticsDataRegulationController({ |
There was a problem hiding this comment.
Nit: Can we use setupController in this test to remove some of the boilerplate? There are some other tests in this file that are also creating the controller and messengers manually?
| }); | ||
|
|
||
| // eslint-disable-next-line @typescript-eslint/no-unused-vars | ||
| const _controller = new AnalyticsDataRegulationController({ |
There was a problem hiding this comment.
Same here. It seems if we used setupController we could only destructure the messenger and avoid from needing an eslint-disable comment.
|
|
||
| describe('stateChange event', () => { | ||
| it('emits stateChange event with new state when hasCollectedDataSinceDeletionRequest is updated', () => { | ||
| const { rootMessenger, messenger } = setupController({ | ||
| state: { | ||
| hasCollectedDataSinceDeletionRequest: false, | ||
| }, | ||
| }); | ||
|
|
||
| const eventListener = jest.fn(); | ||
| messenger.subscribe( | ||
| 'AnalyticsDataRegulationController:stateChange', | ||
| eventListener, | ||
| ); | ||
|
|
||
| rootMessenger.call( | ||
| 'AnalyticsDataRegulationController:updateDataRecordingFlag', | ||
| ); | ||
|
|
||
| expect(eventListener).toHaveBeenCalled(); | ||
| const [newState] = eventListener.mock.calls[0]; | ||
| expect(newState.hasCollectedDataSinceDeletionRequest).toBe(true); | ||
| }); | ||
|
|
||
| it('does not emit stateChange event when hasCollectedDataSinceDeletionRequest is already true', () => { | ||
| const { rootMessenger, messenger } = setupController({ | ||
| state: { | ||
| hasCollectedDataSinceDeletionRequest: true, | ||
| }, | ||
| }); | ||
|
|
||
| const eventListener = jest.fn(); | ||
| messenger.subscribe( | ||
| 'AnalyticsDataRegulationController:stateChange', | ||
| eventListener, | ||
| ); | ||
|
|
||
| rootMessenger.call( | ||
| 'AnalyticsDataRegulationController:updateDataRecordingFlag', | ||
| ); | ||
|
|
||
| expect(eventListener).not.toHaveBeenCalled(); | ||
| }); | ||
| }); |
There was a problem hiding this comment.
Do we need this describe? Typically we don't need to test that the stateChange event is triggered for controllers, as this behavior is already tested in the BaseController tests.
| describe('stateChange event', () => { | |
| it('emits stateChange event with new state when hasCollectedDataSinceDeletionRequest is updated', () => { | |
| const { rootMessenger, messenger } = setupController({ | |
| state: { | |
| hasCollectedDataSinceDeletionRequest: false, | |
| }, | |
| }); | |
| const eventListener = jest.fn(); | |
| messenger.subscribe( | |
| 'AnalyticsDataRegulationController:stateChange', | |
| eventListener, | |
| ); | |
| rootMessenger.call( | |
| 'AnalyticsDataRegulationController:updateDataRecordingFlag', | |
| ); | |
| expect(eventListener).toHaveBeenCalled(); | |
| const [newState] = eventListener.mock.calls[0]; | |
| expect(newState.hasCollectedDataSinceDeletionRequest).toBe(true); | |
| }); | |
| it('does not emit stateChange event when hasCollectedDataSinceDeletionRequest is already true', () => { | |
| const { rootMessenger, messenger } = setupController({ | |
| state: { | |
| hasCollectedDataSinceDeletionRequest: true, | |
| }, | |
| }); | |
| const eventListener = jest.fn(); | |
| messenger.subscribe( | |
| 'AnalyticsDataRegulationController:stateChange', | |
| eventListener, | |
| ); | |
| rootMessenger.call( | |
| 'AnalyticsDataRegulationController:updateDataRecordingFlag', | |
| ); | |
| expect(eventListener).not.toHaveBeenCalled(); | |
| }); | |
| }); |
| cleanAll(); | ||
| disableNetConnect(); |
There was a problem hiding this comment.
Do you need to do this? It should already be happening for all tests in tests/setupAfterEnv/nock.ts.
| cleanAll(); | |
| disableNetConnect(); |
| cleanAll(); | ||
| enableNetConnect(); |
There was a problem hiding this comment.
Do you need to do this? It should already be happening for all tests in tests/setupAfterEnv/nock.ts.
| cleanAll(); | |
| enableNetConnect(); |
| }); | ||
|
|
||
| describe('onBreak', () => { | ||
| it('calls break listener when circuit breaker opens after multiple failures', async () => { |
There was a problem hiding this comment.
Nit: The "it" here is the onBreak listener, so maybe this would be more accurate?
| it('calls break listener when circuit breaker opens after multiple failures', async () => { | |
| it('is called when circuit breaker opens after multiple failures', async () => { |
(Similar for the other onBreak and onDegraded tests)
| if (!this.#analyticsId || this.#analyticsId.trim() === '') { | ||
| const error = new Error( | ||
| 'Analytics ID not found. You need to provide a valid analytics ID when initializing the AnalyticsDataRegulationController.', | ||
| ); | ||
| log('Analytics Deletion Task Error', error); | ||
| throw error; | ||
| } | ||
|
|
There was a problem hiding this comment.
Is this runtime check necessary? There are other instances within core where we perform runtime checks, but I think a lot of those instances are left over from when the code used to be written in JavaScript. I believe we have been largely relying on TypeScript to verify that required inputs have been provided. Of course if you think it's necessary, then that's fine, but I wanted to double-check first.
| if (!this.#analyticsId || this.#analyticsId.trim() === '') { | |
| const error = new Error( | |
| 'Analytics ID not found. You need to provide a valid analytics ID when initializing the AnalyticsDataRegulationController.', | |
| ); | |
| log('Analytics Deletion Task Error', error); | |
| throw error; | |
| } |
| const { deleteRegulationId } = this.state; | ||
| const { deleteRegulationTimestamp } = this.state; | ||
| const { hasCollectedDataSinceDeletionRequest } = this.state; |
There was a problem hiding this comment.
Can these be grouped together?
| const { deleteRegulationId } = this.state; | |
| const { deleteRegulationTimestamp } = this.state; | |
| const { hasCollectedDataSinceDeletionRequest } = this.state; | |
| const { | |
| deleteRegulationId, | |
| deleteRegulationTimestamp, | |
| hasCollectedDataSinceDeletionRequest | |
| } = this.state; |
packages/analytics-data-regulation-controller/src/AnalyticsDataRegulationController.ts
Show resolved
Hide resolved
| // Service validates and throws on all errors, so if we reach here, the response | ||
| // is guaranteed to be a success response with dataDeleteStatus present | ||
| const dataDeletionTaskStatus = await this.messenger.call( | ||
| 'AnalyticsDataRegulationService:checkDataDeleteStatus', | ||
| deleteRegulationId, | ||
| ); | ||
|
|
||
| status.dataDeletionRequestStatus = dataDeletionTaskStatus.dataDeleteStatus; |
There was a problem hiding this comment.
Nit: I think this comment might be misplaced? Maybe it was meant to go after the call.
| // Service validates and throws on all errors, so if we reach here, the response | |
| // is guaranteed to be a success response with dataDeleteStatus present | |
| const dataDeletionTaskStatus = await this.messenger.call( | |
| 'AnalyticsDataRegulationService:checkDataDeleteStatus', | |
| deleteRegulationId, | |
| ); | |
| status.dataDeletionRequestStatus = dataDeletionTaskStatus.dataDeleteStatus; | |
| const dataDeletionTaskStatus = await this.messenger.call( | |
| 'AnalyticsDataRegulationService:checkDataDeleteStatus', | |
| deleteRegulationId, | |
| ); | |
| // Service validates and throws on all errors, so if we reach here, the response | |
| // is guaranteed to be a success response with dataDeleteStatus present | |
| status.dataDeletionRequestStatus = dataDeletionTaskStatus.dataDeleteStatus; |
Explanation
This PR introduces a new
@metamask/analytics-data-regulation-controllerpackage that provides GDPR/CCPA data deletion functionality for analytics data. The package allows to extract the logic from the mobile app (and will be compatible with extension too)Current state: MetaMask mobile app currently has a dedicated mechanism to handle user data deletion requests for analytics data in compliance with GDPR and CCPA regulations.
Solution: This package introduces:
hasCollectedDataSinceDeletionRequestflag,deleteRegulationId, anddeleteRegulationTimestampto support compliance workflowsImplementation details:
analyticsIdas a constructor parameter (provided by the consumer)AnalyticsDataRegulationServicevia messenger actions to make HTTP requests to Segment's Regulations APIcreateServicePolicyfrom@metamask/controller-utilsfor retry logic and error handlingReferences
see also MetaMask/metamask-mobile#22016
Fixes #7618
Checklist
Note
Cursor Bugbot is generating a summary for commit 52f4a2c. Configure here.