diff --git a/packages/devnext/src/pages/_app.tsx b/packages/devnext/src/pages/_app.tsx index cea0c3172..cae846bbf 100644 --- a/packages/devnext/src/pages/_app.tsx +++ b/packages/devnext/src/pages/_app.tsx @@ -29,7 +29,7 @@ const WithSDKConfig = ({ children }: { children: React.ReactNode }) => { debug={true} sdkOptions={{ communicationServerUrl: socketServer, - enableAnalytics: false, + enableAnalytics: true, infuraAPIKey, readonlyRPCMap: { '0x539': process.env.NEXT_PUBLIC_PROVIDER_RPCURL ?? '', diff --git a/packages/sdk-communication-layer/src/Analytics.ts b/packages/sdk-communication-layer/src/Analytics.ts index 8c0df2bf4..b9ccb78b9 100644 --- a/packages/sdk-communication-layer/src/Analytics.ts +++ b/packages/sdk-communication-layer/src/Analytics.ts @@ -2,38 +2,110 @@ import crossFetch from 'cross-fetch'; import { CommunicationLayerPreference } from './types/CommunicationLayerPreference'; import { OriginatorInfo } from './types/OriginatorInfo'; import { TrackingEvents } from './types/TrackingEvent'; +import { logger } from './utils/logger'; -export interface AnaliticsProps { +export interface AnalyticsProps { id: string; event: TrackingEvents; originationInfo?: OriginatorInfo; commLayer?: CommunicationLayerPreference; sdkVersion?: string; - commLayerVersion: string; + commLayerVersion?: string; walletVersion?: string; + params?: Record; } +// Buffer for storing events +let analyticsBuffer: AnalyticsProps[] = []; +let tempBuffer: AnalyticsProps[] = []; // Temporary buffer to hold new events during send operation +let targetUrl: string | undefined; + +// Function to safely add events to the buffer +function addToBuffer(event: AnalyticsProps) { + tempBuffer.push(event); +} + +// Function to swap buffers atomically +function swapBuffers() { + const swap = tempBuffer; + tempBuffer = analyticsBuffer; + analyticsBuffer = swap; +} + +// Function to send buffered events +async function sendBufferedEvents(parameters: AnalyticsProps) { + // TODO: re-enabled once buffered events activated + // if (!targetUrl || (analyticsBuffer.length === 0 && tempBuffer.length === 0)) { + // return; + // } + if (!targetUrl || !parameters) { + return; + } + + // Atomically swap the buffers + swapBuffers(); + + const serverUrl = targetUrl.endsWith('/') + ? `${targetUrl}debug` + : `${targetUrl}/debug`; + + const flatParams: { + [key: string]: unknown; + } = { ...parameters }; + delete flatParams.params; + + // remove params from the event and append each property to the object instead + if (parameters.params) { + for (const [key, value] of Object.entries(parameters.params)) { + flatParams[key] = value; + } + } + + const body = JSON.stringify(flatParams); + + logger.RemoteCommunication( + `[sendBufferedEvents] Sending ${analyticsBuffer.length} analytics events to ${serverUrl}`, + ); + + try { + const response = await crossFetch(serverUrl, { + method: 'POST', + headers: { + Accept: 'application/json', + 'Content-Type': 'application/json', + }, + body, + }); + + const text = await response.text(); + logger.RemoteCommunication(`[sendBufferedEvents] Response: ${text}`); + + // Clear the processed buffer --- operation is atomic and no race condition can happen since we use a separate buffer + // eslint-disable-next-line require-atomic-updates + analyticsBuffer.length = 0; + } catch (error) { + console.warn(`Error sending analytics`, error); + } +} + +// TODO re-enable whenever we want to activate buffered events and socket code has been updated. +// // Initialize timer to send analytics in batch every 15 seconds +// setInterval(() => { +// sendBufferedEvents().catch(() => { +// // ignore errors +// }); +// }, 15000); + +// Modified SendAnalytics to add events to buffer instead of sending directly export const SendAnalytics = async ( - parameters: AnaliticsProps, + parameters: AnalyticsProps, socketServerUrl: string, ) => { - const serverUrl = socketServerUrl.endsWith('/') - ? `${socketServerUrl}debug` - : `${socketServerUrl}/debug`; - - const body = JSON.stringify(parameters); - - const response = await crossFetch(serverUrl, { - method: 'POST', - headers: { - // eslint-disable-next-line prettier/prettier - Accept: 'application/json', - 'Content-Type': 'application/json', - }, - body, - }); + targetUrl = socketServerUrl; - // TODO error management when request fails - const text = await response.text(); - return text; + // Safely add the analytics event to the buffer + addToBuffer(parameters); + sendBufferedEvents(parameters).catch(() => { + // ignore + }); }; diff --git a/packages/sdk-communication-layer/src/SocketService.ts b/packages/sdk-communication-layer/src/SocketService.ts index 245f3f897..c7b26d04f 100644 --- a/packages/sdk-communication-layer/src/SocketService.ts +++ b/packages/sdk-communication-layer/src/SocketService.ts @@ -23,6 +23,7 @@ import { DisconnectOptions } from './types/DisconnectOptions'; import { KeyInfo } from './types/KeyInfo'; import { CommunicationLayerLoggingOptions } from './types/LoggingOptions'; import { logger } from './utils/logger'; +import { RemoteCommunication } from './RemoteCommunication'; export interface SocketServiceProps { communicationLayerPreference: CommunicationLayerPreference; @@ -32,6 +33,7 @@ export interface SocketServiceProps { communicationServerUrl: string; context: string; ecies?: ECIESProps; + remote: RemoteCommunication; logging?: CommunicationLayerLoggingOptions; } @@ -52,6 +54,7 @@ export interface SocketServiceState { hasPlaintext: boolean; socket?: Socket; setupChannelListeners?: boolean; + analytics?: boolean; keyExchange?: KeyExchange; } @@ -86,6 +89,8 @@ export class SocketService extends EventEmitter2 implements CommunicationLayer { communicationServerUrl: '', }; + remote: RemoteCommunication; + constructor({ otherPublicKey, reconnect, @@ -94,6 +99,7 @@ export class SocketService extends EventEmitter2 implements CommunicationLayer { communicationServerUrl, context, ecies, + remote, logging, }: SocketServiceProps) { super(); @@ -102,6 +108,7 @@ export class SocketService extends EventEmitter2 implements CommunicationLayer { this.state.context = context; this.state.communicationLayerPreference = communicationLayerPreference; this.state.debug = logging?.serviceLayer === true; + this.remote = remote; if (logging?.serviceLayer === true) { debug.enable('SocketService:Layer'); diff --git a/packages/sdk-communication-layer/src/index.ts b/packages/sdk-communication-layer/src/index.ts index f4fd9c442..a94a38534 100644 --- a/packages/sdk-communication-layer/src/index.ts +++ b/packages/sdk-communication-layer/src/index.ts @@ -1,4 +1,4 @@ -import { SendAnalytics } from './Analytics'; +import { SendAnalytics, AnalyticsProps } from './Analytics'; import { ECIES, ECIESProps } from './ECIES'; import { RemoteCommunication, @@ -48,6 +48,7 @@ export type { StorageManager, StorageManagerProps, WalletInfo, + AnalyticsProps, }; export { diff --git a/packages/sdk-communication-layer/src/services/RemoteCommunication/ConnectionManager/initCommunicationLayer.ts b/packages/sdk-communication-layer/src/services/RemoteCommunication/ConnectionManager/initCommunicationLayer.ts index 3aeec7abc..55aa76721 100644 --- a/packages/sdk-communication-layer/src/services/RemoteCommunication/ConnectionManager/initCommunicationLayer.ts +++ b/packages/sdk-communication-layer/src/services/RemoteCommunication/ConnectionManager/initCommunicationLayer.ts @@ -74,6 +74,7 @@ export function initCommunicationLayer({ context: state.context, ecies, logging: state.logging, + remote: instance, }); break; default: diff --git a/packages/sdk-communication-layer/src/services/SocketService/MessageHandlers/handleSendMessage.test.ts b/packages/sdk-communication-layer/src/services/SocketService/MessageHandlers/handleSendMessage.test.ts index 63397b003..02ee092b2 100644 --- a/packages/sdk-communication-layer/src/services/SocketService/MessageHandlers/handleSendMessage.test.ts +++ b/packages/sdk-communication-layer/src/services/SocketService/MessageHandlers/handleSendMessage.test.ts @@ -47,6 +47,12 @@ describe('handleSendMessage', () => { areKeysExchanged: jest.fn().mockReturnValue(true), }, }, + remote: { + state: { + analytics: true, + sdkVersion: '1.0.0', + }, + }, } as unknown as SocketService; }); diff --git a/packages/sdk-communication-layer/src/services/SocketService/MessageHandlers/handleSendMessage.ts b/packages/sdk-communication-layer/src/services/SocketService/MessageHandlers/handleSendMessage.ts index f3f5390a4..2c2358fc8 100644 --- a/packages/sdk-communication-layer/src/services/SocketService/MessageHandlers/handleSendMessage.ts +++ b/packages/sdk-communication-layer/src/services/SocketService/MessageHandlers/handleSendMessage.ts @@ -1,7 +1,10 @@ +import { SendAnalytics } from '../../../Analytics'; +import { TrackingEvents } from '../../../types/TrackingEvent'; import { logger } from '../../../utils/logger'; import { SocketService } from '../../../SocketService'; import { CommunicationLayerMessage } from '../../../types/CommunicationLayerMessage'; import { handleKeyHandshake, validateKeyExchange } from '../KeysManager'; +import packageJson from '../../../../package.json'; import { encryptAndSendMessage } from './encryptAndSendMessage'; import { handleRpcReplies } from './handleRpcReplies'; import { trackRpcMethod } from './trackRpcMethod'; @@ -43,10 +46,30 @@ export function handleSendMessage( validateKeyExchange(instance, message); + // trackRpcMethod(instance, message); encryptAndSendMessage(instance, message); + if (instance.remote.state.analytics) { + SendAnalytics( + { + id: instance.remote.state.channelId ?? '', + event: TrackingEvents.SDK_RPC_REQUEST, + sdkVersion: instance.remote.state.sdkVersion, + commLayerVersion: packageJson.version, + walletVersion: instance.remote.state.walletInfo?.version, + params: { + method: message.method, + from: 'mobile', + }, + }, + instance.remote.state.communicationServerUrl, + ).catch((err) => { + console.error(`Cannot send analytics`, err); + }); + } + // Only makes sense on originator side. // wait for reply when eth_requestAccounts is sent. handleRpcReplies(instance, message).catch((err) => { diff --git a/packages/sdk-communication-layer/src/types/TrackingEvent.ts b/packages/sdk-communication-layer/src/types/TrackingEvent.ts index 8d7b189b2..332397f08 100644 --- a/packages/sdk-communication-layer/src/types/TrackingEvent.ts +++ b/packages/sdk-communication-layer/src/types/TrackingEvent.ts @@ -9,6 +9,7 @@ export enum TrackingEvents { TERMINATED = 'sdk_connection_terminated', DISCONNECTED = 'sdk_disconnected', SDK_USE_EXTENSION = 'sdk_use_extension', + SDK_RPC_REQUEST = 'sdk_rpc_request', SDK_EXTENSION_UTILIZED = 'sdk_extension_utilized', SDK_USE_INAPP_BROWSER = 'sdk_use_inapp_browser', } diff --git a/packages/sdk-react/src/MetaMaskProvider.tsx b/packages/sdk-react/src/MetaMaskProvider.tsx index 026f9cb82..bd7a5c582 100644 --- a/packages/sdk-react/src/MetaMaskProvider.tsx +++ b/packages/sdk-react/src/MetaMaskProvider.tsx @@ -238,6 +238,7 @@ const MetaMaskProviderClient = ({ setReady(true); setReadOnlyCalls(_sdk.hasReadOnlyRPCCalls()); }); + }, [sdkOptions]); useEffect(() => { @@ -245,11 +246,15 @@ const MetaMaskProviderClient = ({ return; } - logger(`[MetaMaskProviderClient] init SDK Provider listeners`); + logger(`[MetaMaskProviderClient] init SDK Provider listeners`, sdk); setExtensionActive(sdk.isExtensionActive()); const activeProvider = sdk.getProvider(); + if(!activeProvider) { + console.warn(`[MetaMaskProviderClient] activeProvider is undefined.`); + return; + } setConnected(activeProvider.isConnected()); setAccount(activeProvider.selectedAddress || undefined); setProvider(activeProvider); @@ -286,6 +291,7 @@ const MetaMaskProviderClient = ({ activeProvider.removeListener('disconnect', onDisconnect); activeProvider.removeListener('accountsChanged', onAccountsChanged); activeProvider.removeListener('chainChanged', onChainChanged); + setReady(false); sdk.removeListener(EventType.SERVICE_STATUS, onSDKStatusEvent); }; }, [trigger, sdk, ready]); diff --git a/packages/sdk/src/provider/initializeMobileProvider.ts b/packages/sdk/src/provider/initializeMobileProvider.ts index d15ce640e..e790b20ec 100644 --- a/packages/sdk/src/provider/initializeMobileProvider.ts +++ b/packages/sdk/src/provider/initializeMobileProvider.ts @@ -2,6 +2,7 @@ import { CommunicationLayerPreference, EventType, PlatformType, + TrackingEvents, } from '@metamask/sdk-communication-layer'; import { logger } from '../utils/logger'; import packageJson from '../../package.json'; @@ -158,6 +159,10 @@ const initializeMobileProvider = ({ if (rpcEndpoint && isReadOnlyMethod) { try { const params = args?.[0]?.params; + sdk.analytics?.send({ + event: TrackingEvents.SDK_RPC_REQUEST, + params: { method, from: 'readonly' }, + }); const readOnlyResponse = await rpcRequestHandler({ rpcEndpoint, sdkInfo, diff --git a/packages/sdk/src/provider/wrapExtensionProvider.test.ts b/packages/sdk/src/provider/wrapExtensionProvider.test.ts index 5d0765bc9..4b43a26d4 100644 --- a/packages/sdk/src/provider/wrapExtensionProvider.test.ts +++ b/packages/sdk/src/provider/wrapExtensionProvider.test.ts @@ -1,21 +1,9 @@ -import { Duplex } from 'stream'; import { MetaMaskInpageProvider } from '@metamask/providers'; import { RPC_METHODS } from '../config'; import * as loggerModule from '../utils/logger'; +import { MetaMaskSDK } from '../sdk'; import { wrapExtensionProvider } from './wrapExtensionProvider'; -jest.mock('stream', () => ({ - Duplex: class MockDuplex { - write() { - // Intentionally empty for testing - } - - read() { - // Intentionally empty for testing - } - }, -})); - jest.mock('@metamask/providers', () => { return { MetaMaskInpageProvider: jest.fn(() => { @@ -27,24 +15,37 @@ jest.mock('@metamask/providers', () => { }; }); -function createMockExtensionProvider(): MetaMaskInpageProvider { - const defaultConnectionStream = new Duplex(); - - return new MetaMaskInpageProvider(defaultConnectionStream); -} - describe('wrapExtensionProvider', () => { + let sdkInstance: MetaMaskSDK; + let mockExtensionProvider: MetaMaskInpageProvider; const spyLogger = jest.spyOn(loggerModule, 'logger'); + beforeEach(() => { + jest.clearAllMocks(); + + sdkInstance = { + options: { + dappMetadata: {}, + }, + platformManager: { + getPlatformType: jest.fn(), + }, + } as unknown as MetaMaskSDK; + + mockExtensionProvider = { + request: jest.fn(), + } as unknown as MetaMaskInpageProvider; + }); + it('initializes Proxy around SDKProvider', () => { - const provider = createMockExtensionProvider(); - const wrapped = wrapExtensionProvider({ provider }); + const provider = mockExtensionProvider; + const wrapped = wrapExtensionProvider({ provider, sdkInstance }); expect(typeof wrapped).toBe('object'); }); it('calls the original request method', async () => { - const provider = createMockExtensionProvider(); - const wrapped = wrapExtensionProvider({ provider }); + const provider = mockExtensionProvider; + const wrapped = wrapExtensionProvider({ provider, sdkInstance }); const mockRequest = jest.fn(); provider.request = mockRequest; @@ -56,8 +57,8 @@ describe('wrapExtensionProvider', () => { }); it('logs debug information if debug flag is enabled', async () => { - const provider = createMockExtensionProvider(); - const wrapped = wrapExtensionProvider({ provider }); + const provider = mockExtensionProvider; + const wrapped = wrapExtensionProvider({ provider, sdkInstance }); await wrapped.request({ method: 'someMethod' }); @@ -68,8 +69,8 @@ describe('wrapExtensionProvider', () => { }); it('handles special method correctly', async () => { - const provider = createMockExtensionProvider(); - const wrapped = wrapExtensionProvider({ provider }); + const provider = mockExtensionProvider; + const wrapped = wrapExtensionProvider({ provider, sdkInstance }); const mockRequest = jest.fn().mockResolvedValue('response'); provider.request = mockRequest; diff --git a/packages/sdk/src/provider/wrapExtensionProvider.ts b/packages/sdk/src/provider/wrapExtensionProvider.ts index ddfbabf6f..4c2542051 100644 --- a/packages/sdk/src/provider/wrapExtensionProvider.ts +++ b/packages/sdk/src/provider/wrapExtensionProvider.ts @@ -1,6 +1,8 @@ import { MetaMaskInpageProvider } from '@metamask/providers'; -import { logger } from '../utils/logger'; +import { TrackingEvents } from '@metamask/sdk-communication-layer'; import { RPC_METHODS } from '../config'; +import { MetaMaskSDK } from '../sdk'; +import { logger } from '../utils/logger'; interface RequestArguments { method: string; @@ -9,8 +11,10 @@ interface RequestArguments { export const wrapExtensionProvider = ({ provider, + sdkInstance, }: { provider: MetaMaskInpageProvider; + sdkInstance: MetaMaskSDK; }) => { // prevent double wrapping an invalid provider (it could happen with older web3onboard implementions) // TODO remove after web3onboard is updated @@ -25,6 +29,12 @@ export const wrapExtensionProvider = ({ logger(`[wrapExtensionProvider()] Overwriting request method`, args); const { method, params } = args; + + sdkInstance.analytics?.send({ + event: TrackingEvents.SDK_RPC_REQUEST, + params: { method, from: 'extension' }, + }); + // special method handling if (method === RPC_METHODS.METAMASK_BATCH && Array.isArray(params)) { // params is a list of RPCs to call diff --git a/packages/sdk/src/sdk.test.ts b/packages/sdk/src/sdk.test.ts index 7ffe7d06a..d0a3859a0 100644 --- a/packages/sdk/src/sdk.test.ts +++ b/packages/sdk/src/sdk.test.ts @@ -72,16 +72,22 @@ describe('MetaMaskSDK', () => { expect(sdk.getProvider()).toBe(mockProvider); }); - it('should throw error when getting undefined provider', () => { - expect(() => sdk.getProvider()).toThrow( - 'SDK state invalid -- undefined provider', + it('should log warn message when getting undefined provider', () => { + const spyConsoleWarn = jest.spyOn(console, 'warn'); + sdk.getProvider(); + + expect(spyConsoleWarn).toHaveBeenCalledWith( + 'MetaMaskSDK: No active provider found', ); }); - it('should throw error if SDK is not initialized when calling getProvider', () => { + it('should log warn message if SDK is not initialized when calling getProvider', () => { + const spyConsoleWarn = jest.spyOn(console, 'warn'); + sdk.activeProvider = undefined; - expect(() => sdk.getProvider()).toThrow( - 'SDK state invalid -- undefined provider', + sdk.getProvider(); + expect(spyConsoleWarn).toHaveBeenCalledWith( + 'MetaMaskSDK: No active provider found', ); }); }); diff --git a/packages/sdk/src/sdk.ts b/packages/sdk/src/sdk.ts index 0427f39c3..49644da46 100644 --- a/packages/sdk/src/sdk.ts +++ b/packages/sdk/src/sdk.ts @@ -322,9 +322,10 @@ export class MetaMaskSDK extends EventEmitter2 { } // Return the active ethereum provider object - getProvider(): SDKProvider { + getProvider(): SDKProvider | null { if (!this.activeProvider) { - throw new Error(`SDK state invalid -- undefined provider`); + console.warn(`MetaMaskSDK: No active provider found`); + return null; } return this.activeProvider; diff --git a/packages/sdk/src/services/Analytics.test.ts b/packages/sdk/src/services/Analytics.test.ts index 433e032c8..bf94f8e9a 100644 --- a/packages/sdk/src/services/Analytics.test.ts +++ b/packages/sdk/src/services/Analytics.test.ts @@ -1,16 +1,26 @@ import { SendAnalytics, TrackingEvents, + AnalyticsProps, } from '@metamask/sdk-communication-layer'; import * as loggerModule from '../utils/logger'; -import { Analytics, AnalyticsProps } from './Analytics'; // Replace with your actual import path - +import { Analytics } from './Analytics'; +// Replace with your actual import path jest.mock('@metamask/sdk-communication-layer'); const mockSendAnalytics = SendAnalytics as jest.Mock; +interface Props { + serverUrl: string; + originatorInfo: AnalyticsProps['originationInfo']; + enabled?: boolean; +} describe('Analytics', () => { - let props: AnalyticsProps; + let props: { + serverUrl: string; + originatorInfo: AnalyticsProps['originationInfo']; + enabled?: boolean; + }; const spyLogger = jest.spyOn(loggerModule, 'logger'); beforeEach(() => { @@ -19,12 +29,12 @@ describe('Analytics', () => { mockSendAnalytics.mockResolvedValue(undefined); props = { - serverURL: 'https://test.server.url', - metadata: { - url: 'https://test.url', - title: 'Test Title', + serverUrl: 'https://custom.server.url', + originatorInfo: { + url: 'https://dapp.url', + title: 'DApp Name', platform: 'web', - source: 'test-source', + source: 'custom-source', }, }; }); @@ -36,7 +46,7 @@ describe('Analytics', () => { }); it('should initialize with custom values', () => { - const customProps: AnalyticsProps = { + const customProps: Props = { ...props, enabled: true, }; @@ -54,7 +64,7 @@ describe('Analytics', () => { expect.objectContaining({ event, }), - props.serverURL, + props.serverUrl, ); }); diff --git a/packages/sdk/src/services/Analytics.ts b/packages/sdk/src/services/Analytics.ts index 5dc6a225d..9eb59dd8f 100644 --- a/packages/sdk/src/services/Analytics.ts +++ b/packages/sdk/src/services/Analytics.ts @@ -1,22 +1,12 @@ import { DEFAULT_SERVER_URL, SendAnalytics, + AnalyticsProps, TrackingEvents, } from '@metamask/sdk-communication-layer'; import { logger } from '../utils/logger'; import packageJson from '../../package.json'; -export interface AnalyticsProps { - serverURL: string; - enabled?: boolean; - metadata?: { - url: string; - title: string; - platform: string; - source: string; - }; -} - export const ANALYTICS_CONSTANTS = { DEFAULT_ID: 'sdk', NO_VERSION: 'NONE', @@ -27,29 +17,43 @@ export class Analytics { #enabled: boolean; - #metadata: Readonly; - - constructor(props: AnalyticsProps) { - this.#serverURL = props.serverURL; - this.#metadata = props.metadata || undefined; - this.#enabled = props.enabled ?? true; + #originatorInfo: Readonly; + + constructor({ + serverUrl, + enabled, + originatorInfo, + }: { + serverUrl: string; + originatorInfo: AnalyticsProps['originationInfo']; + enabled?: boolean; + }) { + this.#serverURL = serverUrl; + this.#originatorInfo = originatorInfo; + this.#enabled = enabled ?? true; } - send({ event }: { event: TrackingEvents }) { + send({ + event, + params, + }: { + event: TrackingEvents; + params?: Record; + }) { if (!this.#enabled) { return; } - SendAnalytics( - { - id: ANALYTICS_CONSTANTS.DEFAULT_ID, - event, - sdkVersion: packageJson.version, - commLayerVersion: ANALYTICS_CONSTANTS.NO_VERSION, - originationInfo: this.#metadata, - }, - this.#serverURL, - ).catch((error) => { + const props: AnalyticsProps = { + id: ANALYTICS_CONSTANTS.DEFAULT_ID, + event, + sdkVersion: packageJson.version, + originationInfo: this.#originatorInfo, + params, + }; + logger(`[Analytics: send()] event: ${event}`, props); + + SendAnalytics(props, this.#serverURL).catch((error: unknown) => { logger(`[Analytics: send()] error: ${error}`); }); } diff --git a/packages/sdk/src/services/MetaMaskSDK/InitializerManager/handleAutoAndExtensionConnections.test.ts b/packages/sdk/src/services/MetaMaskSDK/InitializerManager/handleAutoAndExtensionConnections.test.ts index 4944f208c..e3592e9f4 100644 --- a/packages/sdk/src/services/MetaMaskSDK/InitializerManager/handleAutoAndExtensionConnections.test.ts +++ b/packages/sdk/src/services/MetaMaskSDK/InitializerManager/handleAutoAndExtensionConnections.test.ts @@ -1,8 +1,7 @@ import { SendAnalytics } from '@metamask/sdk-communication-layer'; -import { MetaMaskSDK } from '../../../sdk'; import { STORAGE_PROVIDER_TYPE } from '../../../config'; +import { MetaMaskSDK } from '../../../sdk'; import { connectWithExtensionProvider } from '../ProviderManager'; -import { ANALYTICS_CONSTANTS } from '../../Analytics'; import { handleAutoAndExtensionConnections } from './handleAutoAndExtensionConnections'; jest.mock('../ProviderManager', () => ({ @@ -50,35 +49,6 @@ describe('handleAutoAndExtensionConnections', () => { } as unknown as MetaMaskSDK; }); - it('should send SDK_EXTENSION_UTILIZED analytics event with the right metadata when remoteConnection available', async () => { - instance.remoteConnection = { - state: { - connector: { - state: { - originatorInfo: { - id: 'defaultId', - }, - }, - }, - }, - } as unknown as MetaMaskSDK['remoteConnection']; - - const analyticsData = { - id: ANALYTICS_CONSTANTS.DEFAULT_ID, - event: 'SDK_EXTENSION_UTILIZED', - ...instance.remoteConnection?.state.connector?.state.originatorInfo, - commLayerVersion: ANALYTICS_CONSTANTS.NO_VERSION, - }; - - await handleAutoAndExtensionConnections(instance, true); - - expect(mockSendAnalytics).toHaveBeenCalled(); - expect(mockSendAnalytics).toHaveBeenCalledWith( - analyticsData, - expect.any(String), - ); - }); - it('should NOT send SDK_EXTENSION_UTILIZED analytics event with the right metadata when remoteConnection is NOT available', async () => { instance.remoteConnection = undefined; diff --git a/packages/sdk/src/services/MetaMaskSDK/InitializerManager/handleAutoAndExtensionConnections.ts b/packages/sdk/src/services/MetaMaskSDK/InitializerManager/handleAutoAndExtensionConnections.ts index 729a795ad..574a37f88 100644 --- a/packages/sdk/src/services/MetaMaskSDK/InitializerManager/handleAutoAndExtensionConnections.ts +++ b/packages/sdk/src/services/MetaMaskSDK/InitializerManager/handleAutoAndExtensionConnections.ts @@ -1,12 +1,8 @@ -import { - SendAnalytics, - TrackingEvents, -} from '@metamask/sdk-communication-layer'; -import { logger } from '../../../utils/logger'; +import { TrackingEvents } from '@metamask/sdk-communication-layer'; import { STORAGE_PROVIDER_TYPE } from '../../../config'; import { MetaMaskSDK } from '../../../sdk'; +import { logger } from '../../../utils/logger'; import { connectWithExtensionProvider } from '../ProviderManager'; -import { ANALYTICS_CONSTANTS } from '../../Analytics'; /** * Handles automatic and extension-based connections for MetaMask SDK. @@ -32,31 +28,9 @@ export async function handleAutoAndExtensionConnections( `[MetaMaskSDK: handleAutoAndExtensionConnections()] preferExtension is detected -- connect with it.`, ); - const { remoteConnection } = instance; - - if (remoteConnection) { - const { - state: { connector }, - } = remoteConnection; - - const originatorInfo = connector?.state.originatorInfo ?? {}; - const communicationServerUrl = ''; - - const analyticsData = { - id: ANALYTICS_CONSTANTS.DEFAULT_ID, - event: TrackingEvents.SDK_EXTENSION_UTILIZED, - ...originatorInfo, - commLayerVersion: ANALYTICS_CONSTANTS.NO_VERSION, - }; - - if (instance.options.enableAnalytics) { - SendAnalytics(analyticsData, communicationServerUrl).catch((_err) => { - console.warn( - `Can't send the SDK_EXTENSION_UTILIZED analytics event...`, - ); - }); - } - } + instance.analytics?.send({ + event: TrackingEvents.SDK_EXTENSION_UTILIZED, + }); connectWithExtensionProvider(instance).catch((_err) => { console.warn(`Can't connect with MetaMask extension...`, _err); diff --git a/packages/sdk/src/services/MetaMaskSDK/InitializerManager/performSDKInitialization.ts b/packages/sdk/src/services/MetaMaskSDK/InitializerManager/performSDKInitialization.ts index fdd0087c1..804e41226 100644 --- a/packages/sdk/src/services/MetaMaskSDK/InitializerManager/performSDKInitialization.ts +++ b/packages/sdk/src/services/MetaMaskSDK/InitializerManager/performSDKInitialization.ts @@ -83,6 +83,7 @@ export async function performSDKInitialization(instance: MetaMaskSDK) { runtimeLogging.keyExchangeLayer = true; runtimeLogging.remoteLayer = true; runtimeLogging.serviceLayer = true; + runtimeLogging.plaintext = true; } await initializeI18next(instance); diff --git a/packages/sdk/src/services/MetaMaskSDK/InitializerManager/setupAnalytics.test.ts b/packages/sdk/src/services/MetaMaskSDK/InitializerManager/setupAnalytics.test.ts index 8a724d3c2..8811bc16f 100644 --- a/packages/sdk/src/services/MetaMaskSDK/InitializerManager/setupAnalytics.test.ts +++ b/packages/sdk/src/services/MetaMaskSDK/InitializerManager/setupAnalytics.test.ts @@ -33,9 +33,8 @@ describe('setupAnalytics', () => { await setupAnalytics(instance); expect(Analytics).toHaveBeenCalledWith({ - serverURL: DEFAULT_SERVER_URL, - debug: undefined, - metadata: { + serverUrl: DEFAULT_SERVER_URL, + originatorInfo: { url: '', title: '', platform: '', @@ -57,8 +56,8 @@ describe('setupAnalytics', () => { await setupAnalytics(instance); expect(Analytics).toHaveBeenCalledWith({ - serverURL: 'https://custom.server.url', - metadata: { + serverUrl: 'https://custom.server.url', + originatorInfo: { url: 'https://dapp.url', title: 'DApp Name', platform: 'web', diff --git a/packages/sdk/src/services/MetaMaskSDK/InitializerManager/setupAnalytics.ts b/packages/sdk/src/services/MetaMaskSDK/InitializerManager/setupAnalytics.ts index aecd6af6f..50433f660 100644 --- a/packages/sdk/src/services/MetaMaskSDK/InitializerManager/setupAnalytics.ts +++ b/packages/sdk/src/services/MetaMaskSDK/InitializerManager/setupAnalytics.ts @@ -18,8 +18,8 @@ export async function setupAnalytics(instance: MetaMaskSDK) { const platformType = instance.platformManager?.getPlatformType(); instance.analytics = new Analytics({ - serverURL: options.communicationServerUrl ?? DEFAULT_SERVER_URL, - metadata: { + serverUrl: options.communicationServerUrl ?? DEFAULT_SERVER_URL, + originatorInfo: { url: options.dappMetadata.url ?? '', title: options.dappMetadata.name ?? '', platform: platformType ?? '', diff --git a/packages/sdk/src/services/MetaMaskSDK/InitializerManager/setupExtensionPreferences.ts b/packages/sdk/src/services/MetaMaskSDK/InitializerManager/setupExtensionPreferences.ts index 12f78e230..a0a7f46ad 100644 --- a/packages/sdk/src/services/MetaMaskSDK/InitializerManager/setupExtensionPreferences.ts +++ b/packages/sdk/src/services/MetaMaskSDK/InitializerManager/setupExtensionPreferences.ts @@ -41,6 +41,7 @@ export async function setupExtensionPreferences(instance: MetaMaskSDK) { try { metamaskBrowserExtension = await getBrowserExtension({ mustBeMetaMask: true, + sdkInstance: instance, }); window.extension = metamaskBrowserExtension; diff --git a/packages/sdk/src/utils/get-browser-extension.test.ts b/packages/sdk/src/utils/get-browser-extension.test.ts index 154319e32..9b7119aaa 100644 --- a/packages/sdk/src/utils/get-browser-extension.test.ts +++ b/packages/sdk/src/utils/get-browser-extension.test.ts @@ -1,19 +1,31 @@ +import { MetaMaskSDK } from '../sdk'; import { getBrowserExtension } from './get-browser-extension'; import { eip6963RequestProvider } from './eip6963RequestProvider'; jest.mock('./eip6963RequestProvider'); describe('getBrowserExtension', () => { + let sdkInstance: MetaMaskSDK; + beforeEach(() => { jest.clearAllMocks(); + + sdkInstance = { + options: { + dappMetadata: {}, + }, + platformManager: { + getPlatformType: jest.fn(), + }, + } as unknown as MetaMaskSDK; }); it('should throw an error if window is undefined', async () => { global.window = undefined as any; - await expect(getBrowserExtension({ mustBeMetaMask: true })).rejects.toThrow( - 'window not available', - ); + await expect( + getBrowserExtension({ mustBeMetaMask: true, sdkInstance }), + ).rejects.toThrow('window not available'); }); it('should return baseProvider if eip6963RequestProvider resolves successfully', async () => { @@ -21,7 +33,10 @@ describe('getBrowserExtension', () => { global.window = { ethereum: {} } as any; (eip6963RequestProvider as jest.Mock).mockResolvedValue(mockProvider); - const res = await getBrowserExtension({ mustBeMetaMask: true }); + const res = await getBrowserExtension({ + mustBeMetaMask: true, + sdkInstance, + }); expect(res).toStrictEqual(mockProvider); }); @@ -32,9 +47,9 @@ describe('getBrowserExtension', () => { ); global.window = {} as any; - await expect(getBrowserExtension({ mustBeMetaMask: true })).rejects.toThrow( - 'Ethereum not found in window object', - ); + await expect( + getBrowserExtension({ mustBeMetaMask: true, sdkInstance }), + ).rejects.toThrow('Ethereum not found in window object'); }); it('should throw an error if no suitable provider is found', async () => { @@ -43,9 +58,9 @@ describe('getBrowserExtension', () => { ); global.window = { ethereum: { providers: [] } } as any; - await expect(getBrowserExtension({ mustBeMetaMask: true })).rejects.toThrow( - 'No suitable provider found', - ); + await expect( + getBrowserExtension({ mustBeMetaMask: true, sdkInstance }), + ).rejects.toThrow('No suitable provider found'); }); it('should return MetaMask provider if mustBeMetaMask is true', async () => { @@ -55,7 +70,10 @@ describe('getBrowserExtension', () => { ); global.window = { ethereum: { providers: [mockProvider] } } as any; - const res = await getBrowserExtension({ mustBeMetaMask: true }); + const res = await getBrowserExtension({ + mustBeMetaMask: true, + sdkInstance, + }); expect(res).toStrictEqual(mockProvider); }); @@ -67,7 +85,10 @@ describe('getBrowserExtension', () => { ); global.window = { ethereum: { providers: [mockProvider] } } as any; - const res = await getBrowserExtension({ mustBeMetaMask: false }); + const res = await getBrowserExtension({ + mustBeMetaMask: false, + sdkInstance, + }); expect(res).toStrictEqual(mockProvider); }); @@ -78,9 +99,9 @@ describe('getBrowserExtension', () => { ); global.window = { ethereum: { isMetaMask: false } } as any; - await expect(getBrowserExtension({ mustBeMetaMask: true })).rejects.toThrow( - 'MetaMask provider not found in Ethereum', - ); + await expect( + getBrowserExtension({ mustBeMetaMask: true, sdkInstance }), + ).rejects.toThrow('MetaMask provider not found in Ethereum'); }); it('should return ethereum object if mustBeMetaMask is false and ethereum object exists', async () => { @@ -90,7 +111,10 @@ describe('getBrowserExtension', () => { ); global.window = { ethereum: ethereumObj } as any; - const res = await getBrowserExtension({ mustBeMetaMask: false }); + const res = await getBrowserExtension({ + mustBeMetaMask: false, + sdkInstance, + }); expect(res).toStrictEqual(ethereumObj); }); diff --git a/packages/sdk/src/utils/get-browser-extension.ts b/packages/sdk/src/utils/get-browser-extension.ts index c90127b76..e736986d5 100644 --- a/packages/sdk/src/utils/get-browser-extension.ts +++ b/packages/sdk/src/utils/get-browser-extension.ts @@ -1,11 +1,14 @@ import { MetaMaskInpageProvider } from '@metamask/providers'; +import { MetaMaskSDK } from '../sdk'; import { wrapExtensionProvider } from '../provider/wrapExtensionProvider'; import { eip6963RequestProvider } from './eip6963RequestProvider'; export async function getBrowserExtension({ mustBeMetaMask, + sdkInstance, }: { mustBeMetaMask: boolean; + sdkInstance: MetaMaskSDK; }): Promise { if (typeof window === 'undefined') { throw new Error(`window not available`); @@ -15,7 +18,7 @@ export async function getBrowserExtension({ try { extensionProvider = await eip6963RequestProvider(); - return wrapExtensionProvider({ provider: extensionProvider }); + return wrapExtensionProvider({ provider: extensionProvider, sdkInstance }); } catch (e) { const { ethereum } = window; @@ -36,7 +39,7 @@ export async function getBrowserExtension({ throw new Error('No suitable provider found'); } - return wrapExtensionProvider({ provider }); + return wrapExtensionProvider({ provider, sdkInstance }); } } else if (mustBeMetaMask && !ethereum.isMetaMask) { throw new Error('MetaMask provider not found in Ethereum'); @@ -44,6 +47,7 @@ export async function getBrowserExtension({ return wrapExtensionProvider({ provider: ethereum, + sdkInstance, }); } }