Skip to content

Commit

Permalink
feat: rpc event tracking (#745)
Browse files Browse the repository at this point in the history
* feat: rpc event tracking

* fix: unit test

* feat: use flat analytics params

* feat: fix unit tests

---------

Co-authored-by: Omridan159 <[email protected]>
  • Loading branch information
abretonc7s and omridan159 authored Mar 8, 2024
1 parent 528e4ef commit 741bb27
Show file tree
Hide file tree
Showing 24 changed files with 312 additions and 185 deletions.
2 changes: 1 addition & 1 deletion packages/devnext/src/pages/_app.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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 ?? '',
Expand Down
114 changes: 93 additions & 21 deletions packages/sdk-communication-layer/src/Analytics.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<string, unknown>;
}

// 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
});
};
7 changes: 7 additions & 0 deletions packages/sdk-communication-layer/src/SocketService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -32,6 +33,7 @@ export interface SocketServiceProps {
communicationServerUrl: string;
context: string;
ecies?: ECIESProps;
remote: RemoteCommunication;
logging?: CommunicationLayerLoggingOptions;
}

Expand All @@ -52,6 +54,7 @@ export interface SocketServiceState {
hasPlaintext: boolean;
socket?: Socket;
setupChannelListeners?: boolean;
analytics?: boolean;
keyExchange?: KeyExchange;
}

Expand Down Expand Up @@ -86,6 +89,8 @@ export class SocketService extends EventEmitter2 implements CommunicationLayer {
communicationServerUrl: '',
};

remote: RemoteCommunication;

constructor({
otherPublicKey,
reconnect,
Expand All @@ -94,6 +99,7 @@ export class SocketService extends EventEmitter2 implements CommunicationLayer {
communicationServerUrl,
context,
ecies,
remote,
logging,
}: SocketServiceProps) {
super();
Expand All @@ -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');
Expand Down
3 changes: 2 additions & 1 deletion packages/sdk-communication-layer/src/index.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { SendAnalytics } from './Analytics';
import { SendAnalytics, AnalyticsProps } from './Analytics';
import { ECIES, ECIESProps } from './ECIES';
import {
RemoteCommunication,
Expand Down Expand Up @@ -48,6 +48,7 @@ export type {
StorageManager,
StorageManagerProps,
WalletInfo,
AnalyticsProps,
};

export {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,7 @@ export function initCommunicationLayer({
context: state.context,
ecies,
logging: state.logging,
remote: instance,
});
break;
default:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,12 @@ describe('handleSendMessage', () => {
areKeysExchanged: jest.fn().mockReturnValue(true),
},
},
remote: {
state: {
analytics: true,
sdkVersion: '1.0.0',
},
},
} as unknown as SocketService;
});

Expand Down
Original file line number Diff line number Diff line change
@@ -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';
Expand Down Expand Up @@ -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) => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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',
}
8 changes: 7 additions & 1 deletion packages/sdk-react/src/MetaMaskProvider.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -238,18 +238,23 @@ const MetaMaskProviderClient = ({
setReady(true);
setReadOnlyCalls(_sdk.hasReadOnlyRPCCalls());
});

}, [sdkOptions]);

useEffect(() => {
if (!ready || !sdk) {
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);
Expand Down Expand Up @@ -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]);
Expand Down
5 changes: 5 additions & 0 deletions packages/sdk/src/provider/initializeMobileProvider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import {
CommunicationLayerPreference,
EventType,
PlatformType,
TrackingEvents,
} from '@metamask/sdk-communication-layer';
import { logger } from '../utils/logger';
import packageJson from '../../package.json';
Expand Down Expand Up @@ -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,
Expand Down
Loading

0 comments on commit 741bb27

Please sign in to comment.