From 86faa003cc21f14759c8b93ae481c8a6a8988ce7 Mon Sep 17 00:00:00 2001 From: ffmcgee Date: Tue, 24 Mar 2026 14:05:21 +0100 Subject: [PATCH 01/21] feat: implement wallet-side analytics for SDKConnectV2 / MWP --- .../handlers/legacy/handleDeeplink.ts | 31 ++++++++++ .../types/deepLinkAnalytics.types.ts | 1 + .../util/deeplinks/deepLinkAnalytics.ts | 9 +++ .../services/connection-registry.ts | 32 ++++++++++ app/core/SDKConnectV2/services/connection.ts | 20 ++++++ .../SDKConnectV2/services/v2-analytics.ts | 62 +++++++++++++++++++ 6 files changed, 155 insertions(+) create mode 100644 app/core/SDKConnectV2/services/v2-analytics.ts diff --git a/app/core/DeeplinkManager/handlers/legacy/handleDeeplink.ts b/app/core/DeeplinkManager/handlers/legacy/handleDeeplink.ts index d632a6d3a4f..d7f72078bff 100644 --- a/app/core/DeeplinkManager/handlers/legacy/handleDeeplink.ts +++ b/app/core/DeeplinkManager/handlers/legacy/handleDeeplink.ts @@ -3,12 +3,18 @@ import Logger from '../../../../util/Logger'; import { AppStateEventProcessor } from '../../../AppStateEventListener'; import ReduxService from '../../../redux'; import SDKConnectV2 from '../../../SDKConnectV2'; +import { analytics } from '../../../../util/analytics/analytics'; +import { AnalyticsEventBuilder } from '../../../../util/analytics/AnalyticsEventBuilder'; +import { MetaMetricsEvents } from '../../../Analytics/MetaMetrics.events'; +import { DeepLinkRoute } from '../../types/deepLinkAnalytics.types'; +import { detectAppInstallation } from '../../util/deeplinks/deepLinkAnalytics'; export function handleDeeplink(opts: { uri?: string; source?: string }) { // This is the earliest JS entry point for deeplinks. We must handle SDKConnectV2 // links here immediately to establish the WebSocket connection as fast as possible, // without waiting for the app to be unlocked or fully onboarded. if (SDKConnectV2.isMwpDeeplink(opts.uri)) { + trackMwpDeepLinkUsed(opts.uri); SDKConnectV2.handleMwpDeeplink(opts.uri); // By returning here, we bypass the standard saga-based deeplink flow below, // which would otherwise wait for a LOGIN or ONBOARDING_COMPLETED action. @@ -27,3 +33,28 @@ export function handleDeeplink(opts: { uri?: string; source?: string }) { Logger.error(e as Error, `Deeplink: Error parsing deeplink`); } } + +/** + * Fire DEEP_LINK_USED for MWP deeplinks asynchronously so the WebSocket + * handshake is never blocked by analytics work. + */ +function trackMwpDeepLinkUsed(url: string): void { + detectAppInstallation() + .then((wasAppInstalled) => { + const event = AnalyticsEventBuilder.createEventBuilder( + MetaMetricsEvents.DEEP_LINK_USED, + ) + .addProperties({ + route: DeepLinkRoute.SDK_MWP, + was_app_installed: wasAppInstalled, + }) + .build(); + analytics.trackEvent(event); + }) + .catch((error) => { + Logger.error( + error as Error, + 'DeepLinkAnalytics: Failed to track MWP deep link event', + ); + }); +} diff --git a/app/core/DeeplinkManager/types/deepLinkAnalytics.types.ts b/app/core/DeeplinkManager/types/deepLinkAnalytics.types.ts index 9039fc65cc5..5ac1d0f0b94 100644 --- a/app/core/DeeplinkManager/types/deepLinkAnalytics.types.ts +++ b/app/core/DeeplinkManager/types/deepLinkAnalytics.types.ts @@ -55,6 +55,7 @@ export enum DeepLinkRoute { CARD_ONBOARDING = 'card-onboarding', CARD_HOME = 'card-home', NFT = 'nft', + SDK_MWP = 'sdk_mwp', INVALID = 'invalid', } diff --git a/app/core/DeeplinkManager/util/deeplinks/deepLinkAnalytics.ts b/app/core/DeeplinkManager/util/deeplinks/deepLinkAnalytics.ts index 402c2a1cc89..934c2fc54fc 100644 --- a/app/core/DeeplinkManager/util/deeplinks/deepLinkAnalytics.ts +++ b/app/core/DeeplinkManager/util/deeplinks/deepLinkAnalytics.ts @@ -431,6 +431,14 @@ const extractNftProperties = ( // NFT route doesn't have sensitive parameters to extract }; +const extractSdkMwpProperties = ( + _urlParams: UrlParamValues, + _sensitiveProps: Record, +): void => { + // SDK MWP deeplinks carry their payload in a compressed `p` param; + // no route-level sensitive properties to extract here. +}; + /** * Extract properties for INVALID route * No properties to extract, this function is a placeholder @@ -471,6 +479,7 @@ const routeExtractors: Record< [DeepLinkRoute.CARD_ONBOARDING]: extractCardOnboardingProperties, [DeepLinkRoute.CARD_HOME]: extractCardHomeProperties, [DeepLinkRoute.NFT]: extractNftProperties, + [DeepLinkRoute.SDK_MWP]: extractSdkMwpProperties, [DeepLinkRoute.INVALID]: extractInvalidProperties, }; diff --git a/app/core/SDKConnectV2/services/connection-registry.ts b/app/core/SDKConnectV2/services/connection-registry.ts index b9cffda667e..a2e6166a1dd 100644 --- a/app/core/SDKConnectV2/services/connection-registry.ts +++ b/app/core/SDKConnectV2/services/connection-registry.ts @@ -18,6 +18,7 @@ import { whenStoreReady } from '../utils/when-store-ready'; import Engine from '../../Engine'; import { rpcErrors } from '@metamask/rpc-errors'; import { INTERNAL_ORIGINS } from '../../../constants/transaction'; +import { trackWalletEvent, type WalletEventProperties } from './v2-analytics'; /** * The ConnectionRegistry is the central service responsible for managing the @@ -113,9 +114,22 @@ export class ConnectionRegistry { const conn = await this.store.get(id); if (conn) { + trackWalletEvent('wallet_connection_request_received', { + anon_id: id, + platform: 'mobile', + sdk_version: conn.metadata?.sdk?.version, + sdk_platform: conn.metadata?.sdk?.platform, + found_in_store: true, + }); return; } + trackWalletEvent('wallet_connection_request_received', { + anon_id: id, + platform: 'mobile', + found_in_store: false, + }); + logger.error( 'Failed to find connection in store for simple deeplink with id:', id, @@ -169,9 +183,19 @@ export class ConnectionRegistry { let conn: Connection | undefined; let connInfo: ConnectionInfo | undefined; + let baseProps: WalletEventProperties | undefined; + try { const connReq = this.parseConnectionRequest(url); + baseProps = { + anon_id: connReq.sessionRequest.id, + platform: 'mobile', + sdk_version: connReq.metadata.sdk.version, + sdk_platform: connReq.metadata.sdk.platform, + }; + trackWalletEvent('wallet_connection_request_received', baseProps); + // Defense-in-depth: block connections whose self-reported dapp metadata // matches a known internal origin. This check is currently redundant // because isConnectionRequest() validates dapp.url as a valid https:// @@ -200,11 +224,19 @@ export class ConnectionRegistry { this.connections.set(conn.id, conn); await this.store.save(connInfo); this.hostapp.syncConnectionList(Array.from(this.connections.values())); + + trackWalletEvent('wallet_connection_user_approved', baseProps); logger.debug('Handled connect deeplink.', connInfo?.id); } catch (error) { logger.error('Failed to handle connect deeplink:', error, redactUrl(url)); this.hostapp.showConnectionError(); if (conn) await this.disconnect(conn.id); + + const failProps: WalletEventProperties = baseProps ?? { + anon_id: 'unknown', + platform: 'mobile', + }; + trackWalletEvent('wallet_connection_request_failed', failProps); } finally { if (connInfo) this.hostapp.hideConnectionLoading(connInfo); } diff --git a/app/core/SDKConnectV2/services/connection.ts b/app/core/SDKConnectV2/services/connection.ts index 30b39280678..bb7bd5bb94d 100644 --- a/app/core/SDKConnectV2/services/connection.ts +++ b/app/core/SDKConnectV2/services/connection.ts @@ -16,6 +16,7 @@ import { IHostApplicationAdapter } from '../types/host-application-adapter'; import { errorCodes, providerErrors } from '@metamask/rpc-errors'; import Engine from '../../Engine'; import NavigationService from '../../NavigationService'; +import { trackWalletEvent } from './v2-analytics'; /** * Known user-rejection error codes across ecosystems. @@ -61,6 +62,8 @@ export class Connection { public readonly hostApp: IHostApplicationAdapter; public readonly bridge: IRPCBridgeAdapter; + private pendingSessionRequestId: unknown = undefined; + private constructor( connInfo: ConnectionInfo, client: WalletClient, @@ -93,6 +96,10 @@ export class Connection { 'method' in payload.data && payload.data.method === 'wallet_createSession'; + if (isWalletCreateSessionRequest) { + this.pendingSessionRequestId = data?.id; + } + // If the request is a wallet_createSession request and there are pending approval requests, clear those pending approvals before // showing the wallet_createSession approval. We do this to prevent the user from seeing a stale wallet_createSession approval in the // scenario where they make a connection request, but leave the wallet before approving or rejecting the request, return to the dapp @@ -147,6 +154,19 @@ export class Connection { if (REJECTION_CODES.has(errCode) || isRejectionMessage(errMessage)) { this.hostApp.showConfirmationRejectionError(this.info); + + if ( + this.pendingSessionRequestId !== undefined && + responseData.id === this.pendingSessionRequestId + ) { + this.pendingSessionRequestId = undefined; + trackWalletEvent('wallet_connection_user_rejected', { + anon_id: this.id, + platform: 'mobile', + sdk_version: this.info.metadata?.sdk?.version, + sdk_platform: this.info.metadata?.sdk?.platform, + }); + } } else if (isInternalError(errCode)) { this.hostApp.showInternalError(this.info); } else { diff --git a/app/core/SDKConnectV2/services/v2-analytics.ts b/app/core/SDKConnectV2/services/v2-analytics.ts new file mode 100644 index 00000000000..bd00d5bba47 --- /dev/null +++ b/app/core/SDKConnectV2/services/v2-analytics.ts @@ -0,0 +1,62 @@ +import logger from './logger'; + +// TODO: Replace this file with `@metamask/analytics` once its `Analytics` +// class supports the `mobile/sdk-connect-v2` namespace. Currently +// `Analytics.track()` is hard-typed to `MMConnectPayload` (namespace +// `metamask/connect`). The upstream work is tracked in: +// https://consensyssoftware.atlassian.net/browse/WAPI-1350 + +const V2_ANALYTICS_ENDPOINT = + 'https://mm-sdk-analytics.api.cx.metamask.io/v2/events'; + +/** + * Event names defined in the `mobile/sdk-connect-v2` namespace of the + * analytics OpenAPI schema. Only the connection-lifecycle subset is + * listed here; action-level events can be added later. + */ +export type WalletConnectionEventName = + | 'wallet_connection_request_received' + | 'wallet_connection_request_failed' + | 'wallet_connection_user_approved' + | 'wallet_connection_user_rejected'; + +export interface WalletEventProperties { + anon_id: string; + platform: 'mobile'; + sdk_version?: string; + sdk_platform?: string; + /** Only set on reconnect (handleSimpleDeeplink) flows. */ + found_in_store?: boolean; +} + +interface MobileSDKConnectV2Payload { + namespace: 'mobile/sdk-connect-v2'; + event_name: WalletConnectionEventName; + properties: WalletEventProperties; +} + +/** + * Fire-and-forget POST to the V2 analytics relay. + * Mirrors the dapp-side `@metamask/analytics` package format so both + * sides land in the same Segment dataset and can be joined on `anon_id`. + */ +export function trackWalletEvent( + eventName: WalletConnectionEventName, + properties: WalletEventProperties, +): void { + const payload: MobileSDKConnectV2Payload[] = [ + { + namespace: 'mobile/sdk-connect-v2', + event_name: eventName, + properties, + }, + ]; + + fetch(V2_ANALYTICS_ENDPOINT, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify(payload), + }).catch((err) => { + logger.error('v2-analytics: failed to send event', eventName, err); + }); +} From 5af23b646e57e3210310269acf31039e4401cb35 Mon Sep 17 00:00:00 2001 From: ffmcgee Date: Wed, 25 Mar 2026 11:36:28 +0100 Subject: [PATCH 02/21] fix: fully apply analytics consent gate --- app/core/SDKConnectV2/services/v2-analytics.ts | 3 +++ 1 file changed, 3 insertions(+) diff --git a/app/core/SDKConnectV2/services/v2-analytics.ts b/app/core/SDKConnectV2/services/v2-analytics.ts index bd00d5bba47..6f11d647886 100644 --- a/app/core/SDKConnectV2/services/v2-analytics.ts +++ b/app/core/SDKConnectV2/services/v2-analytics.ts @@ -1,4 +1,5 @@ import logger from './logger'; +import { analytics } from '../../../util/analytics/analytics'; // TODO: Replace this file with `@metamask/analytics` once its `Analytics` // class supports the `mobile/sdk-connect-v2` namespace. Currently @@ -44,6 +45,8 @@ export function trackWalletEvent( eventName: WalletConnectionEventName, properties: WalletEventProperties, ): void { + if (!analytics.isEnabled()) return; + const payload: MobileSDKConnectV2Payload[] = [ { namespace: 'mobile/sdk-connect-v2', From a714499f1d92fa4ca7bfa03b7ac070047c6da30c Mon Sep 17 00:00:00 2001 From: ffmcgee Date: Wed, 25 Mar 2026 11:41:24 +0100 Subject: [PATCH 03/21] address jiexi feedback --- .../handlers/legacy/handleDeeplink.ts | 2 +- .../types/deepLinkAnalytics.types.ts | 2 +- .../util/deeplinks/deepLinkAnalytics.ts | 6 +++--- .../services/connection-registry.ts | 20 +++++++++++-------- 4 files changed, 17 insertions(+), 13 deletions(-) diff --git a/app/core/DeeplinkManager/handlers/legacy/handleDeeplink.ts b/app/core/DeeplinkManager/handlers/legacy/handleDeeplink.ts index d7f72078bff..f35ffeb15b8 100644 --- a/app/core/DeeplinkManager/handlers/legacy/handleDeeplink.ts +++ b/app/core/DeeplinkManager/handlers/legacy/handleDeeplink.ts @@ -45,7 +45,7 @@ function trackMwpDeepLinkUsed(url: string): void { MetaMetricsEvents.DEEP_LINK_USED, ) .addProperties({ - route: DeepLinkRoute.SDK_MWP, + route: DeepLinkRoute.MMC_MWP, was_app_installed: wasAppInstalled, }) .build(); diff --git a/app/core/DeeplinkManager/types/deepLinkAnalytics.types.ts b/app/core/DeeplinkManager/types/deepLinkAnalytics.types.ts index 5ac1d0f0b94..3555308dc34 100644 --- a/app/core/DeeplinkManager/types/deepLinkAnalytics.types.ts +++ b/app/core/DeeplinkManager/types/deepLinkAnalytics.types.ts @@ -55,7 +55,7 @@ export enum DeepLinkRoute { CARD_ONBOARDING = 'card-onboarding', CARD_HOME = 'card-home', NFT = 'nft', - SDK_MWP = 'sdk_mwp', + MMC_MWP = 'mmc-mwp', INVALID = 'invalid', } diff --git a/app/core/DeeplinkManager/util/deeplinks/deepLinkAnalytics.ts b/app/core/DeeplinkManager/util/deeplinks/deepLinkAnalytics.ts index 934c2fc54fc..10147b9bbb6 100644 --- a/app/core/DeeplinkManager/util/deeplinks/deepLinkAnalytics.ts +++ b/app/core/DeeplinkManager/util/deeplinks/deepLinkAnalytics.ts @@ -431,11 +431,11 @@ const extractNftProperties = ( // NFT route doesn't have sensitive parameters to extract }; -const extractSdkMwpProperties = ( +const extractMmcMwpProperties = ( _urlParams: UrlParamValues, _sensitiveProps: Record, ): void => { - // SDK MWP deeplinks carry their payload in a compressed `p` param; + // MMC MWP deeplinks carry their payload in a compressed `p` param; // no route-level sensitive properties to extract here. }; @@ -479,7 +479,7 @@ const routeExtractors: Record< [DeepLinkRoute.CARD_ONBOARDING]: extractCardOnboardingProperties, [DeepLinkRoute.CARD_HOME]: extractCardHomeProperties, [DeepLinkRoute.NFT]: extractNftProperties, - [DeepLinkRoute.SDK_MWP]: extractSdkMwpProperties, + [DeepLinkRoute.MMC_MWP]: extractMmcMwpProperties, [DeepLinkRoute.INVALID]: extractInvalidProperties, }; diff --git a/app/core/SDKConnectV2/services/connection-registry.ts b/app/core/SDKConnectV2/services/connection-registry.ts index a2e6166a1dd..cf70a61c5fd 100644 --- a/app/core/SDKConnectV2/services/connection-registry.ts +++ b/app/core/SDKConnectV2/services/connection-registry.ts @@ -182,13 +182,12 @@ export class ConnectionRegistry { let conn: Connection | undefined; let connInfo: ConnectionInfo | undefined; - - let baseProps: WalletEventProperties | undefined; + let connReq: ConnectionRequest | undefined; try { - const connReq = this.parseConnectionRequest(url); + connReq = this.parseConnectionRequest(url); - baseProps = { + const baseProps: WalletEventProperties = { anon_id: connReq.sessionRequest.id, platform: 'mobile', sdk_version: connReq.metadata.sdk.version, @@ -232,11 +231,16 @@ export class ConnectionRegistry { this.hostapp.showConnectionError(); if (conn) await this.disconnect(conn.id); - const failProps: WalletEventProperties = baseProps ?? { - anon_id: 'unknown', + // This catch only handles connection-setup failures (parse, network, + // protocol). User rejections of wallet_createSession are asynchronous + // and tracked separately as wallet_connection_user_rejected in + // Connection's response handler — no double-fire occurs. + trackWalletEvent('wallet_connection_request_failed', { + anon_id: connReq?.sessionRequest?.id ?? 'unknown', platform: 'mobile', - }; - trackWalletEvent('wallet_connection_request_failed', failProps); + sdk_version: connReq?.metadata?.sdk?.version, + sdk_platform: connReq?.metadata?.sdk?.platform, + }); } finally { if (connInfo) this.hostapp.hideConnectionLoading(connInfo); } From f732379b121dcfb69c704920b4321e9cd2972057 Mon Sep 17 00:00:00 2001 From: ffmcgee Date: Wed, 25 Mar 2026 12:43:19 +0100 Subject: [PATCH 04/21] test: increase test coverage --- .../legacy/__tests__/handleDeeplink.test.ts | 136 ++++++++++++++++++ 1 file changed, 136 insertions(+) diff --git a/app/core/DeeplinkManager/handlers/legacy/__tests__/handleDeeplink.test.ts b/app/core/DeeplinkManager/handlers/legacy/__tests__/handleDeeplink.test.ts index 2ec67eb08fc..8ab6fea2671 100644 --- a/app/core/DeeplinkManager/handlers/legacy/__tests__/handleDeeplink.test.ts +++ b/app/core/DeeplinkManager/handlers/legacy/__tests__/handleDeeplink.test.ts @@ -3,6 +3,12 @@ import { checkForDeeplink } from '../../../../../actions/user'; import ReduxService from '../../../../redux'; import Logger from '../../../../../util/Logger'; import { AppStateEventProcessor } from '../../../../AppStateEventListener'; +import SDKConnectV2 from '../../../../SDKConnectV2'; +import { analytics } from '../../../../../util/analytics/analytics'; +import { AnalyticsEventBuilder } from '../../../../../util/analytics/AnalyticsEventBuilder'; +import { MetaMetricsEvents } from '../../../../Analytics/MetaMetrics.events'; +import { DeepLinkRoute } from '../../../types/deepLinkAnalytics.types'; +import { detectAppInstallation } from '../../../util/deeplinks/deepLinkAnalytics'; jest.mock('../../../../../actions/user', () => ({ checkForDeeplink: jest.fn(() => ({ type: 'CHECK_FOR_DEEPLINK' })), @@ -27,15 +33,61 @@ jest.mock('../../../../AppStateEventListener', () => ({ }, })); +jest.mock('../../../../SDKConnectV2', () => ({ + __esModule: true, + default: { + isMwpDeeplink: jest.fn(), + handleMwpDeeplink: jest.fn(), + }, +})); + +jest.mock('../../../../../util/analytics/analytics', () => ({ + analytics: { + trackEvent: jest.fn(), + }, +})); + +const mockBuild = jest.fn().mockReturnValue({ event: 'mocked' }); +const mockAddProperties = jest.fn().mockReturnValue({ build: mockBuild }); +jest.mock('../../../../../util/analytics/AnalyticsEventBuilder', () => ({ + AnalyticsEventBuilder: { + createEventBuilder: jest.fn().mockReturnValue({ + addProperties: (...args: unknown[]) => mockAddProperties(...args), + build: (...args: unknown[]) => mockBuild(...args), + }), + }, +})); + +jest.mock('../../../../Analytics/MetaMetrics.events', () => ({ + MetaMetricsEvents: { + DEEP_LINK_USED: 'Deep Link Used', + }, +})); + +jest.mock('../../../types/deepLinkAnalytics.types', () => ({ + DeepLinkRoute: { + MMC_MWP: 'mmc-mwp', + }, +})); + +jest.mock('../../../util/deeplinks/deepLinkAnalytics', () => ({ + detectAppInstallation: jest.fn(), +})); + describe('handleDeeplink', () => { const mockDispatch = ReduxService.store.dispatch as jest.Mock; const mockCheckForDeeplink = checkForDeeplink as jest.Mock; const mockLoggerError = Logger.error as jest.Mock; const mockSetCurrentDeeplink = AppStateEventProcessor.setCurrentDeeplink as jest.Mock; + const mockIsMwpDeeplink = SDKConnectV2.isMwpDeeplink as unknown as jest.Mock; + const mockHandleMwpDeeplink = SDKConnectV2.handleMwpDeeplink as jest.Mock; + const mockTrackEvent = analytics.trackEvent as jest.Mock; + const mockDetectAppInstallation = detectAppInstallation as jest.Mock; beforeEach(() => { jest.clearAllMocks(); + mockIsMwpDeeplink.mockReturnValue(false); }); it('processes valid URI and dispatch checkForDeeplink', () => { @@ -137,4 +189,88 @@ describe('handleDeeplink', () => { expect(mockDispatch).not.toHaveBeenCalled(); expect(mockCheckForDeeplink).not.toHaveBeenCalled(); }); + + describe('MWP deeplink handling', () => { + const mwpUri = 'metamask://mwp/some-compressed-payload'; + + beforeEach(() => { + mockIsMwpDeeplink.mockReturnValue(true); + }); + + it('routes MWP deeplinks to SDKConnectV2 and bypasses the standard flow', () => { + mockDetectAppInstallation.mockResolvedValue(true); + + handleDeeplink({ uri: mwpUri }); + + expect(mockIsMwpDeeplink).toHaveBeenCalledWith(mwpUri); + expect(mockHandleMwpDeeplink).toHaveBeenCalledWith(mwpUri); + expect(mockSetCurrentDeeplink).not.toHaveBeenCalled(); + expect(mockDispatch).not.toHaveBeenCalled(); + expect(mockCheckForDeeplink).not.toHaveBeenCalled(); + }); + + it('fires DEEP_LINK_USED with route MMC_MWP and was_app_installed=true', async () => { + mockDetectAppInstallation.mockResolvedValue(true); + + handleDeeplink({ uri: mwpUri }); + + await flushPromises(); + + expect(mockDetectAppInstallation).toHaveBeenCalled(); + expect(AnalyticsEventBuilder.createEventBuilder).toHaveBeenCalledWith( + MetaMetricsEvents.DEEP_LINK_USED, + ); + expect(mockAddProperties).toHaveBeenCalledWith({ + route: DeepLinkRoute.MMC_MWP, + was_app_installed: true, + }); + expect(mockBuild).toHaveBeenCalled(); + expect(mockTrackEvent).toHaveBeenCalledWith({ event: 'mocked' }); + }); + + it('fires DEEP_LINK_USED with was_app_installed=false for fresh installs', async () => { + mockDetectAppInstallation.mockResolvedValue(false); + + handleDeeplink({ uri: mwpUri }); + + await flushPromises(); + + expect(mockAddProperties).toHaveBeenCalledWith({ + route: DeepLinkRoute.MMC_MWP, + was_app_installed: false, + }); + expect(mockTrackEvent).toHaveBeenCalledWith({ event: 'mocked' }); + }); + + it('logs error when detectAppInstallation rejects', async () => { + const installError = new Error('install check failed'); + mockDetectAppInstallation.mockRejectedValue(installError); + + handleDeeplink({ uri: mwpUri }); + + await flushPromises(); + + expect(mockTrackEvent).not.toHaveBeenCalled(); + expect(mockLoggerError).toHaveBeenCalledWith( + installError, + 'DeepLinkAnalytics: Failed to track MWP deep link event', + ); + }); + + it('still routes to SDKConnectV2 even if analytics fails', async () => { + mockDetectAppInstallation.mockRejectedValue(new Error('oops')); + + handleDeeplink({ uri: mwpUri }); + + expect(mockHandleMwpDeeplink).toHaveBeenCalledWith(mwpUri); + + await flushPromises(); + + expect(mockHandleMwpDeeplink).toHaveBeenCalledTimes(1); + }); + }); }); + +function flushPromises(): Promise { + return new Promise((resolve) => setImmediate(resolve)); +} From 9515c47be129d0c58fb0eda3e5e815ac4373c7c3 Mon Sep 17 00:00:00 2001 From: ffmcgee Date: Wed, 25 Mar 2026 14:20:27 +0100 Subject: [PATCH 05/21] minor change --- app/core/SDKConnectV2/services/v2-analytics.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/core/SDKConnectV2/services/v2-analytics.ts b/app/core/SDKConnectV2/services/v2-analytics.ts index 6f11d647886..b17bba6ac8c 100644 --- a/app/core/SDKConnectV2/services/v2-analytics.ts +++ b/app/core/SDKConnectV2/services/v2-analytics.ts @@ -45,7 +45,7 @@ export function trackWalletEvent( eventName: WalletConnectionEventName, properties: WalletEventProperties, ): void { - if (!analytics.isEnabled()) return; + if (!analytics.isOptedIn()) return; const payload: MobileSDKConnectV2Payload[] = [ { From f7cfbfe46ae5a8e3a611b1aff965e9a942af0a99 Mon Sep 17 00:00:00 2001 From: ffmcgee Date: Wed, 25 Mar 2026 15:12:30 +0100 Subject: [PATCH 06/21] refactor: remove unecessary async behaviour from synchronous function --- app/util/analytics/analytics.test.ts | 10 +++++----- app/util/analytics/analytics.ts | 6 +++--- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/app/util/analytics/analytics.test.ts b/app/util/analytics/analytics.test.ts index 7017f89d9e7..25578e3d85d 100644 --- a/app/util/analytics/analytics.test.ts +++ b/app/util/analytics/analytics.test.ts @@ -366,7 +366,7 @@ describe('analytics', () => { it('returns true when user opted in', async () => { mockedSelectAnalyticsOptedIn.mockReturnValue(true); - const result = await analytics.isOptedIn(); + const result = analytics.isOptedIn(); expect(result).toBe(true); expect(mockedSelectAnalyticsOptedIn).toHaveBeenCalledWith({}); @@ -375,7 +375,7 @@ describe('analytics', () => { it('returns false when user opted out', async () => { mockedSelectAnalyticsOptedIn.mockReturnValue(false); - const result = await analytics.isOptedIn(); + const result = analytics.isOptedIn(); expect(result).toBe(false); }); @@ -385,7 +385,7 @@ describe('analytics', () => { (() => undefined) as unknown as () => boolean, ); - const result = await analytics.isOptedIn(); + const result = analytics.isOptedIn(); expect(result).toBe(false); }); @@ -396,7 +396,7 @@ describe('analytics', () => { throw error; }); - const result = await analytics.isOptedIn(); + const result = analytics.isOptedIn(); expect(result).toBe(false); expect(mockedLoggerLog).toHaveBeenCalledWith( @@ -411,7 +411,7 @@ describe('analytics', () => { throw error; }); - const result = await analytics.isOptedIn(); + const result = analytics.isOptedIn(); expect(result).toBe(false); expect(mockedLoggerLog).toHaveBeenCalledWith( diff --git a/app/util/analytics/analytics.ts b/app/util/analytics/analytics.ts index e54ed68acb3..b229f1c677c 100644 --- a/app/util/analytics/analytics.ts +++ b/app/util/analytics/analytics.ts @@ -25,7 +25,7 @@ export interface AnalyticsHelper { optOut: () => Promise; getAnalyticsId: () => Promise; isEnabled: () => boolean; - isOptedIn: () => Promise; + isOptedIn: () => boolean; } /** @@ -149,9 +149,9 @@ const isEnabled = (): boolean => { /** * Check if user opted in * - * @returns Promise with true if opted in, false otherwise + * @returns True if opted in, false otherwise */ -const isOptedIn = async (): Promise => { +const isOptedIn = (): boolean => { try { const optedIn = selectAnalyticsOptedIn(store.getState()); return optedIn ?? false; From 67cfeb68aee229e905d5e5b87ab60526bf2ec441 Mon Sep 17 00:00:00 2001 From: ffmcgee Date: Thu, 26 Mar 2026 10:13:35 +0100 Subject: [PATCH 07/21] Revert "minor change" This reverts commit 9515c47be129d0c58fb0eda3e5e815ac4373c7c3. --- app/core/SDKConnectV2/services/v2-analytics.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/core/SDKConnectV2/services/v2-analytics.ts b/app/core/SDKConnectV2/services/v2-analytics.ts index b17bba6ac8c..6f11d647886 100644 --- a/app/core/SDKConnectV2/services/v2-analytics.ts +++ b/app/core/SDKConnectV2/services/v2-analytics.ts @@ -45,7 +45,7 @@ export function trackWalletEvent( eventName: WalletConnectionEventName, properties: WalletEventProperties, ): void { - if (!analytics.isOptedIn()) return; + if (!analytics.isEnabled()) return; const payload: MobileSDKConnectV2Payload[] = [ { From 661cd13cd5f18418d8f3a93e9f3259e3be70f7d7 Mon Sep 17 00:00:00 2001 From: ffmcgee Date: Thu, 26 Mar 2026 10:13:43 +0100 Subject: [PATCH 08/21] Revert "refactor: remove unecessary async behaviour from synchronous function" This reverts commit f7cfbfe46ae5a8e3a611b1aff965e9a942af0a99. --- app/util/analytics/analytics.test.ts | 10 +++++----- app/util/analytics/analytics.ts | 6 +++--- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/app/util/analytics/analytics.test.ts b/app/util/analytics/analytics.test.ts index 25578e3d85d..7017f89d9e7 100644 --- a/app/util/analytics/analytics.test.ts +++ b/app/util/analytics/analytics.test.ts @@ -366,7 +366,7 @@ describe('analytics', () => { it('returns true when user opted in', async () => { mockedSelectAnalyticsOptedIn.mockReturnValue(true); - const result = analytics.isOptedIn(); + const result = await analytics.isOptedIn(); expect(result).toBe(true); expect(mockedSelectAnalyticsOptedIn).toHaveBeenCalledWith({}); @@ -375,7 +375,7 @@ describe('analytics', () => { it('returns false when user opted out', async () => { mockedSelectAnalyticsOptedIn.mockReturnValue(false); - const result = analytics.isOptedIn(); + const result = await analytics.isOptedIn(); expect(result).toBe(false); }); @@ -385,7 +385,7 @@ describe('analytics', () => { (() => undefined) as unknown as () => boolean, ); - const result = analytics.isOptedIn(); + const result = await analytics.isOptedIn(); expect(result).toBe(false); }); @@ -396,7 +396,7 @@ describe('analytics', () => { throw error; }); - const result = analytics.isOptedIn(); + const result = await analytics.isOptedIn(); expect(result).toBe(false); expect(mockedLoggerLog).toHaveBeenCalledWith( @@ -411,7 +411,7 @@ describe('analytics', () => { throw error; }); - const result = analytics.isOptedIn(); + const result = await analytics.isOptedIn(); expect(result).toBe(false); expect(mockedLoggerLog).toHaveBeenCalledWith( diff --git a/app/util/analytics/analytics.ts b/app/util/analytics/analytics.ts index b229f1c677c..e54ed68acb3 100644 --- a/app/util/analytics/analytics.ts +++ b/app/util/analytics/analytics.ts @@ -25,7 +25,7 @@ export interface AnalyticsHelper { optOut: () => Promise; getAnalyticsId: () => Promise; isEnabled: () => boolean; - isOptedIn: () => boolean; + isOptedIn: () => Promise; } /** @@ -149,9 +149,9 @@ const isEnabled = (): boolean => { /** * Check if user opted in * - * @returns True if opted in, false otherwise + * @returns Promise with true if opted in, false otherwise */ -const isOptedIn = (): boolean => { +const isOptedIn = async (): Promise => { try { const optedIn = selectAnalyticsOptedIn(store.getState()); return optedIn ?? false; From ee26b034ee6aff9b9893ddebb0309762990f1d44 Mon Sep 17 00:00:00 2001 From: ffmcgee Date: Thu, 26 Mar 2026 10:16:28 +0100 Subject: [PATCH 09/21] test: coverage for trackWalletEvent function call --- .../services/v2-analytics.test.ts | 134 ++++++++++++++++++ 1 file changed, 134 insertions(+) create mode 100644 app/core/SDKConnectV2/services/v2-analytics.test.ts diff --git a/app/core/SDKConnectV2/services/v2-analytics.test.ts b/app/core/SDKConnectV2/services/v2-analytics.test.ts new file mode 100644 index 00000000000..b18eead2ab1 --- /dev/null +++ b/app/core/SDKConnectV2/services/v2-analytics.test.ts @@ -0,0 +1,134 @@ +import { analytics } from '../../../util/analytics/analytics'; +import logger from './logger'; +import { + trackWalletEvent, + type WalletConnectionEventName, + type WalletEventProperties, +} from './v2-analytics'; + +jest.mock('../../../util/analytics/analytics', () => ({ + analytics: { + isEnabled: jest.fn(), + }, +})); + +jest.mock('./logger', () => ({ + __esModule: true, + default: { + error: jest.fn(), + }, +})); + +const mockAnalytics = analytics as jest.Mocked; +const mockLogger = logger as jest.Mocked; + +const V2_ANALYTICS_ENDPOINT = + 'https://mm-sdk-analytics.api.cx.metamask.io/v2/events'; + +const createProperties = ( + overrides: Partial = {}, +): WalletEventProperties => ({ + anon_id: 'test-anon-id', + platform: 'mobile', + ...overrides, +}); + +describe('trackWalletEvent', () => { + let fetchSpy: jest.SpyInstance; + + beforeEach(() => { + jest.clearAllMocks(); + fetchSpy = jest + .spyOn(globalThis, 'fetch') + .mockResolvedValue(new Response(null, { status: 200 })); + }); + + afterEach(() => { + fetchSpy.mockRestore(); + }); + + it('does not send event when analytics is disabled', () => { + mockAnalytics.isEnabled.mockReturnValue(false); + const properties = createProperties(); + + trackWalletEvent('wallet_connection_request_received', properties); + + expect(fetchSpy).not.toHaveBeenCalled(); + }); + + it('sends POST request with correct payload when analytics is enabled', () => { + mockAnalytics.isEnabled.mockReturnValue(true); + const eventName: WalletConnectionEventName = + 'wallet_connection_user_approved'; + const properties = createProperties({ sdk_version: '1.0.0' }); + + trackWalletEvent(eventName, properties); + + expect(fetchSpy).toHaveBeenCalledWith(V2_ANALYTICS_ENDPOINT, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify([ + { + namespace: 'mobile/sdk-connect-v2', + event_name: eventName, + properties, + }, + ]), + }); + }); + + it('sends payload wrapped in a single-element array', () => { + mockAnalytics.isEnabled.mockReturnValue(true); + const properties = createProperties(); + + trackWalletEvent('wallet_connection_request_failed', properties); + + const body = JSON.parse(fetchSpy.mock.calls[0][1].body); + expect(body).toHaveLength(1); + expect(body[0].namespace).toBe('mobile/sdk-connect-v2'); + }); + + it('includes optional properties in the payload', () => { + mockAnalytics.isEnabled.mockReturnValue(true); + const properties = createProperties({ + sdk_version: '2.0.0', + sdk_platform: 'react-native', + found_in_store: true, + }); + + trackWalletEvent('wallet_connection_user_rejected', properties); + + const body = JSON.parse(fetchSpy.mock.calls[0][1].body); + expect(body[0].properties).toStrictEqual(properties); + }); + + it('logs error when fetch rejects', async () => { + mockAnalytics.isEnabled.mockReturnValue(true); + const networkError = new Error('Network failure'); + fetchSpy.mockRejectedValue(networkError); + const eventName: WalletConnectionEventName = + 'wallet_connection_request_received'; + const properties = createProperties(); + + trackWalletEvent(eventName, properties); + + await new Promise(process.nextTick); + + expect(mockLogger.error).toHaveBeenCalledWith( + 'v2-analytics: failed to send event', + eventName, + networkError, + ); + }); + + it('does not log error when fetch resolves', async () => { + mockAnalytics.isEnabled.mockReturnValue(true); + const properties = createProperties(); + + trackWalletEvent('wallet_connection_request_received', properties); + + await new Promise(process.nextTick); + + expect(mockLogger.error).not.toHaveBeenCalled(); + }); +}); From d43807313327cef7bed7e1c821c205e56088922c Mon Sep 17 00:00:00 2001 From: Alex Donesky Date: Thu, 2 Apr 2026 14:42:06 -0500 Subject: [PATCH 10/21] refactor: remove redundant wallet_connection_user_approved/rejected events MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit With the useOriginSource fix (WAPI-1380), the existing MetaMetrics events CONNECT_REQUEST_COMPLETED and CONNECT_REQUEST_CANCELLED now correctly carry source: 'sdk_connect_v2' for MWP connections, making the dedicated wallet_connection_user_approved and wallet_connection_user_rejected V2 relay events redundant. Removed: - wallet_connection_user_approved from connection-registry.ts - wallet_connection_user_rejected from connection.ts (plus the pendingSessionRequestId tracking machinery that existed solely for it) - Both event names from the WalletConnectionEventName type Kept (no existing MetaMetrics event covers these): - wallet_connection_request_received — fires at deeplink parse time, before transport/approval, needed to measure dapp-to-wallet drop-off - wallet_connection_request_failed — catches pre-permission transport and parsing failures --- .../services/connection-registry.ts | 7 +++---- app/core/SDKConnectV2/services/connection.ts | 20 ------------------- .../services/v2-analytics.test.ts | 4 ++-- .../SDKConnectV2/services/v2-analytics.ts | 4 +--- 4 files changed, 6 insertions(+), 29 deletions(-) diff --git a/app/core/SDKConnectV2/services/connection-registry.ts b/app/core/SDKConnectV2/services/connection-registry.ts index 2eb3b0a7da0..556fda8a0f5 100644 --- a/app/core/SDKConnectV2/services/connection-registry.ts +++ b/app/core/SDKConnectV2/services/connection-registry.ts @@ -263,7 +263,6 @@ export class ConnectionRegistry { await this.store.save(connInfo); this.hostapp.syncConnectionList(Array.from(this.connections.values())); - trackWalletEvent('wallet_connection_user_approved', baseProps); logger.debug('Handled connect deeplink.', connInfo?.id); } catch (error) { logger.error('Failed to handle connect deeplink:', error, redactUrl(url)); @@ -271,9 +270,9 @@ export class ConnectionRegistry { if (conn) await this.disconnect(conn.id); // This catch only handles connection-setup failures (parse, network, - // protocol). User rejections of wallet_createSession are asynchronous - // and tracked separately as wallet_connection_user_rejected in - // Connection's response handler — no double-fire occurs. + // protocol). User rejections of wallet_createSession are tracked by + // the existing CONNECT_REQUEST_CANCELLED MetaMetrics event (with + // source: 'sdk_connect_v2') — no double-fire occurs. trackWalletEvent('wallet_connection_request_failed', { anon_id: connReq?.sessionRequest?.id ?? 'unknown', platform: 'mobile', diff --git a/app/core/SDKConnectV2/services/connection.ts b/app/core/SDKConnectV2/services/connection.ts index bb7bd5bb94d..30b39280678 100644 --- a/app/core/SDKConnectV2/services/connection.ts +++ b/app/core/SDKConnectV2/services/connection.ts @@ -16,7 +16,6 @@ import { IHostApplicationAdapter } from '../types/host-application-adapter'; import { errorCodes, providerErrors } from '@metamask/rpc-errors'; import Engine from '../../Engine'; import NavigationService from '../../NavigationService'; -import { trackWalletEvent } from './v2-analytics'; /** * Known user-rejection error codes across ecosystems. @@ -62,8 +61,6 @@ export class Connection { public readonly hostApp: IHostApplicationAdapter; public readonly bridge: IRPCBridgeAdapter; - private pendingSessionRequestId: unknown = undefined; - private constructor( connInfo: ConnectionInfo, client: WalletClient, @@ -96,10 +93,6 @@ export class Connection { 'method' in payload.data && payload.data.method === 'wallet_createSession'; - if (isWalletCreateSessionRequest) { - this.pendingSessionRequestId = data?.id; - } - // If the request is a wallet_createSession request and there are pending approval requests, clear those pending approvals before // showing the wallet_createSession approval. We do this to prevent the user from seeing a stale wallet_createSession approval in the // scenario where they make a connection request, but leave the wallet before approving or rejecting the request, return to the dapp @@ -154,19 +147,6 @@ export class Connection { if (REJECTION_CODES.has(errCode) || isRejectionMessage(errMessage)) { this.hostApp.showConfirmationRejectionError(this.info); - - if ( - this.pendingSessionRequestId !== undefined && - responseData.id === this.pendingSessionRequestId - ) { - this.pendingSessionRequestId = undefined; - trackWalletEvent('wallet_connection_user_rejected', { - anon_id: this.id, - platform: 'mobile', - sdk_version: this.info.metadata?.sdk?.version, - sdk_platform: this.info.metadata?.sdk?.platform, - }); - } } else if (isInternalError(errCode)) { this.hostApp.showInternalError(this.info); } else { diff --git a/app/core/SDKConnectV2/services/v2-analytics.test.ts b/app/core/SDKConnectV2/services/v2-analytics.test.ts index b18eead2ab1..4900cf627be 100644 --- a/app/core/SDKConnectV2/services/v2-analytics.test.ts +++ b/app/core/SDKConnectV2/services/v2-analytics.test.ts @@ -59,7 +59,7 @@ describe('trackWalletEvent', () => { it('sends POST request with correct payload when analytics is enabled', () => { mockAnalytics.isEnabled.mockReturnValue(true); const eventName: WalletConnectionEventName = - 'wallet_connection_user_approved'; + 'wallet_connection_request_received'; const properties = createProperties({ sdk_version: '1.0.0' }); trackWalletEvent(eventName, properties); @@ -96,7 +96,7 @@ describe('trackWalletEvent', () => { found_in_store: true, }); - trackWalletEvent('wallet_connection_user_rejected', properties); + trackWalletEvent('wallet_connection_request_failed', properties); const body = JSON.parse(fetchSpy.mock.calls[0][1].body); expect(body[0].properties).toStrictEqual(properties); diff --git a/app/core/SDKConnectV2/services/v2-analytics.ts b/app/core/SDKConnectV2/services/v2-analytics.ts index 6f11d647886..f063d2027cc 100644 --- a/app/core/SDKConnectV2/services/v2-analytics.ts +++ b/app/core/SDKConnectV2/services/v2-analytics.ts @@ -17,9 +17,7 @@ const V2_ANALYTICS_ENDPOINT = */ export type WalletConnectionEventName = | 'wallet_connection_request_received' - | 'wallet_connection_request_failed' - | 'wallet_connection_user_approved' - | 'wallet_connection_user_rejected'; + | 'wallet_connection_request_failed'; export interface WalletEventProperties { anon_id: string; From 1c6e53c5b6eeae775c25ecf48ec08137ac9c8c32 Mon Sep 17 00:00:00 2001 From: Alex Donesky Date: Thu, 9 Apr 2026 16:08:05 -0500 Subject: [PATCH 11/21] refactor: align MWP event names and properties with Segment schema MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Rename `wallet_connection_request_received` → `Remote Connection Request Received` - Rename `wallet_connection_request_failed` → `Remote Connection Request Failed` to match the finalized naming convention in segment-schema PRs #519/#520 - Rename `anon_id` property → `remote_session_id` for clarity - Add `failure_reason` to the failed event for better diagnostics - Update corresponding test assertions --- .../services/connection-registry.ts | 18 ++++++++++-------- .../SDKConnectV2/services/v2-analytics.test.ts | 14 +++++++------- app/core/SDKConnectV2/services/v2-analytics.ts | 9 +++++---- 3 files changed, 22 insertions(+), 19 deletions(-) diff --git a/app/core/SDKConnectV2/services/connection-registry.ts b/app/core/SDKConnectV2/services/connection-registry.ts index 556fda8a0f5..5986cc4b346 100644 --- a/app/core/SDKConnectV2/services/connection-registry.ts +++ b/app/core/SDKConnectV2/services/connection-registry.ts @@ -152,8 +152,8 @@ export class ConnectionRegistry { const conn = await this.store.get(id); if (conn) { - trackWalletEvent('wallet_connection_request_received', { - anon_id: id, + trackWalletEvent('Remote Connection Request Received', { + remote_session_id: id, platform: 'mobile', sdk_version: conn.metadata?.sdk?.version, sdk_platform: conn.metadata?.sdk?.platform, @@ -162,8 +162,8 @@ export class ConnectionRegistry { return; } - trackWalletEvent('wallet_connection_request_received', { - anon_id: id, + trackWalletEvent('Remote Connection Request Received', { + remote_session_id: id, platform: 'mobile', found_in_store: false, }); @@ -226,12 +226,12 @@ export class ConnectionRegistry { connReq = this.parseConnectionRequest(url); const baseProps: WalletEventProperties = { - anon_id: connReq.sessionRequest.id, + remote_session_id: connReq.sessionRequest.id, platform: 'mobile', sdk_version: connReq.metadata.sdk.version, sdk_platform: connReq.metadata.sdk.platform, }; - trackWalletEvent('wallet_connection_request_received', baseProps); + trackWalletEvent('Remote Connection Request Received', baseProps); // Defense-in-depth: block connections whose self-reported dapp metadata // matches a known internal origin. This check is currently redundant @@ -273,11 +273,13 @@ export class ConnectionRegistry { // protocol). User rejections of wallet_createSession are tracked by // the existing CONNECT_REQUEST_CANCELLED MetaMetrics event (with // source: 'sdk_connect_v2') — no double-fire occurs. - trackWalletEvent('wallet_connection_request_failed', { - anon_id: connReq?.sessionRequest?.id ?? 'unknown', + trackWalletEvent('Remote Connection Request Failed', { + remote_session_id: connReq?.sessionRequest?.id ?? 'unknown', platform: 'mobile', sdk_version: connReq?.metadata?.sdk?.version, sdk_platform: connReq?.metadata?.sdk?.platform, + failure_reason: + error instanceof Error ? error.message : String(error), }); } finally { if (connInfo) this.hostapp.hideConnectionLoading(connInfo); diff --git a/app/core/SDKConnectV2/services/v2-analytics.test.ts b/app/core/SDKConnectV2/services/v2-analytics.test.ts index 4900cf627be..665f0068633 100644 --- a/app/core/SDKConnectV2/services/v2-analytics.test.ts +++ b/app/core/SDKConnectV2/services/v2-analytics.test.ts @@ -28,7 +28,7 @@ const V2_ANALYTICS_ENDPOINT = const createProperties = ( overrides: Partial = {}, ): WalletEventProperties => ({ - anon_id: 'test-anon-id', + remote_session_id: 'test-anon-id', platform: 'mobile', ...overrides, }); @@ -51,7 +51,7 @@ describe('trackWalletEvent', () => { mockAnalytics.isEnabled.mockReturnValue(false); const properties = createProperties(); - trackWalletEvent('wallet_connection_request_received', properties); + trackWalletEvent('Remote Connection Request Received', properties); expect(fetchSpy).not.toHaveBeenCalled(); }); @@ -59,7 +59,7 @@ describe('trackWalletEvent', () => { it('sends POST request with correct payload when analytics is enabled', () => { mockAnalytics.isEnabled.mockReturnValue(true); const eventName: WalletConnectionEventName = - 'wallet_connection_request_received'; + 'Remote Connection Request Received'; const properties = createProperties({ sdk_version: '1.0.0' }); trackWalletEvent(eventName, properties); @@ -81,7 +81,7 @@ describe('trackWalletEvent', () => { mockAnalytics.isEnabled.mockReturnValue(true); const properties = createProperties(); - trackWalletEvent('wallet_connection_request_failed', properties); + trackWalletEvent('Remote Connection Request Failed', properties); const body = JSON.parse(fetchSpy.mock.calls[0][1].body); expect(body).toHaveLength(1); @@ -96,7 +96,7 @@ describe('trackWalletEvent', () => { found_in_store: true, }); - trackWalletEvent('wallet_connection_request_failed', properties); + trackWalletEvent('Remote Connection Request Failed', properties); const body = JSON.parse(fetchSpy.mock.calls[0][1].body); expect(body[0].properties).toStrictEqual(properties); @@ -107,7 +107,7 @@ describe('trackWalletEvent', () => { const networkError = new Error('Network failure'); fetchSpy.mockRejectedValue(networkError); const eventName: WalletConnectionEventName = - 'wallet_connection_request_received'; + 'Remote Connection Request Received'; const properties = createProperties(); trackWalletEvent(eventName, properties); @@ -125,7 +125,7 @@ describe('trackWalletEvent', () => { mockAnalytics.isEnabled.mockReturnValue(true); const properties = createProperties(); - trackWalletEvent('wallet_connection_request_received', properties); + trackWalletEvent('Remote Connection Request Received', properties); await new Promise(process.nextTick); diff --git a/app/core/SDKConnectV2/services/v2-analytics.ts b/app/core/SDKConnectV2/services/v2-analytics.ts index f063d2027cc..0d247c1bd25 100644 --- a/app/core/SDKConnectV2/services/v2-analytics.ts +++ b/app/core/SDKConnectV2/services/v2-analytics.ts @@ -16,16 +16,17 @@ const V2_ANALYTICS_ENDPOINT = * listed here; action-level events can be added later. */ export type WalletConnectionEventName = - | 'wallet_connection_request_received' - | 'wallet_connection_request_failed'; + | 'Remote Connection Request Received' + | 'Remote Connection Request Failed'; export interface WalletEventProperties { - anon_id: string; + remote_session_id: string; platform: 'mobile'; sdk_version?: string; sdk_platform?: string; /** Only set on reconnect (handleSimpleDeeplink) flows. */ found_in_store?: boolean; + failure_reason?: string; } interface MobileSDKConnectV2Payload { @@ -37,7 +38,7 @@ interface MobileSDKConnectV2Payload { /** * Fire-and-forget POST to the V2 analytics relay. * Mirrors the dapp-side `@metamask/analytics` package format so both - * sides land in the same Segment dataset and can be joined on `anon_id`. + * sides land in the same Segment dataset and can be joined on `remote_session_id`. */ export function trackWalletEvent( eventName: WalletConnectionEventName, From 265b384f81d807024bf6970789bfdbcd2e8ca90c Mon Sep 17 00:00:00 2001 From: ffmcgee Date: Tue, 14 Apr 2026 12:57:24 +0200 Subject: [PATCH 12/21] refactor: use the proper MetaMetrics flow rather than the out of band relay --- app/core/Analytics/MetaMetrics.events.ts | 12 ++ .../services/connection-registry.test.ts | 63 ++++++++ .../services/connection-registry.ts | 81 +++++++---- .../services/v2-analytics.test.ts | 134 ------------------ .../SDKConnectV2/services/v2-analytics.ts | 64 --------- 5 files changed, 128 insertions(+), 226 deletions(-) delete mode 100644 app/core/SDKConnectV2/services/v2-analytics.test.ts delete mode 100644 app/core/SDKConnectV2/services/v2-analytics.ts diff --git a/app/core/Analytics/MetaMetrics.events.ts b/app/core/Analytics/MetaMetrics.events.ts index 55ba628c1dd..0889ea36031 100644 --- a/app/core/Analytics/MetaMetrics.events.ts +++ b/app/core/Analytics/MetaMetrics.events.ts @@ -678,6 +678,10 @@ enum EVENT_NAME { // Assets ASSETS_FIRST_INIT_FETCH_COMPLETED = 'Assets First Init Fetch Completed', + + // Remote connection events (SDK v1 socket relay, MWP, and WalletConnect) + REMOTE_CONNECTION_REQUEST_RECEIVED = 'Remote Connection Request Received', + REMOTE_CONNECTION_REQUEST_FAILED = 'Remote Connection Request Failed', } export enum HARDWARE_WALLET_BUTTON_TYPE { @@ -1757,6 +1761,14 @@ const events = { MUSD_QUICK_CONVERT_TOKEN_ROW_BUTTON_CLICKED: generateOpt( EVENT_NAME.MUSD_QUICK_CONVERT_TOKEN_ROW_BUTTON_CLICKED, ), + + // Remote connection events (SDK v1 socket relay, MWP, and WalletConnect) + REMOTE_CONNECTION_REQUEST_RECEIVED: generateOpt( + EVENT_NAME.REMOTE_CONNECTION_REQUEST_RECEIVED, + ), + REMOTE_CONNECTION_REQUEST_FAILED: generateOpt( + EVENT_NAME.REMOTE_CONNECTION_REQUEST_FAILED, + ), }; /** diff --git a/app/core/SDKConnectV2/services/connection-registry.test.ts b/app/core/SDKConnectV2/services/connection-registry.test.ts index 465039f5544..b97ae483c85 100644 --- a/app/core/SDKConnectV2/services/connection-registry.test.ts +++ b/app/core/SDKConnectV2/services/connection-registry.test.ts @@ -7,6 +7,8 @@ import { Connection } from './connection'; import { ConnectionRequest } from '../types/connection-request'; import { ConnectionInfo } from '../types/connection-info'; import Engine from '../../Engine'; +import { analytics } from '../../../util/analytics/analytics'; +import { MetaMetricsEvents } from '../../Analytics'; jest.mock('../adapters/host-application-adapter'); jest.mock('../store/connection-store'); @@ -15,6 +17,11 @@ jest.mock('./connection'); jest.mock('react-native'); jest.mock('@sentry/react-native'); jest.mock('../../Permissions'); +jest.mock('../../../util/analytics/analytics', () => ({ + analytics: { + trackEvent: jest.fn(), + }, +})); jest.mock('../../../store', () => ({ store: { dispatch: jest.fn(), @@ -24,6 +31,8 @@ jest.mock('../../../store', () => ({ }, })); +const mockTrackEvent = analytics.trackEvent as jest.Mock; + // Factory functions for creating mock objects // eslint-disable-next-line @typescript-eslint/no-explicit-any const createMockConnection = (id: string, overrides: any = {}) => ({ @@ -285,6 +294,17 @@ describe('ConnectionRegistry', () => { expect(mockStore.get).toHaveBeenCalledWith('mock-conn-id'); expect(mockHostApp.showNotFoundError).not.toHaveBeenCalled(); + + expect(mockTrackEvent).toHaveBeenCalledTimes(1); + const trackedEvent = mockTrackEvent.mock.calls[0][0]; + expect(trackedEvent.name).toBe('Remote Connection Request Received'); + expect(trackedEvent.properties).toEqual( + expect.objectContaining({ + remote_session_id: 'mock-conn-id', + platform: 'mobile', + found_in_store: true, + }), + ); }); describe('when the connection is not found in the store', () => { @@ -296,11 +316,24 @@ describe('ConnectionRegistry', () => { mockStore, ); + const eventName = + MetaMetricsEvents.REMOTE_CONNECTION_REQUEST_RECEIVED.category; await registry.handleSimpleDeeplink('mock-conn-id'); await jest.advanceTimersByTimeAsync(1000); expect(mockStore.get).toHaveBeenCalledWith('mock-conn-id'); expect(mockHostApp.showNotFoundError).toHaveBeenCalled(); + + expect(mockTrackEvent).toHaveBeenCalledTimes(1); + const trackedEvent = mockTrackEvent.mock.calls[0][0]; + expect(trackedEvent.name).toBe(eventName); + expect(trackedEvent.properties).toEqual( + expect.objectContaining({ + remote_session_id: 'mock-conn-id', + platform: 'mobile', + found_in_store: false, + }), + ); }); it('should show error if the keyring is not unlocked but becomes unlocked later', async () => { @@ -348,6 +381,9 @@ describe('ConnectionRegistry', () => { mockStore, ); + const eventName = + MetaMetricsEvents.REMOTE_CONNECTION_REQUEST_RECEIVED.category; + await registry.handleConnectDeeplink(validDeeplink); // UI loading state is properly managed @@ -394,6 +430,19 @@ describe('ConnectionRegistry', () => { expiresAt: expect.any(Number), }), ); + + // Analytics: "received" event is tracked via MetaMetrics pipeline + expect(mockTrackEvent).toHaveBeenCalledTimes(1); + const trackedEvent = mockTrackEvent.mock.calls[0][0]; + expect(trackedEvent.name).toBe(eventName); + expect(trackedEvent.properties).toEqual( + expect.objectContaining({ + remote_session_id: mockConnectionRequest.sessionRequest.id, + platform: 'mobile', + sdk_version: '2.0.0', + sdk_platform: 'JavaScript', + }), + ); }); it('should handle invalid URL gracefully', async () => { @@ -491,6 +540,8 @@ describe('ConnectionRegistry', () => { mockConnection.connect.mockRejectedValue(connectionError); const disconnectSpy = jest.spyOn(registry, 'disconnect'); + const eventName = + MetaMetricsEvents.REMOTE_CONNECTION_REQUEST_FAILED.category; await registry.handleConnectDeeplink(validDeeplink); @@ -523,6 +574,18 @@ describe('ConnectionRegistry', () => { }), ); + // Analytics: both "received" and "failed" events are tracked + expect(mockTrackEvent).toHaveBeenCalledTimes(2); + const failedEvent = mockTrackEvent.mock.calls[1][0]; + expect(failedEvent.name).toBe(eventName); + expect(failedEvent.properties).toEqual( + expect.objectContaining({ + remote_session_id: mockConnectionRequest.sessionRequest.id, + platform: 'mobile', + failure_reason: 'Connection failed', + }), + ); + disconnectSpy.mockRestore(); }); diff --git a/app/core/SDKConnectV2/services/connection-registry.ts b/app/core/SDKConnectV2/services/connection-registry.ts index 5986cc4b346..098f1123bf5 100644 --- a/app/core/SDKConnectV2/services/connection-registry.ts +++ b/app/core/SDKConnectV2/services/connection-registry.ts @@ -18,7 +18,9 @@ import { whenStoreReady } from '../utils/when-store-ready'; import Engine from '../../Engine'; import { rpcErrors } from '@metamask/rpc-errors'; import { INTERNAL_ORIGINS } from '../../../constants/transaction'; -import { trackWalletEvent, type WalletEventProperties } from './v2-analytics'; +import { analytics } from '../../../util/analytics/analytics'; +import { AnalyticsEventBuilder } from '../../../util/analytics/AnalyticsEventBuilder'; +import { MetaMetricsEvents } from '../../Analytics/MetaMetrics.events'; /** * Hard cap on the number of simultaneous active connections. @@ -152,21 +154,33 @@ export class ConnectionRegistry { const conn = await this.store.get(id); if (conn) { - trackWalletEvent('Remote Connection Request Received', { - remote_session_id: id, - platform: 'mobile', - sdk_version: conn.metadata?.sdk?.version, - sdk_platform: conn.metadata?.sdk?.platform, - found_in_store: true, - }); + analytics.trackEvent( + AnalyticsEventBuilder.createEventBuilder( + MetaMetricsEvents.REMOTE_CONNECTION_REQUEST_RECEIVED, + ) + .addProperties({ + remote_session_id: id, + platform: 'mobile', + sdk_version: conn.metadata?.sdk?.version, + sdk_platform: conn.metadata?.sdk?.platform, + found_in_store: true, + }) + .build(), + ); return; } - trackWalletEvent('Remote Connection Request Received', { - remote_session_id: id, - platform: 'mobile', - found_in_store: false, - }); + analytics.trackEvent( + AnalyticsEventBuilder.createEventBuilder( + MetaMetricsEvents.REMOTE_CONNECTION_REQUEST_RECEIVED, + ) + .addProperties({ + remote_session_id: id, + platform: 'mobile', + found_in_store: false, + }) + .build(), + ); logger.error( 'Failed to find connection in store for simple deeplink with id:', @@ -225,13 +239,18 @@ export class ConnectionRegistry { try { connReq = this.parseConnectionRequest(url); - const baseProps: WalletEventProperties = { - remote_session_id: connReq.sessionRequest.id, - platform: 'mobile', - sdk_version: connReq.metadata.sdk.version, - sdk_platform: connReq.metadata.sdk.platform, - }; - trackWalletEvent('Remote Connection Request Received', baseProps); + analytics.trackEvent( + AnalyticsEventBuilder.createEventBuilder( + MetaMetricsEvents.REMOTE_CONNECTION_REQUEST_RECEIVED, + ) + .addProperties({ + remote_session_id: connReq.sessionRequest.id, + platform: 'mobile', + sdk_version: connReq.metadata.sdk.version, + sdk_platform: connReq.metadata.sdk.platform, + }) + .build(), + ); // Defense-in-depth: block connections whose self-reported dapp metadata // matches a known internal origin. This check is currently redundant @@ -273,14 +292,20 @@ export class ConnectionRegistry { // protocol). User rejections of wallet_createSession are tracked by // the existing CONNECT_REQUEST_CANCELLED MetaMetrics event (with // source: 'sdk_connect_v2') — no double-fire occurs. - trackWalletEvent('Remote Connection Request Failed', { - remote_session_id: connReq?.sessionRequest?.id ?? 'unknown', - platform: 'mobile', - sdk_version: connReq?.metadata?.sdk?.version, - sdk_platform: connReq?.metadata?.sdk?.platform, - failure_reason: - error instanceof Error ? error.message : String(error), - }); + analytics.trackEvent( + AnalyticsEventBuilder.createEventBuilder( + MetaMetricsEvents.REMOTE_CONNECTION_REQUEST_FAILED, + ) + .addProperties({ + remote_session_id: connReq?.sessionRequest?.id ?? 'unknown', + platform: 'mobile', + sdk_version: connReq?.metadata?.sdk?.version, + sdk_platform: connReq?.metadata?.sdk?.platform, + failure_reason: + error instanceof Error ? error.message : String(error), + }) + .build(), + ); } finally { if (connInfo) this.hostapp.hideConnectionLoading(connInfo); } diff --git a/app/core/SDKConnectV2/services/v2-analytics.test.ts b/app/core/SDKConnectV2/services/v2-analytics.test.ts deleted file mode 100644 index 665f0068633..00000000000 --- a/app/core/SDKConnectV2/services/v2-analytics.test.ts +++ /dev/null @@ -1,134 +0,0 @@ -import { analytics } from '../../../util/analytics/analytics'; -import logger from './logger'; -import { - trackWalletEvent, - type WalletConnectionEventName, - type WalletEventProperties, -} from './v2-analytics'; - -jest.mock('../../../util/analytics/analytics', () => ({ - analytics: { - isEnabled: jest.fn(), - }, -})); - -jest.mock('./logger', () => ({ - __esModule: true, - default: { - error: jest.fn(), - }, -})); - -const mockAnalytics = analytics as jest.Mocked; -const mockLogger = logger as jest.Mocked; - -const V2_ANALYTICS_ENDPOINT = - 'https://mm-sdk-analytics.api.cx.metamask.io/v2/events'; - -const createProperties = ( - overrides: Partial = {}, -): WalletEventProperties => ({ - remote_session_id: 'test-anon-id', - platform: 'mobile', - ...overrides, -}); - -describe('trackWalletEvent', () => { - let fetchSpy: jest.SpyInstance; - - beforeEach(() => { - jest.clearAllMocks(); - fetchSpy = jest - .spyOn(globalThis, 'fetch') - .mockResolvedValue(new Response(null, { status: 200 })); - }); - - afterEach(() => { - fetchSpy.mockRestore(); - }); - - it('does not send event when analytics is disabled', () => { - mockAnalytics.isEnabled.mockReturnValue(false); - const properties = createProperties(); - - trackWalletEvent('Remote Connection Request Received', properties); - - expect(fetchSpy).not.toHaveBeenCalled(); - }); - - it('sends POST request with correct payload when analytics is enabled', () => { - mockAnalytics.isEnabled.mockReturnValue(true); - const eventName: WalletConnectionEventName = - 'Remote Connection Request Received'; - const properties = createProperties({ sdk_version: '1.0.0' }); - - trackWalletEvent(eventName, properties); - - expect(fetchSpy).toHaveBeenCalledWith(V2_ANALYTICS_ENDPOINT, { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify([ - { - namespace: 'mobile/sdk-connect-v2', - event_name: eventName, - properties, - }, - ]), - }); - }); - - it('sends payload wrapped in a single-element array', () => { - mockAnalytics.isEnabled.mockReturnValue(true); - const properties = createProperties(); - - trackWalletEvent('Remote Connection Request Failed', properties); - - const body = JSON.parse(fetchSpy.mock.calls[0][1].body); - expect(body).toHaveLength(1); - expect(body[0].namespace).toBe('mobile/sdk-connect-v2'); - }); - - it('includes optional properties in the payload', () => { - mockAnalytics.isEnabled.mockReturnValue(true); - const properties = createProperties({ - sdk_version: '2.0.0', - sdk_platform: 'react-native', - found_in_store: true, - }); - - trackWalletEvent('Remote Connection Request Failed', properties); - - const body = JSON.parse(fetchSpy.mock.calls[0][1].body); - expect(body[0].properties).toStrictEqual(properties); - }); - - it('logs error when fetch rejects', async () => { - mockAnalytics.isEnabled.mockReturnValue(true); - const networkError = new Error('Network failure'); - fetchSpy.mockRejectedValue(networkError); - const eventName: WalletConnectionEventName = - 'Remote Connection Request Received'; - const properties = createProperties(); - - trackWalletEvent(eventName, properties); - - await new Promise(process.nextTick); - - expect(mockLogger.error).toHaveBeenCalledWith( - 'v2-analytics: failed to send event', - eventName, - networkError, - ); - }); - - it('does not log error when fetch resolves', async () => { - mockAnalytics.isEnabled.mockReturnValue(true); - const properties = createProperties(); - - trackWalletEvent('Remote Connection Request Received', properties); - - await new Promise(process.nextTick); - - expect(mockLogger.error).not.toHaveBeenCalled(); - }); -}); diff --git a/app/core/SDKConnectV2/services/v2-analytics.ts b/app/core/SDKConnectV2/services/v2-analytics.ts deleted file mode 100644 index 0d247c1bd25..00000000000 --- a/app/core/SDKConnectV2/services/v2-analytics.ts +++ /dev/null @@ -1,64 +0,0 @@ -import logger from './logger'; -import { analytics } from '../../../util/analytics/analytics'; - -// TODO: Replace this file with `@metamask/analytics` once its `Analytics` -// class supports the `mobile/sdk-connect-v2` namespace. Currently -// `Analytics.track()` is hard-typed to `MMConnectPayload` (namespace -// `metamask/connect`). The upstream work is tracked in: -// https://consensyssoftware.atlassian.net/browse/WAPI-1350 - -const V2_ANALYTICS_ENDPOINT = - 'https://mm-sdk-analytics.api.cx.metamask.io/v2/events'; - -/** - * Event names defined in the `mobile/sdk-connect-v2` namespace of the - * analytics OpenAPI schema. Only the connection-lifecycle subset is - * listed here; action-level events can be added later. - */ -export type WalletConnectionEventName = - | 'Remote Connection Request Received' - | 'Remote Connection Request Failed'; - -export interface WalletEventProperties { - remote_session_id: string; - platform: 'mobile'; - sdk_version?: string; - sdk_platform?: string; - /** Only set on reconnect (handleSimpleDeeplink) flows. */ - found_in_store?: boolean; - failure_reason?: string; -} - -interface MobileSDKConnectV2Payload { - namespace: 'mobile/sdk-connect-v2'; - event_name: WalletConnectionEventName; - properties: WalletEventProperties; -} - -/** - * Fire-and-forget POST to the V2 analytics relay. - * Mirrors the dapp-side `@metamask/analytics` package format so both - * sides land in the same Segment dataset and can be joined on `remote_session_id`. - */ -export function trackWalletEvent( - eventName: WalletConnectionEventName, - properties: WalletEventProperties, -): void { - if (!analytics.isEnabled()) return; - - const payload: MobileSDKConnectV2Payload[] = [ - { - namespace: 'mobile/sdk-connect-v2', - event_name: eventName, - properties, - }, - ]; - - fetch(V2_ANALYTICS_ENDPOINT, { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify(payload), - }).catch((err) => { - logger.error('v2-analytics: failed to send event', eventName, err); - }); -} From 11068f994d8b2ddd301c20680b725935a41510bc Mon Sep 17 00:00:00 2001 From: ffmcgee Date: Tue, 14 Apr 2026 13:10:03 +0200 Subject: [PATCH 13/21] fix: make sure analytics calls are not blocking mwp flows --- .../services/connection-registry.ts | 95 +++++++++---------- 1 file changed, 45 insertions(+), 50 deletions(-) diff --git a/app/core/SDKConnectV2/services/connection-registry.ts b/app/core/SDKConnectV2/services/connection-registry.ts index 098f1123bf5..6d84d9c105b 100644 --- a/app/core/SDKConnectV2/services/connection-registry.ts +++ b/app/core/SDKConnectV2/services/connection-registry.ts @@ -20,8 +20,28 @@ import { rpcErrors } from '@metamask/rpc-errors'; import { INTERNAL_ORIGINS } from '../../../constants/transaction'; import { analytics } from '../../../util/analytics/analytics'; import { AnalyticsEventBuilder } from '../../../util/analytics/AnalyticsEventBuilder'; +import type { IMetaMetricsEvent } from '../../Analytics/MetaMetrics.types'; import { MetaMetricsEvents } from '../../Analytics/MetaMetrics.events'; +/** + * Fire-and-forget analytics helper. Never throws — a broken analytics + * call must never abort connection establishment or error handling. + */ +function trackMwpEvent( + event: IMetaMetricsEvent, + properties: Record, +): void { + try { + analytics.trackEvent( + AnalyticsEventBuilder.createEventBuilder(event) + .addProperties(properties) + .build(), + ); + } catch { + // Intentionally swallowed: analytics must not block MWP flows. + } +} + /** * Hard cap on the number of simultaneous active connections. * @@ -154,33 +174,21 @@ export class ConnectionRegistry { const conn = await this.store.get(id); if (conn) { - analytics.trackEvent( - AnalyticsEventBuilder.createEventBuilder( - MetaMetricsEvents.REMOTE_CONNECTION_REQUEST_RECEIVED, - ) - .addProperties({ - remote_session_id: id, - platform: 'mobile', - sdk_version: conn.metadata?.sdk?.version, - sdk_platform: conn.metadata?.sdk?.platform, - found_in_store: true, - }) - .build(), - ); + trackMwpEvent(MetaMetricsEvents.REMOTE_CONNECTION_REQUEST_RECEIVED, { + remote_session_id: id, + platform: 'mobile', + sdk_version: conn.metadata?.sdk?.version, + sdk_platform: conn.metadata?.sdk?.platform, + found_in_store: true, + }); return; } - analytics.trackEvent( - AnalyticsEventBuilder.createEventBuilder( - MetaMetricsEvents.REMOTE_CONNECTION_REQUEST_RECEIVED, - ) - .addProperties({ - remote_session_id: id, - platform: 'mobile', - found_in_store: false, - }) - .build(), - ); + trackMwpEvent(MetaMetricsEvents.REMOTE_CONNECTION_REQUEST_RECEIVED, { + remote_session_id: id, + platform: 'mobile', + found_in_store: false, + }); logger.error( 'Failed to find connection in store for simple deeplink with id:', @@ -239,18 +247,12 @@ export class ConnectionRegistry { try { connReq = this.parseConnectionRequest(url); - analytics.trackEvent( - AnalyticsEventBuilder.createEventBuilder( - MetaMetricsEvents.REMOTE_CONNECTION_REQUEST_RECEIVED, - ) - .addProperties({ - remote_session_id: connReq.sessionRequest.id, - platform: 'mobile', - sdk_version: connReq.metadata.sdk.version, - sdk_platform: connReq.metadata.sdk.platform, - }) - .build(), - ); + trackMwpEvent(MetaMetricsEvents.REMOTE_CONNECTION_REQUEST_RECEIVED, { + remote_session_id: connReq.sessionRequest.id, + platform: 'mobile', + sdk_version: connReq.metadata.sdk.version, + sdk_platform: connReq.metadata.sdk.platform, + }); // Defense-in-depth: block connections whose self-reported dapp metadata // matches a known internal origin. This check is currently redundant @@ -292,20 +294,13 @@ export class ConnectionRegistry { // protocol). User rejections of wallet_createSession are tracked by // the existing CONNECT_REQUEST_CANCELLED MetaMetrics event (with // source: 'sdk_connect_v2') — no double-fire occurs. - analytics.trackEvent( - AnalyticsEventBuilder.createEventBuilder( - MetaMetricsEvents.REMOTE_CONNECTION_REQUEST_FAILED, - ) - .addProperties({ - remote_session_id: connReq?.sessionRequest?.id ?? 'unknown', - platform: 'mobile', - sdk_version: connReq?.metadata?.sdk?.version, - sdk_platform: connReq?.metadata?.sdk?.platform, - failure_reason: - error instanceof Error ? error.message : String(error), - }) - .build(), - ); + trackMwpEvent(MetaMetricsEvents.REMOTE_CONNECTION_REQUEST_FAILED, { + remote_session_id: connReq?.sessionRequest?.id ?? 'unknown', + platform: 'mobile', + sdk_version: connReq?.metadata?.sdk?.version, + sdk_platform: connReq?.metadata?.sdk?.platform, + failure_reason: error instanceof Error ? error.message : String(error), + }); } finally { if (connInfo) this.hostapp.hideConnectionLoading(connInfo); } From da0cf7fa774bbfd8d47f2e6e64fa5be7fcc4a3ad Mon Sep 17 00:00:00 2001 From: ffmcgee Date: Tue, 14 Apr 2026 13:35:36 +0200 Subject: [PATCH 14/21] fix: make sure failure event tracking is not lost --- app/core/SDKConnectV2/services/connection-registry.ts | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/app/core/SDKConnectV2/services/connection-registry.ts b/app/core/SDKConnectV2/services/connection-registry.ts index 6d84d9c105b..c2d245c6c9c 100644 --- a/app/core/SDKConnectV2/services/connection-registry.ts +++ b/app/core/SDKConnectV2/services/connection-registry.ts @@ -288,12 +288,9 @@ export class ConnectionRegistry { } catch (error) { logger.error('Failed to handle connect deeplink:', error, redactUrl(url)); this.hostapp.showConnectionError(); - if (conn) await this.disconnect(conn.id); - // This catch only handles connection-setup failures (parse, network, - // protocol). User rejections of wallet_createSession are tracked by - // the existing CONNECT_REQUEST_CANCELLED MetaMetrics event (with - // source: 'sdk_connect_v2') — no double-fire occurs. + // Track the failure before cleanup so the event fires even if + // disconnect() throws. trackMwpEvent(MetaMetricsEvents.REMOTE_CONNECTION_REQUEST_FAILED, { remote_session_id: connReq?.sessionRequest?.id ?? 'unknown', platform: 'mobile', @@ -301,6 +298,8 @@ export class ConnectionRegistry { sdk_platform: connReq?.metadata?.sdk?.platform, failure_reason: error instanceof Error ? error.message : String(error), }); + + if (conn) await this.disconnect(conn.id); } finally { if (connInfo) this.hostapp.hideConnectionLoading(connInfo); } From 4ac6ac5411d801e6fd1b184f7da5f11ddcb05e58 Mon Sep 17 00:00:00 2001 From: ffmcgee Date: Wed, 15 Apr 2026 10:11:49 +0200 Subject: [PATCH 15/21] add transport type to mwp calls and fix remote_session_id --- .../services/connection-registry.test.ts | 4 ++++ .../SDKConnectV2/services/connection-registry.ts | 15 ++++++++++++--- 2 files changed, 16 insertions(+), 3 deletions(-) diff --git a/app/core/SDKConnectV2/services/connection-registry.test.ts b/app/core/SDKConnectV2/services/connection-registry.test.ts index b97ae483c85..a9fd8776e77 100644 --- a/app/core/SDKConnectV2/services/connection-registry.test.ts +++ b/app/core/SDKConnectV2/services/connection-registry.test.ts @@ -302,6 +302,7 @@ describe('ConnectionRegistry', () => { expect.objectContaining({ remote_session_id: 'mock-conn-id', platform: 'mobile', + transport_type: 'mwp', found_in_store: true, }), ); @@ -331,6 +332,7 @@ describe('ConnectionRegistry', () => { expect.objectContaining({ remote_session_id: 'mock-conn-id', platform: 'mobile', + transport_type: 'mwp', found_in_store: false, }), ); @@ -439,6 +441,7 @@ describe('ConnectionRegistry', () => { expect.objectContaining({ remote_session_id: mockConnectionRequest.sessionRequest.id, platform: 'mobile', + transport_type: 'mwp', sdk_version: '2.0.0', sdk_platform: 'JavaScript', }), @@ -582,6 +585,7 @@ describe('ConnectionRegistry', () => { expect.objectContaining({ remote_session_id: mockConnectionRequest.sessionRequest.id, platform: 'mobile', + transport_type: 'mwp', failure_reason: 'Connection failed', }), ); diff --git a/app/core/SDKConnectV2/services/connection-registry.ts b/app/core/SDKConnectV2/services/connection-registry.ts index c2d245c6c9c..aa03ea00d0b 100644 --- a/app/core/SDKConnectV2/services/connection-registry.ts +++ b/app/core/SDKConnectV2/services/connection-registry.ts @@ -175,8 +175,9 @@ export class ConnectionRegistry { if (conn) { trackMwpEvent(MetaMetricsEvents.REMOTE_CONNECTION_REQUEST_RECEIVED, { - remote_session_id: id, + remote_session_id: conn.metadata?.analytics?.remote_session_id ?? id, platform: 'mobile', + transport_type: 'mwp', sdk_version: conn.metadata?.sdk?.version, sdk_platform: conn.metadata?.sdk?.platform, found_in_store: true, @@ -187,6 +188,7 @@ export class ConnectionRegistry { trackMwpEvent(MetaMetricsEvents.REMOTE_CONNECTION_REQUEST_RECEIVED, { remote_session_id: id, platform: 'mobile', + transport_type: 'mwp', found_in_store: false, }); @@ -248,8 +250,11 @@ export class ConnectionRegistry { connReq = this.parseConnectionRequest(url); trackMwpEvent(MetaMetricsEvents.REMOTE_CONNECTION_REQUEST_RECEIVED, { - remote_session_id: connReq.sessionRequest.id, + remote_session_id: + connReq.metadata.analytics?.remote_session_id ?? + connReq.sessionRequest.id, platform: 'mobile', + transport_type: 'mwp', sdk_version: connReq.metadata.sdk.version, sdk_platform: connReq.metadata.sdk.platform, }); @@ -292,8 +297,12 @@ export class ConnectionRegistry { // Track the failure before cleanup so the event fires even if // disconnect() throws. trackMwpEvent(MetaMetricsEvents.REMOTE_CONNECTION_REQUEST_FAILED, { - remote_session_id: connReq?.sessionRequest?.id ?? 'unknown', + remote_session_id: + connReq?.metadata?.analytics?.remote_session_id ?? + connReq?.sessionRequest?.id ?? + 'unknown', platform: 'mobile', + transport_type: 'mwp', sdk_version: connReq?.metadata?.sdk?.version, sdk_platform: connReq?.metadata?.sdk?.platform, failure_reason: error instanceof Error ? error.message : String(error), From 891879c765768e3f5dc77c4fe049811555c9f59e Mon Sep 17 00:00:00 2001 From: ffmcgee Date: Wed, 15 Apr 2026 16:48:27 +0200 Subject: [PATCH 16/21] refactor: use constant for mwp ref --- .../SDKConnectV2/services/connection-registry.test.ts | 9 +++++---- app/core/SDKConnectV2/services/connection-registry.ts | 9 +++++---- 2 files changed, 10 insertions(+), 8 deletions(-) diff --git a/app/core/SDKConnectV2/services/connection-registry.test.ts b/app/core/SDKConnectV2/services/connection-registry.test.ts index a9fd8776e77..3d74e0dc139 100644 --- a/app/core/SDKConnectV2/services/connection-registry.test.ts +++ b/app/core/SDKConnectV2/services/connection-registry.test.ts @@ -9,6 +9,7 @@ import { ConnectionInfo } from '../types/connection-info'; import Engine from '../../Engine'; import { analytics } from '../../../util/analytics/analytics'; import { MetaMetricsEvents } from '../../Analytics'; +import { TransportType } from '../../../components/hooks/useAnalytics/useAnalytics.types'; jest.mock('../adapters/host-application-adapter'); jest.mock('../store/connection-store'); @@ -302,7 +303,7 @@ describe('ConnectionRegistry', () => { expect.objectContaining({ remote_session_id: 'mock-conn-id', platform: 'mobile', - transport_type: 'mwp', + transport_type: TransportType.MWP, found_in_store: true, }), ); @@ -332,7 +333,7 @@ describe('ConnectionRegistry', () => { expect.objectContaining({ remote_session_id: 'mock-conn-id', platform: 'mobile', - transport_type: 'mwp', + transport_type: TransportType.MWP, found_in_store: false, }), ); @@ -441,7 +442,7 @@ describe('ConnectionRegistry', () => { expect.objectContaining({ remote_session_id: mockConnectionRequest.sessionRequest.id, platform: 'mobile', - transport_type: 'mwp', + transport_type: TransportType.MWP, sdk_version: '2.0.0', sdk_platform: 'JavaScript', }), @@ -585,7 +586,7 @@ describe('ConnectionRegistry', () => { expect.objectContaining({ remote_session_id: mockConnectionRequest.sessionRequest.id, platform: 'mobile', - transport_type: 'mwp', + transport_type: TransportType.MWP, failure_reason: 'Connection failed', }), ); diff --git a/app/core/SDKConnectV2/services/connection-registry.ts b/app/core/SDKConnectV2/services/connection-registry.ts index aa03ea00d0b..47160363a80 100644 --- a/app/core/SDKConnectV2/services/connection-registry.ts +++ b/app/core/SDKConnectV2/services/connection-registry.ts @@ -22,6 +22,7 @@ import { analytics } from '../../../util/analytics/analytics'; import { AnalyticsEventBuilder } from '../../../util/analytics/AnalyticsEventBuilder'; import type { IMetaMetricsEvent } from '../../Analytics/MetaMetrics.types'; import { MetaMetricsEvents } from '../../Analytics/MetaMetrics.events'; +import { TransportType } from '../../../components/hooks/useAnalytics/useAnalytics.types'; /** * Fire-and-forget analytics helper. Never throws — a broken analytics @@ -177,7 +178,7 @@ export class ConnectionRegistry { trackMwpEvent(MetaMetricsEvents.REMOTE_CONNECTION_REQUEST_RECEIVED, { remote_session_id: conn.metadata?.analytics?.remote_session_id ?? id, platform: 'mobile', - transport_type: 'mwp', + transport_type: TransportType.MWP, sdk_version: conn.metadata?.sdk?.version, sdk_platform: conn.metadata?.sdk?.platform, found_in_store: true, @@ -188,7 +189,7 @@ export class ConnectionRegistry { trackMwpEvent(MetaMetricsEvents.REMOTE_CONNECTION_REQUEST_RECEIVED, { remote_session_id: id, platform: 'mobile', - transport_type: 'mwp', + transport_type: TransportType.MWP, found_in_store: false, }); @@ -254,7 +255,7 @@ export class ConnectionRegistry { connReq.metadata.analytics?.remote_session_id ?? connReq.sessionRequest.id, platform: 'mobile', - transport_type: 'mwp', + transport_type: TransportType.MWP, sdk_version: connReq.metadata.sdk.version, sdk_platform: connReq.metadata.sdk.platform, }); @@ -302,7 +303,7 @@ export class ConnectionRegistry { connReq?.sessionRequest?.id ?? 'unknown', platform: 'mobile', - transport_type: 'mwp', + transport_type: TransportType.MWP, sdk_version: connReq?.metadata?.sdk?.version, sdk_platform: connReq?.metadata?.sdk?.platform, failure_reason: error instanceof Error ? error.message : String(error), From fecd54ce01677dbb186abef890d0d18cf7901ea4 Mon Sep 17 00:00:00 2001 From: Alex Donesky Date: Wed, 15 Apr 2026 10:02:58 -0500 Subject: [PATCH 17/21] fix: add missing signature property to MWP Deep Link Used event The Deep Link Used schema requires `signature` (required: true), but trackMwpDeepLinkUsed was not sending it. MWP deeplinks use a custom scheme (metamask://mwp/...) and bypass the universal link signature verification pipeline entirely, so `signature: 'missing'` is the correct value. Without this, every MWP deeplink fire generates a Segment Protocols violation for the missing required property. --- .../handlers/legacy/__tests__/handleDeeplink.test.ts | 10 +++++++++- .../DeeplinkManager/handlers/legacy/handleDeeplink.ts | 6 +++++- 2 files changed, 14 insertions(+), 2 deletions(-) diff --git a/app/core/DeeplinkManager/handlers/legacy/__tests__/handleDeeplink.test.ts b/app/core/DeeplinkManager/handlers/legacy/__tests__/handleDeeplink.test.ts index 8ab6fea2671..5d3fc36ca9d 100644 --- a/app/core/DeeplinkManager/handlers/legacy/__tests__/handleDeeplink.test.ts +++ b/app/core/DeeplinkManager/handlers/legacy/__tests__/handleDeeplink.test.ts @@ -7,7 +7,10 @@ import SDKConnectV2 from '../../../../SDKConnectV2'; import { analytics } from '../../../../../util/analytics/analytics'; import { AnalyticsEventBuilder } from '../../../../../util/analytics/AnalyticsEventBuilder'; import { MetaMetricsEvents } from '../../../../Analytics/MetaMetrics.events'; -import { DeepLinkRoute } from '../../../types/deepLinkAnalytics.types'; +import { + DeepLinkRoute, + SignatureStatus, +} from '../../../types/deepLinkAnalytics.types'; import { detectAppInstallation } from '../../../util/deeplinks/deepLinkAnalytics'; jest.mock('../../../../../actions/user', () => ({ @@ -68,6 +71,9 @@ jest.mock('../../../types/deepLinkAnalytics.types', () => ({ DeepLinkRoute: { MMC_MWP: 'mmc-mwp', }, + SignatureStatus: { + MISSING: 'missing', + }, })); jest.mock('../../../util/deeplinks/deepLinkAnalytics', () => ({ @@ -222,6 +228,7 @@ describe('handleDeeplink', () => { ); expect(mockAddProperties).toHaveBeenCalledWith({ route: DeepLinkRoute.MMC_MWP, + signature: SignatureStatus.MISSING, was_app_installed: true, }); expect(mockBuild).toHaveBeenCalled(); @@ -237,6 +244,7 @@ describe('handleDeeplink', () => { expect(mockAddProperties).toHaveBeenCalledWith({ route: DeepLinkRoute.MMC_MWP, + signature: SignatureStatus.MISSING, was_app_installed: false, }); expect(mockTrackEvent).toHaveBeenCalledWith({ event: 'mocked' }); diff --git a/app/core/DeeplinkManager/handlers/legacy/handleDeeplink.ts b/app/core/DeeplinkManager/handlers/legacy/handleDeeplink.ts index f35ffeb15b8..8efd566d0e2 100644 --- a/app/core/DeeplinkManager/handlers/legacy/handleDeeplink.ts +++ b/app/core/DeeplinkManager/handlers/legacy/handleDeeplink.ts @@ -6,7 +6,10 @@ import SDKConnectV2 from '../../../SDKConnectV2'; import { analytics } from '../../../../util/analytics/analytics'; import { AnalyticsEventBuilder } from '../../../../util/analytics/AnalyticsEventBuilder'; import { MetaMetricsEvents } from '../../../Analytics/MetaMetrics.events'; -import { DeepLinkRoute } from '../../types/deepLinkAnalytics.types'; +import { + DeepLinkRoute, + SignatureStatus, +} from '../../types/deepLinkAnalytics.types'; import { detectAppInstallation } from '../../util/deeplinks/deepLinkAnalytics'; export function handleDeeplink(opts: { uri?: string; source?: string }) { @@ -46,6 +49,7 @@ function trackMwpDeepLinkUsed(url: string): void { ) .addProperties({ route: DeepLinkRoute.MMC_MWP, + signature: SignatureStatus.MISSING, was_app_installed: wasAppInstalled, }) .build(); From 373f7d4576f6ddd4ca647f45933618d2da8ba349 Mon Sep 17 00:00:00 2001 From: Alex Donesky Date: Wed, 15 Apr 2026 11:26:18 -0500 Subject: [PATCH 18/21] fix: remove unplanned platform property from MWP analytics events MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit platform: 'mobile' is not in the Remote Connection Request Received/Failed schemas or metamask-mobile-globals, causing Segment Protocols violations. The property is redundant — these events only fire from mobile. --- app/core/SDKConnectV2/services/connection-registry.test.ts | 4 ---- app/core/SDKConnectV2/services/connection-registry.ts | 4 ---- 2 files changed, 8 deletions(-) diff --git a/app/core/SDKConnectV2/services/connection-registry.test.ts b/app/core/SDKConnectV2/services/connection-registry.test.ts index 3d74e0dc139..7e8bacf8251 100644 --- a/app/core/SDKConnectV2/services/connection-registry.test.ts +++ b/app/core/SDKConnectV2/services/connection-registry.test.ts @@ -302,7 +302,6 @@ describe('ConnectionRegistry', () => { expect(trackedEvent.properties).toEqual( expect.objectContaining({ remote_session_id: 'mock-conn-id', - platform: 'mobile', transport_type: TransportType.MWP, found_in_store: true, }), @@ -332,7 +331,6 @@ describe('ConnectionRegistry', () => { expect(trackedEvent.properties).toEqual( expect.objectContaining({ remote_session_id: 'mock-conn-id', - platform: 'mobile', transport_type: TransportType.MWP, found_in_store: false, }), @@ -441,7 +439,6 @@ describe('ConnectionRegistry', () => { expect(trackedEvent.properties).toEqual( expect.objectContaining({ remote_session_id: mockConnectionRequest.sessionRequest.id, - platform: 'mobile', transport_type: TransportType.MWP, sdk_version: '2.0.0', sdk_platform: 'JavaScript', @@ -585,7 +582,6 @@ describe('ConnectionRegistry', () => { expect(failedEvent.properties).toEqual( expect.objectContaining({ remote_session_id: mockConnectionRequest.sessionRequest.id, - platform: 'mobile', transport_type: TransportType.MWP, failure_reason: 'Connection failed', }), diff --git a/app/core/SDKConnectV2/services/connection-registry.ts b/app/core/SDKConnectV2/services/connection-registry.ts index 47160363a80..8e8770d2c96 100644 --- a/app/core/SDKConnectV2/services/connection-registry.ts +++ b/app/core/SDKConnectV2/services/connection-registry.ts @@ -177,7 +177,6 @@ export class ConnectionRegistry { if (conn) { trackMwpEvent(MetaMetricsEvents.REMOTE_CONNECTION_REQUEST_RECEIVED, { remote_session_id: conn.metadata?.analytics?.remote_session_id ?? id, - platform: 'mobile', transport_type: TransportType.MWP, sdk_version: conn.metadata?.sdk?.version, sdk_platform: conn.metadata?.sdk?.platform, @@ -188,7 +187,6 @@ export class ConnectionRegistry { trackMwpEvent(MetaMetricsEvents.REMOTE_CONNECTION_REQUEST_RECEIVED, { remote_session_id: id, - platform: 'mobile', transport_type: TransportType.MWP, found_in_store: false, }); @@ -254,7 +252,6 @@ export class ConnectionRegistry { remote_session_id: connReq.metadata.analytics?.remote_session_id ?? connReq.sessionRequest.id, - platform: 'mobile', transport_type: TransportType.MWP, sdk_version: connReq.metadata.sdk.version, sdk_platform: connReq.metadata.sdk.platform, @@ -302,7 +299,6 @@ export class ConnectionRegistry { connReq?.metadata?.analytics?.remote_session_id ?? connReq?.sessionRequest?.id ?? 'unknown', - platform: 'mobile', transport_type: TransportType.MWP, sdk_version: connReq?.metadata?.sdk?.version, sdk_platform: connReq?.metadata?.sdk?.platform, From 30c487527aa7e3c913b3130ee0da3dd86fe741cc Mon Sep 17 00:00:00 2001 From: ffmcgee Date: Thu, 16 Apr 2026 11:36:59 +0200 Subject: [PATCH 19/21] remove unecessary mocks --- .../legacy/__tests__/handleDeeplink.test.ts | 15 --------------- 1 file changed, 15 deletions(-) diff --git a/app/core/DeeplinkManager/handlers/legacy/__tests__/handleDeeplink.test.ts b/app/core/DeeplinkManager/handlers/legacy/__tests__/handleDeeplink.test.ts index 5d3fc36ca9d..ddf22bddd1c 100644 --- a/app/core/DeeplinkManager/handlers/legacy/__tests__/handleDeeplink.test.ts +++ b/app/core/DeeplinkManager/handlers/legacy/__tests__/handleDeeplink.test.ts @@ -61,21 +61,6 @@ jest.mock('../../../../../util/analytics/AnalyticsEventBuilder', () => ({ }, })); -jest.mock('../../../../Analytics/MetaMetrics.events', () => ({ - MetaMetricsEvents: { - DEEP_LINK_USED: 'Deep Link Used', - }, -})); - -jest.mock('../../../types/deepLinkAnalytics.types', () => ({ - DeepLinkRoute: { - MMC_MWP: 'mmc-mwp', - }, - SignatureStatus: { - MISSING: 'missing', - }, -})); - jest.mock('../../../util/deeplinks/deepLinkAnalytics', () => ({ detectAppInstallation: jest.fn(), })); From 0e612b56e4b2ecb39bc5983f5017621c102d15eb Mon Sep 17 00:00:00 2001 From: Alex Donesky Date: Thu, 16 Apr 2026 13:59:49 -0500 Subject: [PATCH 20/21] fix: resolve duplicate REMOTE_CONNECTION_REQUEST_RECEIVED after main merge The main merge brought in PR #28322's existing REMOTE_CONNECTION_REQUEST_RECEIVED entries, which collided with the ones this branch had added. Consolidate by keeping main's originals in their groups and moving REMOTE_CONNECTION_REQUEST_FAILED alongside them, fixing TS2300/TS1117 errors in lint:tsc. --- app/core/Analytics/MetaMetrics.events.ts | 16 ++++------------ 1 file changed, 4 insertions(+), 12 deletions(-) diff --git a/app/core/Analytics/MetaMetrics.events.ts b/app/core/Analytics/MetaMetrics.events.ts index 30f84c21c89..50ff38f248f 100644 --- a/app/core/Analytics/MetaMetrics.events.ts +++ b/app/core/Analytics/MetaMetrics.events.ts @@ -67,6 +67,7 @@ enum EVENT_NAME { // Remote connection events (SDK v1 socket relay, MWP, and WalletConnect) REMOTE_CONNECTION_REQUEST_RECEIVED = 'Remote Connection Request Received', + REMOTE_CONNECTION_REQUEST_FAILED = 'Remote Connection Request Failed', // SDK v1 legacy RPC events (socket relay + deeplink protocol only) SDK_LEGACY_RPC_REQUEST_RECEIVED = 'SDK Legacy RPC Request Received', @@ -684,10 +685,6 @@ enum EVENT_NAME { // Assets ASSETS_FIRST_INIT_FETCH_COMPLETED = 'Assets First Init Fetch Completed', - - // Remote connection events (SDK v1 socket relay, MWP, and WalletConnect) - REMOTE_CONNECTION_REQUEST_RECEIVED = 'Remote Connection Request Received', - REMOTE_CONNECTION_REQUEST_FAILED = 'Remote Connection Request Failed', } export enum HARDWARE_WALLET_BUTTON_TYPE { @@ -802,6 +799,9 @@ const events = { REMOTE_CONNECTION_REQUEST_RECEIVED: generateOpt( EVENT_NAME.REMOTE_CONNECTION_REQUEST_RECEIVED, ), + REMOTE_CONNECTION_REQUEST_FAILED: generateOpt( + EVENT_NAME.REMOTE_CONNECTION_REQUEST_FAILED, + ), // SDK v1 legacy RPC events (socket relay + deeplink protocol only) SDK_LEGACY_RPC_REQUEST_RECEIVED: generateOpt( @@ -1779,14 +1779,6 @@ const events = { MUSD_QUICK_CONVERT_TOKEN_ROW_BUTTON_CLICKED: generateOpt( EVENT_NAME.MUSD_QUICK_CONVERT_TOKEN_ROW_BUTTON_CLICKED, ), - - // Remote connection events (SDK v1 socket relay, MWP, and WalletConnect) - REMOTE_CONNECTION_REQUEST_RECEIVED: generateOpt( - EVENT_NAME.REMOTE_CONNECTION_REQUEST_RECEIVED, - ), - REMOTE_CONNECTION_REQUEST_FAILED: generateOpt( - EVENT_NAME.REMOTE_CONNECTION_REQUEST_FAILED, - ), }; /** From efb7b89f989adfc07262c031f26a13955626acd0 Mon Sep 17 00:00:00 2001 From: ffmcgee Date: Fri, 17 Apr 2026 13:02:59 +0200 Subject: [PATCH 21/21] refactor: remove dead parameter from function trackMwpDeepLinkUsed --- app/core/DeeplinkManager/handlers/legacy/handleDeeplink.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/core/DeeplinkManager/handlers/legacy/handleDeeplink.ts b/app/core/DeeplinkManager/handlers/legacy/handleDeeplink.ts index 8efd566d0e2..a3c054adfc1 100644 --- a/app/core/DeeplinkManager/handlers/legacy/handleDeeplink.ts +++ b/app/core/DeeplinkManager/handlers/legacy/handleDeeplink.ts @@ -17,7 +17,7 @@ export function handleDeeplink(opts: { uri?: string; source?: string }) { // links here immediately to establish the WebSocket connection as fast as possible, // without waiting for the app to be unlocked or fully onboarded. if (SDKConnectV2.isMwpDeeplink(opts.uri)) { - trackMwpDeepLinkUsed(opts.uri); + trackMwpDeepLinkUsed(); SDKConnectV2.handleMwpDeeplink(opts.uri); // By returning here, we bypass the standard saga-based deeplink flow below, // which would otherwise wait for a LOGIN or ONBOARDING_COMPLETED action. @@ -41,7 +41,7 @@ export function handleDeeplink(opts: { uri?: string; source?: string }) { * Fire DEEP_LINK_USED for MWP deeplinks asynchronously so the WebSocket * handshake is never blocked by analytics work. */ -function trackMwpDeepLinkUsed(url: string): void { +function trackMwpDeepLinkUsed(): void { detectAppInstallation() .then((wasAppInstalled) => { const event = AnalyticsEventBuilder.createEventBuilder(