|
| 1 | +import { MESSAGE_TYPE, ORIGIN_METAMASK } from '../../../shared/constants/app'; |
1 | 2 | import { EVENT, EVENT_NAMES } from '../../../shared/constants/metametrics'; |
2 | 3 | import { SECOND } from '../../../shared/constants/time'; |
3 | 4 |
|
4 | | -const USER_PROMPTED_EVENT_NAME_MAP = { |
5 | | - eth_signTypedData_v4: EVENT_NAMES.SIGNATURE_REQUESTED, |
6 | | - eth_signTypedData_v3: EVENT_NAMES.SIGNATURE_REQUESTED, |
7 | | - eth_signTypedData: EVENT_NAMES.SIGNATURE_REQUESTED, |
8 | | - eth_personal_sign: EVENT_NAMES.SIGNATURE_REQUESTED, |
9 | | - eth_sign: EVENT_NAMES.SIGNATURE_REQUESTED, |
10 | | - eth_getEncryptionPublicKey: EVENT_NAMES.ENCRYPTION_PUBLIC_KEY_REQUESTED, |
11 | | - eth_decrypt: EVENT_NAMES.DECRYPTION_REQUESTED, |
12 | | - wallet_requestPermissions: EVENT_NAMES.PERMISSIONS_REQUESTED, |
13 | | - eth_requestAccounts: EVENT_NAMES.PERMISSIONS_REQUESTED, |
| 5 | +/** |
| 6 | + * These types determine how the method tracking middleware handles incoming |
| 7 | + * requests based on the method name. There are three options right now but |
| 8 | + * the types could be expanded to cover other options in the future. |
| 9 | + */ |
| 10 | +const RATE_LIMIT_TYPES = { |
| 11 | + RATE_LIMITED: 'rate_limited', |
| 12 | + BLOCKED: 'blocked', |
| 13 | + NON_RATE_LIMITED: 'non_rate_limited', |
14 | 14 | }; |
15 | 15 |
|
16 | | -const samplingTimeouts = {}; |
| 16 | +/** |
| 17 | + * This object maps a method name to a RATE_LIMIT_TYPE. If not in this map the |
| 18 | + * default is 'RATE_LIMITED' |
| 19 | + */ |
| 20 | +const RATE_LIMIT_MAP = { |
| 21 | + [MESSAGE_TYPE.ETH_SIGN]: RATE_LIMIT_TYPES.NON_RATE_LIMITED, |
| 22 | + [MESSAGE_TYPE.ETH_SIGN_TYPED_DATA]: RATE_LIMIT_TYPES.NON_RATE_LIMITED, |
| 23 | + [MESSAGE_TYPE.ETH_SIGN_TYPED_DATA_V3]: RATE_LIMIT_TYPES.NON_RATE_LIMITED, |
| 24 | + [MESSAGE_TYPE.ETH_SIGN_TYPED_DATA_V4]: RATE_LIMIT_TYPES.NON_RATE_LIMITED, |
| 25 | + [MESSAGE_TYPE.PERSONAL_SIGN]: RATE_LIMIT_TYPES.NON_RATE_LIMITED, |
| 26 | + [MESSAGE_TYPE.ETH_DECRYPT]: RATE_LIMIT_TYPES.NON_RATE_LIMITED, |
| 27 | + [MESSAGE_TYPE.ETH_GET_ENCRYPTION_PUBLIC_KEY]: |
| 28 | + RATE_LIMIT_TYPES.NON_RATE_LIMITED, |
| 29 | + [MESSAGE_TYPE.ETH_REQUEST_ACCOUNTS]: RATE_LIMIT_TYPES.RATE_LIMITED, |
| 30 | + [MESSAGE_TYPE.WALLET_REQUEST_PERMISSIONS]: RATE_LIMIT_TYPES.RATE_LIMITED, |
| 31 | + [MESSAGE_TYPE.SEND_METADATA]: RATE_LIMIT_TYPES.BLOCKED, |
| 32 | + [MESSAGE_TYPE.GET_PROVIDER_STATE]: RATE_LIMIT_TYPES.BLOCKED, |
| 33 | +}; |
| 34 | + |
| 35 | +/** |
| 36 | + * For events with user interaction (approve / reject | cancel) this map will |
| 37 | + * return an object with APPROVED, REJECTED and REQUESTED keys that map to the |
| 38 | + * appropriate event names. |
| 39 | + */ |
| 40 | +const EVENT_NAME_MAP = { |
| 41 | + [MESSAGE_TYPE.ETH_SIGN]: { |
| 42 | + APPROVED: EVENT_NAMES.SIGNATURE_APPROVED, |
| 43 | + REJECTED: EVENT_NAMES.SIGNATURE_REJECTED, |
| 44 | + REQUESTED: EVENT_NAMES.SIGNATURE_REQUESTED, |
| 45 | + }, |
| 46 | + [MESSAGE_TYPE.ETH_SIGN_TYPED_DATA]: { |
| 47 | + APPROVED: EVENT_NAMES.SIGNATURE_APPROVED, |
| 48 | + REJECTED: EVENT_NAMES.SIGNATURE_REJECTED, |
| 49 | + REQUESTED: EVENT_NAMES.SIGNATURE_REQUESTED, |
| 50 | + }, |
| 51 | + [MESSAGE_TYPE.ETH_SIGN_TYPED_DATA_V3]: { |
| 52 | + APPROVED: EVENT_NAMES.SIGNATURE_APPROVED, |
| 53 | + REJECTED: EVENT_NAMES.SIGNATURE_REJECTED, |
| 54 | + REQUESTED: EVENT_NAMES.SIGNATURE_REQUESTED, |
| 55 | + }, |
| 56 | + [MESSAGE_TYPE.ETH_SIGN_TYPED_DATA_V4]: { |
| 57 | + APPROVED: EVENT_NAMES.SIGNATURE_APPROVED, |
| 58 | + REJECTED: EVENT_NAMES.SIGNATURE_REJECTED, |
| 59 | + REQUESTED: EVENT_NAMES.SIGNATURE_REQUESTED, |
| 60 | + }, |
| 61 | + [MESSAGE_TYPE.PERSONAL_SIGN]: { |
| 62 | + APPROVED: EVENT_NAMES.SIGNATURE_APPROVED, |
| 63 | + REJECTED: EVENT_NAMES.SIGNATURE_REJECTED, |
| 64 | + REQUESTED: EVENT_NAMES.SIGNATURE_REQUESTED, |
| 65 | + }, |
| 66 | + [MESSAGE_TYPE.ETH_DECRYPT]: { |
| 67 | + APPROVED: EVENT_NAMES.DECRYPTION_APPROVED, |
| 68 | + REJECTED: EVENT_NAMES.DECRYPTION_REJECTED, |
| 69 | + REQUESTED: EVENT_NAMES.DECRYPTION_REQUESTED, |
| 70 | + }, |
| 71 | + [MESSAGE_TYPE.ETH_GET_ENCRYPTION_PUBLIC_KEY]: { |
| 72 | + APPROVED: EVENT_NAMES.ENCRYPTION_PUBLIC_KEY_APPROVED, |
| 73 | + REJECTED: EVENT_NAMES.ENCRYPTION_PUBLIC_KEY_REJECTED, |
| 74 | + REQUESTED: EVENT_NAMES.ENCRYPTION_PUBLIC_KEY_REQUESTED, |
| 75 | + }, |
| 76 | + [MESSAGE_TYPE.ETH_REQUEST_ACCOUNTS]: { |
| 77 | + APPROVED: EVENT_NAMES.PERMISSIONS_APPROVED, |
| 78 | + REJECTED: EVENT_NAMES.PERMISSIONS_REJECTED, |
| 79 | + REQUESTED: EVENT_NAMES.PERMISSIONS_REQUESTED, |
| 80 | + }, |
| 81 | + [MESSAGE_TYPE.WALLET_REQUEST_PERMISSIONS]: { |
| 82 | + APPROVED: EVENT_NAMES.PERMISSIONS_APPROVED, |
| 83 | + REJECTED: EVENT_NAMES.PERMISSIONS_REJECTED, |
| 84 | + REQUESTED: EVENT_NAMES.PERMISSIONS_REQUESTED, |
| 85 | + }, |
| 86 | +}; |
| 87 | + |
| 88 | +const rateLimitTimeouts = {}; |
17 | 89 |
|
18 | 90 | /** |
19 | 91 | * Returns a middleware that tracks inpage_provider usage using sampling for |
20 | 92 | * each type of event except those that require user interaction, such as |
21 | 93 | * signature requests |
22 | 94 | * |
23 | 95 | * @param {object} opts - options for the rpc method tracking middleware |
24 | | - * @param {Function} opts.trackEvent - trackEvent method from MetaMetricsController |
25 | | - * @param {Function} opts.getMetricsState - get the state of MetaMetricsController |
| 96 | + * @param {Function} opts.trackEvent - trackEvent method from |
| 97 | + * MetaMetricsController |
| 98 | + * @param {Function} opts.getMetricsState - get the state of |
| 99 | + * MetaMetricsController |
| 100 | + * @param {number} [opts.rateLimitSeconds] - number of seconds to wait before |
| 101 | + * allowing another set of events to be tracked. |
26 | 102 | * @returns {Function} |
27 | 103 | */ |
28 | 104 | export default function createRPCMethodTrackingMiddleware({ |
29 | 105 | trackEvent, |
30 | 106 | getMetricsState, |
| 107 | + rateLimitSeconds = 60, |
31 | 108 | }) { |
32 | 109 | return function rpcMethodTrackingMiddleware( |
33 | 110 | /** @type {any} */ req, |
34 | 111 | /** @type {any} */ res, |
35 | 112 | /** @type {Function} */ next, |
36 | 113 | ) { |
37 | | - const startTime = Date.now(); |
38 | | - const { origin } = req; |
| 114 | + const { origin, method } = req; |
| 115 | + |
| 116 | + // Determine what type of rate limit to apply based on method |
| 117 | + const rateLimitType = |
| 118 | + RATE_LIMIT_MAP[method] ?? RATE_LIMIT_TYPES.RATE_LIMITED; |
| 119 | + |
| 120 | + // If the rateLimitType is RATE_LIMITED check the rateLimitTimeouts |
| 121 | + const rateLimited = |
| 122 | + rateLimitType === RATE_LIMIT_TYPES.RATE_LIMITED && |
| 123 | + typeof rateLimitTimeouts[method] !== 'undefined'; |
| 124 | + |
| 125 | + // Get the participateInMetaMetrics state to determine if we should track |
| 126 | + // anything. This is extra redundancy because this value is checked in |
| 127 | + // the metametrics controller's trackEvent method as well. |
| 128 | + const userParticipatingInMetaMetrics = |
| 129 | + getMetricsState().participateInMetaMetrics === true; |
| 130 | + |
| 131 | + // Get the event type, each of which has APPROVED, REJECTED and REQUESTED |
| 132 | + // keys for the various events in the flow. |
| 133 | + const eventType = EVENT_NAME_MAP[method]; |
| 134 | + |
| 135 | + // Boolean variable that reduces code duplication and increases legibility |
| 136 | + const shouldTrackEvent = |
| 137 | + // Don't track if the request came from our own UI or background |
| 138 | + origin !== ORIGIN_METAMASK && |
| 139 | + // Don't track if this is a blocked method |
| 140 | + rateLimitType !== RATE_LIMIT_TYPES.BLOCKED && |
| 141 | + // Don't track if the rate limit has been hit |
| 142 | + rateLimited === false && |
| 143 | + // Don't track if the user isn't participating in metametrics |
| 144 | + userParticipatingInMetaMetrics === true; |
| 145 | + |
| 146 | + if (shouldTrackEvent) { |
| 147 | + // We track an initial "requested" event as soon as the dapp calls the |
| 148 | + // provider method. For the events not special cased this is the only |
| 149 | + // event that will be fired and the event name will be |
| 150 | + // 'Provider Method Called'. |
| 151 | + const event = eventType |
| 152 | + ? eventType.REQUESTED |
| 153 | + : EVENT_NAMES.PROVIDER_METHOD_CALLED; |
| 154 | + |
| 155 | + const properties = {}; |
| 156 | + |
| 157 | + if (event === EVENT_NAMES.SIGNATURE_REQUESTED) { |
| 158 | + properties.signature_type = method; |
| 159 | + } else { |
| 160 | + properties.method = method; |
| 161 | + } |
| 162 | + |
| 163 | + trackEvent({ |
| 164 | + event, |
| 165 | + category: EVENT.CATEGORIES.INPAGE_PROVIDER, |
| 166 | + referrer: { |
| 167 | + url: origin, |
| 168 | + }, |
| 169 | + properties, |
| 170 | + }); |
| 171 | + |
| 172 | + rateLimitTimeouts[method] = setTimeout(() => { |
| 173 | + delete rateLimitTimeouts[method]; |
| 174 | + }, SECOND * rateLimitSeconds); |
| 175 | + } |
39 | 176 |
|
40 | 177 | next((callback) => { |
41 | | - const endTime = Date.now(); |
42 | | - if (!getMetricsState().participateInMetaMetrics) { |
| 178 | + if (shouldTrackEvent === false || typeof eventType === 'undefined') { |
43 | 179 | return callback(); |
44 | 180 | } |
45 | | - if (USER_PROMPTED_EVENT_NAME_MAP[req.method]) { |
46 | | - const userRejected = res.error?.code === 4001; |
47 | | - trackEvent({ |
48 | | - event: USER_PROMPTED_EVENT_NAME_MAP[req.method], |
49 | | - category: EVENT.CATEGORIES.INPAGE_PROVIDER, |
50 | | - referrer: { |
51 | | - url: origin, |
52 | | - }, |
53 | | - properties: { |
54 | | - method: req.method, |
55 | | - status: userRejected ? 'rejected' : 'approved', |
56 | | - error_code: res.error?.code, |
57 | | - error_message: res.error?.message, |
58 | | - has_result: typeof res.result !== 'undefined', |
59 | | - duration: endTime - startTime, |
60 | | - }, |
61 | | - }); |
62 | | - } else if (typeof samplingTimeouts[req.method] === 'undefined') { |
63 | | - trackEvent({ |
64 | | - event: 'Provider Method Called', |
65 | | - category: EVENT.CATEGORIES.INPAGE_PROVIDER, |
66 | | - referrer: { |
67 | | - url: origin, |
68 | | - }, |
69 | | - properties: { |
70 | | - method: req.method, |
71 | | - error_code: res.error?.code, |
72 | | - error_message: res.error?.message, |
73 | | - has_result: typeof res.result !== 'undefined', |
74 | | - duration: endTime - startTime, |
75 | | - }, |
76 | | - }); |
77 | | - // Only record one call to this method every ten seconds to avoid |
78 | | - // overloading network requests. |
79 | | - samplingTimeouts[req.method] = setTimeout(() => { |
80 | | - delete samplingTimeouts[req.method]; |
81 | | - }, SECOND * 10); |
| 181 | + |
| 182 | + // An error code of 4001 means the user rejected the request, which we |
| 183 | + // can use here to determine which event to track. |
| 184 | + const event = |
| 185 | + res.error?.code === 4001 ? eventType.REJECTED : eventType.APPROVED; |
| 186 | + |
| 187 | + const properties = {}; |
| 188 | + |
| 189 | + if (eventType.REQUESTED === EVENT_NAMES.SIGNATURE_REQUESTED) { |
| 190 | + properties.signature_type = method; |
| 191 | + } else { |
| 192 | + properties.method = method; |
82 | 193 | } |
| 194 | + |
| 195 | + trackEvent({ |
| 196 | + event, |
| 197 | + category: EVENT.CATEGORIES.INPAGE_PROVIDER, |
| 198 | + referrer: { |
| 199 | + url: origin, |
| 200 | + }, |
| 201 | + properties, |
| 202 | + }); |
83 | 203 | return callback(); |
84 | 204 | }); |
85 | 205 | }; |
|
0 commit comments