diff --git a/packages/analytics-controller/CHANGELOG.md b/packages/analytics-controller/CHANGELOG.md index 88710f6ee0f..a1e26a2aba6 100644 --- a/packages/analytics-controller/CHANGELOG.md +++ b/packages/analytics-controller/CHANGELOG.md @@ -7,6 +7,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +### Added + +- Optional `skipUUIDv4Check` on `AnalyticsPlatformAdapter` to allow non-UUIDv4 `analyticsId` strings when constructing `AnalyticsController` ([#8543](https://github.com/MetaMask/core/pull/8543)) + ### Changed - Bump `@metamask/messenger` from `^1.0.0` to `^1.1.1` ([#8364](https://github.com/MetaMask/core/pull/8364), [#8373](https://github.com/MetaMask/core/pull/8373)) diff --git a/packages/analytics-controller/src/AnalyticsController.test.ts b/packages/analytics-controller/src/AnalyticsController.test.ts index e6c9a43a32b..e012047a27f 100644 --- a/packages/analytics-controller/src/AnalyticsController.test.ts +++ b/packages/analytics-controller/src/AnalyticsController.test.ts @@ -377,6 +377,26 @@ describe('AnalyticsController', () => { }).toThrow('Invalid analyticsId'); }); + it('accepts non-UUID analyticsId when adapter skipUUIDv4Check is true', async () => { + const analyticsId = 'not-a-uuid'; + const platformAdapter = { + ...createMockAdapter(), + skipUUIDv4Check: true, + }; + const { controller } = await setupController({ + state: { + optedIn: false, + analyticsId, + }, + platformAdapter, + }); + + expect(controller.state.analyticsId).toBe(analyticsId); + expect(platformAdapter.onSetupCompleted).toHaveBeenCalledWith( + analyticsId, + ); + }); + it('accepts different valid UUIDv4 values', async () => { const analyticsId1 = '11111111-1111-4111-8111-111111111111'; const analyticsId2 = '22222222-2222-4222-9222-222222222222'; diff --git a/packages/analytics-controller/src/AnalyticsController.ts b/packages/analytics-controller/src/AnalyticsController.ts index 352b1702475..8f87062be92 100644 --- a/packages/analytics-controller/src/AnalyticsController.ts +++ b/packages/analytics-controller/src/AnalyticsController.ts @@ -219,7 +219,10 @@ export class AnalyticsController extends BaseController< ...state, }; - validateAnalyticsControllerState(initialState); + validateAnalyticsControllerState( + initialState, + platformAdapter.skipUUIDv4Check === true, + ); super({ name: controllerName, diff --git a/packages/analytics-controller/src/AnalyticsPlatformAdapter.types.ts b/packages/analytics-controller/src/AnalyticsPlatformAdapter.types.ts index 39d22ff5928..78485ba6f4f 100644 --- a/packages/analytics-controller/src/AnalyticsPlatformAdapter.types.ts +++ b/packages/analytics-controller/src/AnalyticsPlatformAdapter.types.ts @@ -33,6 +33,12 @@ export type AnalyticsTrackingEvent = { * Implementations should handle platform-specific details (Segment SDK, etc.) */ export type AnalyticsPlatformAdapter = { + /** + * When `true`, the controller accepts any non-empty `analyticsId` string + * instead of requiring UUIDv4 format. Defaults to validation against UUIDv4 when omitted or `false`. + */ + skipUUIDv4Check?: boolean; + /** * Track an analytics event. * diff --git a/packages/analytics-controller/src/analyticsControllerStateValidator.test.ts b/packages/analytics-controller/src/analyticsControllerStateValidator.test.ts index b36dee38163..2e1dea9c964 100644 --- a/packages/analytics-controller/src/analyticsControllerStateValidator.test.ts +++ b/packages/analytics-controller/src/analyticsControllerStateValidator.test.ts @@ -47,5 +47,36 @@ describe('analyticsControllerStateValidator', () => { expect(() => validateAnalyticsControllerState(state)).not.toThrow(); }); + + it('does not throw for non-UUID string when skipUUIDv4Check is true', () => { + const state: AnalyticsControllerState = { + optedIn: false, + analyticsId: 'not-a-uuid', + }; + + expect(() => validateAnalyticsControllerState(state, true)).not.toThrow(); + }); + + it('throws when analyticsId is not UUIDv4 and skipUUIDv4Check is false', () => { + const state: AnalyticsControllerState = { + optedIn: false, + analyticsId: 'not-a-uuid', + }; + + expect(() => validateAnalyticsControllerState(state, false)).toThrow( + 'Invalid analyticsId', + ); + }); + + it('throws when analyticsId is empty string even if skipUUIDv4Check is true', () => { + const state: AnalyticsControllerState = { + optedIn: false, + analyticsId: '', + }; + + expect(() => validateAnalyticsControllerState(state, true)).toThrow( + 'Invalid analyticsId', + ); + }); }); }); diff --git a/packages/analytics-controller/src/analyticsControllerStateValidator.ts b/packages/analytics-controller/src/analyticsControllerStateValidator.ts index 1713a9731bb..f40773994d2 100644 --- a/packages/analytics-controller/src/analyticsControllerStateValidator.ts +++ b/packages/analytics-controller/src/analyticsControllerStateValidator.ts @@ -24,12 +24,17 @@ export function isValidUUIDv4(value: string): boolean { * Validates that the analytics state has a valid UUIDv4 analyticsId. * * @param state - The analytics controller state to validate + * @param skipUUIDv4Check - When `true`, skips UUIDv4 format validation * @throws Error if analyticsId is missing or not a valid UUIDv4 */ export function validateAnalyticsControllerState( state: AnalyticsControllerState, + skipUUIDv4Check?: boolean, ): void { - if (!state.analyticsId || !isValidUUIDv4(state.analyticsId)) { + if ( + !state.analyticsId || + (skipUUIDv4Check !== true && !isValidUUIDv4(state.analyticsId)) + ) { throw new Error('Invalid analyticsId'); } }