diff --git a/app/components/Approvals/SignatureApproval/SignatureApproval.tsx b/app/components/Approvals/SignatureApproval/SignatureApproval.tsx index df0c2c6726a..11c3d315541 100644 --- a/app/components/Approvals/SignatureApproval/SignatureApproval.tsx +++ b/app/components/Approvals/SignatureApproval/SignatureApproval.tsx @@ -1,10 +1,12 @@ -import React, { useCallback } from 'react'; +import React, { useCallback, useEffect } from 'react'; import useApprovalRequest from '../../Views/confirmations/hooks/useApprovalRequest'; import { ApprovalTypes } from '../../../core/RPCMethods/RPCMethodMiddleware'; import SignatureRequestRoot from '../../Views/confirmations/components/SignatureRequest/Root'; +import { endTrace, TraceName } from '../../../util/trace'; const SignatureApproval = () => { const { approvalRequest, onReject, onConfirm } = useApprovalRequest(); + const signatureRequestId = approvalRequest?.requestData?.requestId; const onSignConfirm = useCallback(async () => { await onConfirm({ @@ -14,6 +16,13 @@ const SignatureApproval = () => { }); }, [onConfirm]); + useEffect(() => { + endTrace({ + name: TraceName.NotificationDisplay, + id: signatureRequestId, + }); + }, [signatureRequestId]); + const messageParams = approvalRequest && [ApprovalTypes.PERSONAL_SIGN, ApprovalTypes.ETH_SIGN_TYPED_DATA].includes( diff --git a/app/components/Views/confirmations/components/SignatureRequest/Root/Root.tsx b/app/components/Views/confirmations/components/SignatureRequest/Root/Root.tsx index 0dfb8e11683..f610b170a57 100644 --- a/app/components/Views/confirmations/components/SignatureRequest/Root/Root.tsx +++ b/app/components/Views/confirmations/components/SignatureRequest/Root/Root.tsx @@ -2,6 +2,7 @@ import Modal from 'react-native-modal'; import React, { useEffect, useState } from 'react'; import { StyleSheet } from 'react-native'; import { useNavigation } from '@react-navigation/native'; +import { useSelector } from 'react-redux'; import setSignatureRequestSecurityAlertResponse from '../../../../../../actions/signatureRequest'; import { store } from '../../../../../../store'; import { useTheme } from '../../../../../../util/theme'; @@ -10,7 +11,6 @@ import PersonalSign from '../../PersonalSign'; import TypedSign from '../../TypedSign'; import { MessageParams } from '../types'; import { ApprovalTypes } from '../../../../../../core/RPCMethods/RPCMethodMiddleware'; -import { useSelector } from 'react-redux'; interface RootProps { messageParams?: MessageParams; diff --git a/app/core/BackgroundBridge/BackgroundBridge.js b/app/core/BackgroundBridge/BackgroundBridge.js index a59ec26470d..5051d77cbd5 100644 --- a/app/core/BackgroundBridge/BackgroundBridge.js +++ b/app/core/BackgroundBridge/BackgroundBridge.js @@ -39,6 +39,7 @@ import { NetworkStatus } from '@metamask/network-controller'; import { NETWORK_ID_LOADING } from '../redux/slices/inpageProvider'; import createUnsupportedMethodMiddleware from '../RPCMethods/createUnsupportedMethodMiddleware'; import createLegacyMethodMiddleware from '../RPCMethods/createLegacyMethodMiddleware'; +import createTracingMiddleware from '../createTracingMiddleware'; const legacyNetworkId = () => { const { networksMetadata, selectedNetworkClientId } = @@ -442,6 +443,9 @@ export class BackgroundBridge extends EventEmitter { }), ); + // Sentry tracing middleware + engine.push(createTracingMiddleware()); + // Append PermissionController middleware engine.push( Engine.context.PermissionController.createPermissionMiddleware({ diff --git a/app/core/Engine.ts b/app/core/Engine.ts index 0c9901b42a5..82297a29e06 100644 --- a/app/core/Engine.ts +++ b/app/core/Engine.ts @@ -198,6 +198,7 @@ import { SignatureController, SignatureControllerActions, SignatureControllerEvents, + SignatureControllerOptions, } from '@metamask/signature-controller'; import { hasProperty, Hex, Json } from '@metamask/utils'; // TODO: Export this type from the package directly @@ -250,6 +251,7 @@ import { keyringSnapPermissionsBuilder } from './SnapKeyring/keyringSnapsPermiss import { HandleSnapRequestArgs } from './Snaps/types'; import { handleSnapRequest } from './Snaps/utils'; ///: END:ONLY_INCLUDE_IF +import { trace } from '../util/trace'; const NON_EMPTY = 'NON_EMPTY'; @@ -1602,6 +1604,8 @@ class Engine { networkController.getNetworkClientById( networkController?.state.selectedNetworkClientId, ).configuration.chainId, + // This casting expected due to mismatch of browser and react-native version of Sentry traceContext + trace: trace as unknown as SignatureControllerOptions['trace'], }), loggingController, ///: BEGIN:ONLY_INCLUDE_IF(preinstalled-snaps,external-snaps) diff --git a/app/core/RPCMethods/RPCMethodMiddleware.test.ts b/app/core/RPCMethods/RPCMethodMiddleware.test.ts index 50bc62f9a84..c482a2f2f73 100644 --- a/app/core/RPCMethods/RPCMethodMiddleware.test.ts +++ b/app/core/RPCMethods/RPCMethodMiddleware.test.ts @@ -1257,6 +1257,7 @@ describe('getRpcMethodMiddleware', () => { from: addressMock, meta: expect.any(Object), origin: hostMock, + requestId: 1, }); }); @@ -1306,6 +1307,7 @@ describe('getRpcMethodMiddleware', () => { from: addressMock, meta: expect.any(Object), origin: hostMock, + requestId: 1, }, expect.any(Object), version, diff --git a/app/core/RPCMethods/RPCMethodMiddleware.ts b/app/core/RPCMethods/RPCMethodMiddleware.ts index 51c2c293340..0928bb73267 100644 --- a/app/core/RPCMethods/RPCMethodMiddleware.ts +++ b/app/core/RPCMethods/RPCMethodMiddleware.ts @@ -39,6 +39,7 @@ import Logger from '../../../app/util/Logger'; import DevLogger from '../SDKConnect/utils/DevLogger'; import { addTransaction } from '../../util/transaction-controller'; import Routes from '../../constants/navigation/Routes'; +import { endTrace, trace, TraceName } from '../../util/trace'; // TODO: Replace "any" with type // eslint-disable-next-line @typescript-eslint/no-explicit-any @@ -232,6 +233,7 @@ const generateRawSignature = async ({ // eslint-disable-next-line @typescript-eslint/no-explicit-any checkTabActive: any; }) => { + endTrace({ name: TraceName.Middleware, id: req.id }); const { SignatureController } = Engine.context; const pageMeta = { @@ -260,6 +262,7 @@ const generateRawSignature = async ({ { data: req.params[1], from: req.params[0], + requestId: req.id, ...pageMeta, channelId, origin: hostname, @@ -271,6 +274,7 @@ const generateRawSignature = async ({ parseJsonData: false, }, ); + endTrace({ name: TraceName.Signature, id: req.id }); return rawSig; }; @@ -538,6 +542,7 @@ export const getRpcMethodMiddleware = ({ const params = { data: firstParam, from: secondParam, + requestId: req.id, }; if (resemblesAddress(firstParam) && !resemblesAddress(secondParam)) { @@ -567,13 +572,18 @@ export const getRpcMethodMiddleware = ({ }); DevLogger.log(`personal_sign`, params, pageMeta, hostname); - PPOMUtil.validateRequest(req); + + trace( + { name: TraceName.PPOMValidation, parentContext: req.traceContext }, + () => PPOMUtil.validateRequest(req), + ); const rawSig = await SignatureController.newUnsignedPersonalMessage({ ...params, ...pageMeta, origin: hostname, }); + endTrace({ name: TraceName.Signature, id: req.id }); res.result = rawSig; }, @@ -594,6 +604,7 @@ export const getRpcMethodMiddleware = ({ }, eth_signTypedData: async () => { + endTrace({ name: TraceName.Middleware, id: req.id }); const { SignatureController } = Engine.context; const pageMeta = { meta: { @@ -616,18 +627,23 @@ export const getRpcMethodMiddleware = ({ isWalletConnect, }); - PPOMUtil.validateRequest(req); + trace( + { name: TraceName.PPOMValidation, parentContext: req.traceContext }, + () => PPOMUtil.validateRequest(req), + ); const rawSig = await SignatureController.newUnsignedTypedMessage( { data: req.params[0], from: req.params[1], + requestId: req.id, ...pageMeta, origin: hostname, }, req, 'V1', ); + endTrace({ name: TraceName.Signature, id: req.id }); res.result = rawSig; }, @@ -638,7 +654,12 @@ export const getRpcMethodMiddleware = ({ ? JSON.parse(req.params[1]) : req.params[1]; const chainId = data.domain.chainId; - PPOMUtil.validateRequest(req); + + trace( + { name: TraceName.PPOMValidation, parentContext: req.traceContext }, + () => PPOMUtil.validateRequest(req), + ); + res.result = await generateRawSignature({ version: 'V3', req, @@ -659,7 +680,12 @@ export const getRpcMethodMiddleware = ({ eth_signTypedData_v4: async () => { const data = JSON.parse(req.params[1]); const chainId = data.domain.chainId; - PPOMUtil.validateRequest(req); + + trace( + { name: TraceName.PPOMValidation, parentContext: req.traceContext }, + () => PPOMUtil.validateRequest(req), + ); + res.result = await generateRawSignature({ version: 'V4', req, diff --git a/app/core/createTracingMiddleware/index.test.ts b/app/core/createTracingMiddleware/index.test.ts new file mode 100644 index 00000000000..c9a6f84ad2d --- /dev/null +++ b/app/core/createTracingMiddleware/index.test.ts @@ -0,0 +1,39 @@ +import type { JsonRpcRequest, PendingJsonRpcResponse } from 'json-rpc-engine'; +import { default as createTracingMiddleware, MESSAGE_TYPE } from './index'; + +const REQUEST_MOCK = { + id: 'testId', + method: MESSAGE_TYPE.PERSONAL_SIGN, +} as JsonRpcRequest; +const RESPONSE_MOCK = {} as PendingJsonRpcResponse; +const NEXT_MOCK = jest.fn(); + +jest.mock('../../util/trace', () => ({ + ...jest.requireActual('../../util/trace'), + trace: jest.fn().mockResolvedValue({}), +})); + +describe('createTracingMiddleware', () => { + let request: JsonRpcRequest & { traceContext?: unknown }; + beforeEach(() => { + jest.clearAllMocks(); + request = { ...REQUEST_MOCK }; + }); + + it('adds trace context to request if method is send transaction', async () => { + await createTracingMiddleware()(request, RESPONSE_MOCK, NEXT_MOCK); + expect(request.traceContext).toBeDefined(); + }); + + it('does not add trace context to request if method not supported', async () => { + request.method = 'unsupportedMethod'; + + await createTracingMiddleware()(request, RESPONSE_MOCK, NEXT_MOCK); + + expect(request.traceContext).toBeUndefined(); + }); + it('calls next', async () => { + await createTracingMiddleware()(request, RESPONSE_MOCK, NEXT_MOCK); + expect(NEXT_MOCK).toHaveBeenCalledTimes(1); + }); +}); diff --git a/app/core/createTracingMiddleware/index.ts b/app/core/createTracingMiddleware/index.ts new file mode 100644 index 00000000000..092447d3d01 --- /dev/null +++ b/app/core/createTracingMiddleware/index.ts @@ -0,0 +1,45 @@ +import type { JsonRpcRequest, PendingJsonRpcResponse } from 'json-rpc-engine'; +import { trace, TraceName } from '../../util/trace'; + +export const MESSAGE_TYPE = { + ETH_SIGN_TYPED_DATA: 'eth_signTypedData', + ETH_SIGN_TYPED_DATA_V1: 'eth_signTypedData_v1', + ETH_SIGN_TYPED_DATA_V3: 'eth_signTypedData_v3', + ETH_SIGN_TYPED_DATA_V4: 'eth_signTypedData_v4', + PERSONAL_SIGN: 'personal_sign', +}; + +const METHOD_TYPE_TO_TRACE_NAME: Record = { + [MESSAGE_TYPE.ETH_SIGN_TYPED_DATA]: TraceName.Signature, + [MESSAGE_TYPE.ETH_SIGN_TYPED_DATA_V1]: TraceName.Signature, + [MESSAGE_TYPE.ETH_SIGN_TYPED_DATA_V3]: TraceName.Signature, + [MESSAGE_TYPE.ETH_SIGN_TYPED_DATA_V4]: TraceName.Signature, + [MESSAGE_TYPE.PERSONAL_SIGN]: TraceName.Signature, +}; + +export default function createTracingMiddleware() { + return async function tracingMiddleware( + req: JsonRpcRequest & { traceContext?: unknown }, + _res: PendingJsonRpcResponse, + next: () => void, + ) { + const { id, method } = req; + + const traceName = METHOD_TYPE_TO_TRACE_NAME[method]; + + if (traceName) { + req.traceContext = await trace({ + name: traceName, + id: id as string, + }); + + await trace({ + name: TraceName.Middleware, + id: id as string, + parentContext: req.traceContext, + }); + } + + next(); + }; +} diff --git a/app/util/trace.ts b/app/util/trace.ts index d74ca003249..8275c521b84 100644 --- a/app/util/trace.ts +++ b/app/util/trace.ts @@ -16,6 +16,9 @@ export enum TraceName { Middleware = 'Middleware', NestedTest1 = 'Nested Test 1', NestedTest2 = 'Nested Test 2', + NotificationDisplay = 'Notification Display', + PPOMValidation = 'PPOM Validation', + Signature = 'Signature', } const ID_DEFAULT = 'default';