diff --git a/packages/profile-sync-controller/CHANGELOG.md b/packages/profile-sync-controller/CHANGELOG.md index a36c8a0953f..f10fb9f9071 100644 --- a/packages/profile-sync-controller/CHANGELOG.md +++ b/packages/profile-sync-controller/CHANGELOG.md @@ -7,6 +7,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +### Added + +- Add MetaMetrics event library to standardize event properties across clients ([#6044](https://github.com/MetaMask/core/pull/6044)) + ## [19.0.0] ### Changed diff --git a/packages/profile-sync-controller/src/index.ts b/packages/profile-sync-controller/src/index.ts index 0140b052cbe..b76c71926a9 100644 --- a/packages/profile-sync-controller/src/index.ts +++ b/packages/profile-sync-controller/src/index.ts @@ -1,3 +1,4 @@ export * as SDK from './sdk'; export * as AuthenticationController from './controllers/authentication'; export * as UserStorageController from './controllers/user-storage'; +export * as MetaMetrics from './shared/metametrics'; diff --git a/packages/profile-sync-controller/src/shared/metametrics.test.ts b/packages/profile-sync-controller/src/shared/metametrics.test.ts new file mode 100644 index 00000000000..1e45262b1af --- /dev/null +++ b/packages/profile-sync-controller/src/shared/metametrics.test.ts @@ -0,0 +1,332 @@ +import { + IDENTITY_EVENTS, + buildIdentityEvent, + type IdentityEventBuilder, + type IdentityEventDefinitions, +} from './metametrics'; + +describe('Identity MetaMetrics Library', () => { + describe('IDENTITY_EVENTS', () => { + it('should have correct structure for account syncing events', () => { + expect(IDENTITY_EVENTS.ACCOUNT_SYNCING.ACCOUNTS_SYNC_ADDED).toStrictEqual( + { + name: 'Accounts Sync Added', + properties: { + profile_id: { + required: true, + type: 'string', + }, + }, + }, + ); + + expect( + IDENTITY_EVENTS.ACCOUNT_SYNCING.ACCOUNTS_SYNC_NAME_UPDATED, + ).toStrictEqual({ + name: 'Accounts Sync Name Updated', + properties: { + profile_id: { + required: true, + type: 'string', + }, + }, + }); + + expect( + IDENTITY_EVENTS.ACCOUNT_SYNCING.ACCOUNTS_SYNC_ERRONEOUS_SITUATION, + ).toStrictEqual({ + name: 'Accounts Sync Erroneous Situation', + properties: { + profile_id: { + required: true, + type: 'string', + }, + situation_message: { + required: true, + type: 'string', + }, + }, + }); + }); + + it('should have correct structure for network syncing events', () => { + expect(IDENTITY_EVENTS.NETWORK_SYNCING.NETWORK_SYNC_ADDED).toStrictEqual({ + name: 'Network Sync Added', + properties: { + profile_id: { + required: true, + type: 'string', + }, + chain_id: { + required: true, + type: 'string', + }, + }, + }); + + expect( + IDENTITY_EVENTS.NETWORK_SYNCING.NETWORK_SYNC_UPDATED, + ).toStrictEqual({ + name: 'Network Sync Updated', + properties: { + profile_id: { + required: true, + type: 'string', + }, + chain_id: { + required: true, + type: 'string', + }, + }, + }); + + expect( + IDENTITY_EVENTS.NETWORK_SYNCING.NETWORK_SYNC_REMOVED, + ).toStrictEqual({ + name: 'Network Sync Removed', + properties: { + profile_id: { + required: true, + type: 'string', + }, + chain_id: { + required: true, + type: 'string', + }, + }, + }); + }); + + it('should have correct structure for profile events', () => { + expect(IDENTITY_EVENTS.PROFILE.ACTIVITY_UPDATED).toStrictEqual({ + name: 'Profile Activity Updated', + properties: { + profile_id: { + required: false, + type: 'string', + }, + feature_name: { + required: true, + type: 'string', + fromObject: { + BACKUP_AND_SYNC: 'Backup And Sync', + AUTHENTICATION: 'Authentication', + }, + }, + action: { + required: true, + type: 'string', + fromObject: { + CONTACTS_SYNC_CONTACT_UPDATED: 'Contacts Sync Contact Updated', + CONTACTS_SYNC_CONTACT_DELETED: 'Contacts Sync Contact Deleted', + CONTACTS_SYNC_ERRONEOUS_SITUATION: + 'Contacts Sync Erroneous Situation', + SETTINGS_TOGGLE_ENABLED: 'settings_toggle_enabled', + SETTINGS_TOGGLE_DISABLED: 'settings_toggle_disabled', + SIGN_IN: 'Sign In', + SIGN_OUT: 'Sign Out', + AUTHENTICATION_FAILED: 'Authentication Failed', + }, + }, + additional_description: { + required: false, + type: 'string', + }, + }, + }); + + expect( + IDENTITY_EVENTS.PROFILE.BACKUP_AND_SYNC_INTRODUCTION_MODAL_INTERACTION, + ).toStrictEqual({ + name: 'Backup And Sync Introduction Modal Interaction', + properties: { + profile_id: { + required: false, + type: 'string', + }, + action: { + required: true, + type: 'string', + fromObject: { + MODAL_OPENED: 'modal_opened', + MODAL_CLOSED: 'modal_closed', + ENABLE_CLICKED: 'enable_clicked', + DISMISS_CLICKED: 'dismiss_clicked', + }, + }, + }, + }); + }); + }); + + describe('buildIdentityEvent', () => { + it('should build account sync added event correctly', () => { + const event = buildIdentityEvent( + IDENTITY_EVENTS.ACCOUNT_SYNCING.ACCOUNTS_SYNC_ADDED, + { + profile_id: 'test-profile-id', + }, + ); + + expect(event.name).toBe('Accounts Sync Added'); + expect(event.properties).toStrictEqual({ + profile_id: 'test-profile-id', + }); + }); + + it('should build account sync name updated event correctly', () => { + const event = buildIdentityEvent( + IDENTITY_EVENTS.ACCOUNT_SYNCING.ACCOUNTS_SYNC_NAME_UPDATED, + { + profile_id: 'test-profile-id', + }, + ); + + expect(event.name).toBe('Accounts Sync Name Updated'); + expect(event.properties).toStrictEqual({ + profile_id: 'test-profile-id', + }); + }); + + it('should build account sync erroneous situation event correctly', () => { + const event = buildIdentityEvent( + IDENTITY_EVENTS.ACCOUNT_SYNCING.ACCOUNTS_SYNC_ERRONEOUS_SITUATION, + { + profile_id: 'test-profile-id', + situation_message: 'test error message', + }, + ); + + expect(event.name).toBe('Accounts Sync Erroneous Situation'); + expect(event.properties).toStrictEqual({ + profile_id: 'test-profile-id', + situation_message: 'test error message', + }); + }); + + it('should build network sync events correctly', () => { + const addedEvent = buildIdentityEvent( + IDENTITY_EVENTS.NETWORK_SYNCING.NETWORK_SYNC_ADDED, + { + profile_id: 'test-profile-id', + chain_id: '0x1', + }, + ); + + expect(addedEvent.name).toBe('Network Sync Added'); + expect(addedEvent.properties).toStrictEqual({ + profile_id: 'test-profile-id', + chain_id: '0x1', + }); + + const updatedEvent = buildIdentityEvent( + IDENTITY_EVENTS.NETWORK_SYNCING.NETWORK_SYNC_UPDATED, + { + profile_id: 'test-profile-id', + chain_id: '0x1', + }, + ); + + expect(updatedEvent.name).toBe('Network Sync Updated'); + expect(updatedEvent.properties).toStrictEqual({ + profile_id: 'test-profile-id', + chain_id: '0x1', + }); + + const removedEvent = buildIdentityEvent( + IDENTITY_EVENTS.NETWORK_SYNCING.NETWORK_SYNC_REMOVED, + { + profile_id: 'test-profile-id', + chain_id: '0x1', + }, + ); + + expect(removedEvent.name).toBe('Network Sync Removed'); + expect(removedEvent.properties).toStrictEqual({ + profile_id: 'test-profile-id', + chain_id: '0x1', + }); + }); + + it('should build profile activity updated event with contacts sync action', () => { + const event = buildIdentityEvent( + IDENTITY_EVENTS.PROFILE.ACTIVITY_UPDATED, + { + profile_id: 'test-profile-id', + feature_name: 'Backup And Sync', + action: 'Contacts Sync Contact Updated', + }, + ); + + expect(event.name).toBe('Profile Activity Updated'); + expect(event.properties).toStrictEqual({ + profile_id: 'test-profile-id', + feature_name: 'Backup And Sync', + action: 'Contacts Sync Contact Updated', + }); + }); + + it('should build profile activity updated event with authentication action', () => { + const event = buildIdentityEvent( + IDENTITY_EVENTS.PROFILE.ACTIVITY_UPDATED, + { + feature_name: 'Authentication', + action: 'Sign In', + }, + ); + + expect(event.name).toBe('Profile Activity Updated'); + expect(event.properties).toStrictEqual({ + feature_name: 'Authentication', + action: 'Sign In', + }); + }); + + it('should build profile activity updated event with optional additional description', () => { + const event = buildIdentityEvent( + IDENTITY_EVENTS.PROFILE.ACTIVITY_UPDATED, + { + profile_id: 'test-profile-id', + feature_name: 'Backup And Sync', + action: 'Contacts Sync Erroneous Situation', + additional_description: 'test error message', + }, + ); + + expect(event.name).toBe('Profile Activity Updated'); + expect(event.properties).toStrictEqual({ + profile_id: 'test-profile-id', + feature_name: 'Backup And Sync', + action: 'Contacts Sync Erroneous Situation', + additional_description: 'test error message', + }); + }); + + it('should build backup and sync introduction modal interaction event', () => { + const event = buildIdentityEvent( + IDENTITY_EVENTS.PROFILE.BACKUP_AND_SYNC_INTRODUCTION_MODAL_INTERACTION, + { + profile_id: 'test-profile-id', + action: 'enable_clicked', + }, + ); + + expect(event.name).toBe('Backup And Sync Introduction Modal Interaction'); + expect(event.properties).toStrictEqual({ + profile_id: 'test-profile-id', + action: 'enable_clicked', + }); + }); + }); + + describe('Type safety', () => { + it('should export correct types', () => { + // These are compile-time checks, ensuring types are properly exported + const builder: IdentityEventBuilder = buildIdentityEvent; + const events: IdentityEventDefinitions = IDENTITY_EVENTS; + + expect(builder).toBeDefined(); + expect(events).toBeDefined(); + }); + }); +}); diff --git a/packages/profile-sync-controller/src/shared/metametrics.ts b/packages/profile-sync-controller/src/shared/metametrics.ts new file mode 100644 index 00000000000..02838de5424 --- /dev/null +++ b/packages/profile-sync-controller/src/shared/metametrics.ts @@ -0,0 +1,209 @@ +/** + * MetaMetrics events library for profile-sync-controller + * + * This library provides type-safe event definitions that mirror the segment schema, + * ensuring consistency and providing compile-time validation for event properties. + */ + +/** + * Interface defining the structure of an identity event + */ +export type IdentityEvent = { + name: string; + properties: { + [key: string]: { + required: boolean; + type: 'string' | 'number' | 'boolean'; + fromObject?: Record; + }; + }; +}; + +/** + * Type utility to extract property types from event definitions + */ +type PropertyType = + FromObject extends Record + ? FromObject[keyof FromObject] + : T extends 'string' + ? string + : T extends 'number' + ? number + : T extends 'boolean' + ? boolean + : never; + +/** + * Type utility to generate event properties with proper required/optional handling + */ +type EventProperties = { + [K in keyof T['properties'] as T['properties'][K]['required'] extends true + ? K + : never]: PropertyType< + T['properties'][K]['type'], + T['properties'][K]['fromObject'] + >; +} & { + [K in keyof T['properties'] as T['properties'][K]['required'] extends false + ? K + : never]?: PropertyType< + T['properties'][K]['type'], + T['properties'][K]['fromObject'] + >; +}; + +/** + * Identity events definitions matching the segment schema + */ +export const IDENTITY_EVENTS = { + ACCOUNT_SYNCING: { + ACCOUNTS_SYNC_ADDED: { + name: 'Accounts Sync Added', + properties: { + profile_id: { + required: true, + type: 'string', + }, + }, + }, + ACCOUNTS_SYNC_NAME_UPDATED: { + name: 'Accounts Sync Name Updated', + properties: { + profile_id: { + required: true, + type: 'string', + }, + }, + }, + ACCOUNTS_SYNC_ERRONEOUS_SITUATION: { + name: 'Accounts Sync Erroneous Situation', + properties: { + profile_id: { + required: true, + type: 'string', + }, + situation_message: { + required: true, + type: 'string', + }, + }, + }, + }, + NETWORK_SYNCING: { + NETWORK_SYNC_ADDED: { + name: 'Network Sync Added', + properties: { + profile_id: { + required: true, + type: 'string', + }, + chain_id: { + required: true, + type: 'string', + }, + }, + }, + NETWORK_SYNC_UPDATED: { + name: 'Network Sync Updated', + properties: { + profile_id: { + required: true, + type: 'string', + }, + chain_id: { + required: true, + type: 'string', + }, + }, + }, + NETWORK_SYNC_REMOVED: { + name: 'Network Sync Removed', + properties: { + profile_id: { + required: true, + type: 'string', + }, + chain_id: { + required: true, + type: 'string', + }, + }, + }, + }, + PROFILE: { + ACTIVITY_UPDATED: { + name: 'Profile Activity Updated', + properties: { + profile_id: { + required: false, + type: 'string', + }, + feature_name: { + required: true, + type: 'string', + fromObject: { + BACKUP_AND_SYNC: 'Backup And Sync', + AUTHENTICATION: 'Authentication', + }, + }, + action: { + required: true, + type: 'string', + fromObject: { + CONTACTS_SYNC_CONTACT_UPDATED: 'Contacts Sync Contact Updated', + CONTACTS_SYNC_CONTACT_DELETED: 'Contacts Sync Contact Deleted', + CONTACTS_SYNC_ERRONEOUS_SITUATION: + 'Contacts Sync Erroneous Situation', + SETTINGS_TOGGLE_ENABLED: 'settings_toggle_enabled', + SETTINGS_TOGGLE_DISABLED: 'settings_toggle_disabled', + SIGN_IN: 'Sign In', + SIGN_OUT: 'Sign Out', + AUTHENTICATION_FAILED: 'Authentication Failed', + }, + }, + additional_description: { + required: false, + type: 'string', + }, + }, + }, + BACKUP_AND_SYNC_INTRODUCTION_MODAL_INTERACTION: { + name: 'Backup And Sync Introduction Modal Interaction', + properties: { + profile_id: { + required: false, + type: 'string', + }, + action: { + required: true, + type: 'string', + fromObject: { + MODAL_OPENED: 'modal_opened', + MODAL_CLOSED: 'modal_closed', + ENABLE_CLICKED: 'enable_clicked', + DISMISS_CLICKED: 'dismiss_clicked', + }, + }, + }, + }, + }, +} as const satisfies Record>; + +/** + * Type-safe event builder function + * + * @param event - The event definition + * @param properties - The event properties (type-checked) + * @returns An object with the event name and properties + */ +export const buildIdentityEvent = ( + event: T, + properties: EventProperties, +): { name: (typeof event)['name']; properties: typeof properties } => ({ + name: event.name, + properties, +}); + +// Export types for external use +export type IdentityEventBuilder = typeof buildIdentityEvent; +export type IdentityEventDefinitions = typeof IDENTITY_EVENTS;