From d6df3700f7356772bdbb689c389ff1ad4ea9e4ef Mon Sep 17 00:00:00 2001 From: Brad Decker Date: Mon, 4 Apr 2022 14:26:13 -0500 Subject: [PATCH] track rpc method usage (#14269) --- .../lib/createRPCMethodTrackingMiddleware.js | 86 +++++++++++++++++++ app/scripts/metamask-controller.js | 12 +++ shared/constants/metametrics.js | 7 ++ 3 files changed, 105 insertions(+) create mode 100644 app/scripts/lib/createRPCMethodTrackingMiddleware.js diff --git a/app/scripts/lib/createRPCMethodTrackingMiddleware.js b/app/scripts/lib/createRPCMethodTrackingMiddleware.js new file mode 100644 index 000000000..0b47c3462 --- /dev/null +++ b/app/scripts/lib/createRPCMethodTrackingMiddleware.js @@ -0,0 +1,86 @@ +import { EVENT_NAMES } from '../../../shared/constants/metametrics'; +import { SECOND } from '../../../shared/constants/time'; + +const USER_PROMPTED_EVENT_NAME_MAP = { + eth_signTypedData_v4: EVENT_NAMES.SIGNATURE_REQUESTED, + eth_signTypedData_v3: EVENT_NAMES.SIGNATURE_REQUESTED, + eth_signTypedData: EVENT_NAMES.SIGNATURE_REQUESTED, + eth_personal_sign: EVENT_NAMES.SIGNATURE_REQUESTED, + eth_sign: EVENT_NAMES.SIGNATURE_REQUESTED, + eth_getEncryptionPublicKey: EVENT_NAMES.ENCRYPTION_PUBLIC_KEY_REQUESTED, + eth_decrypt: EVENT_NAMES.DECRYPTION_REQUESTED, + wallet_requestPermissions: EVENT_NAMES.PERMISSIONS_REQUESTED, + eth_requestAccounts: EVENT_NAMES.PERMISSIONS_REQUESTED, +}; + +const samplingTimeouts = {}; + +/** + * Returns a middleware that tracks inpage_provider usage using sampling for + * each type of event except those that require user interaction, such as + * signature requests + * + * @param {object} opts - options for the rpc method tracking middleware + * @param {Function} opts.trackEvent - trackEvent method from MetaMetricsController + * @param {Function} opts.getMetricsState - get the state of MetaMetricsController + * @returns {Function} + */ +export default function createRPCMethodTrackingMiddleware({ + trackEvent, + getMetricsState, +}) { + return function rpcMethodTrackingMiddleware( + /** @type {any} */ req, + /** @type {any} */ res, + /** @type {Function} */ next, + ) { + const startTime = Date.now(); + const { origin } = req; + + next((callback) => { + const endTime = Date.now(); + if (!getMetricsState().participateInMetaMetrics) { + return callback(); + } + if (USER_PROMPTED_EVENT_NAME_MAP[req.method]) { + const userRejected = res.error?.code === 4001; + trackEvent({ + event: USER_PROMPTED_EVENT_NAME_MAP[req.method], + category: 'inpage_provider', + referrer: { + url: origin, + }, + properties: { + method: req.method, + status: userRejected ? 'rejected' : 'approved', + error_code: res.error?.code, + error_message: res.error?.message, + has_result: typeof res.result !== 'undefined', + duration: endTime - startTime, + }, + }); + } else if (typeof samplingTimeouts[req.method] === 'undefined') { + trackEvent({ + event: 'Provider Method Called', + category: 'inpage_provider', + referrer: { + url: origin, + }, + properties: { + method: req.method, + error_code: res.error?.code, + error_message: res.error?.message, + has_result: typeof res.result !== 'undefined', + duration: endTime - startTime, + }, + }); + // Only record one call to this method every ten seconds to avoid + // overloading network requests. + samplingTimeouts[req.method] = setTimeout(() => { + delete samplingTimeouts[req.method]; + }, SECOND * 10); + } + return callback(); + }); + }; +} diff --git a/app/scripts/metamask-controller.js b/app/scripts/metamask-controller.js index 9f5311d05..f615d0a51 100644 --- a/app/scripts/metamask-controller.js +++ b/app/scripts/metamask-controller.js @@ -137,6 +137,7 @@ import { buildSnapRestrictedMethodSpecifications, ///: END:ONLY_INCLUDE_IN } from './controllers/permissions'; +import createRPCMethodTrackingMiddleware from './lib/createRPCMethodTrackingMiddleware'; ///: BEGIN:ONLY_INCLUDE_IN(flask) import { getPlatform } from './lib/util'; @@ -3330,6 +3331,17 @@ export default class MetamaskController extends EventEmitter { engine.push(createLoggerMiddleware({ origin })); engine.push(this.permissionLogController.createMiddleware()); + engine.push( + createRPCMethodTrackingMiddleware({ + trackEvent: this.metaMetricsController.trackEvent.bind( + this.metaMetricsController, + ), + getMetricsState: this.metaMetricsController.store.getState.bind( + this.metaMetricsController.store, + ), + }), + ); + // onboarding if (subjectType === SUBJECT_TYPES.WEBSITE) { engine.push( diff --git a/shared/constants/metametrics.js b/shared/constants/metametrics.js index 6639fb250..ffcfc4012 100644 --- a/shared/constants/metametrics.js +++ b/shared/constants/metametrics.js @@ -213,3 +213,10 @@ export const METAMETRICS_BACKGROUND_PAGE_OBJECT = { export const REJECT_NOTFICIATION_CLOSE = 'Cancel Via Notification Close'; export const REJECT_NOTFICIATION_CLOSE_SIG = 'Cancel Sig Request Via Notification Close'; + +export const EVENT_NAMES = { + SIGNATURE_REQUESTED: 'Signature Requested', + ENCRYPTION_PUBLIC_KEY_REQUESTED: 'Encryption Public Key Requested', + DECRYPTION_REQUESTED: 'Decryption Requested', + PERMISSIONS_REQUESTED: 'Permissions Requested', +};