From 6426816411a1d4b33262dbda24c24d1184ef30bc Mon Sep 17 00:00:00 2001 From: Erik Marks <25517051+rekmarks@users.noreply.github.com> Date: Thu, 5 Nov 2020 14:55:28 -0800 Subject: [PATCH] Log web3 usage for functions and nested properties only (#9797) * Log web3 usage for functions and nested properties only * Change web3 metrics source to legacy * Update web3 metrics properties and event name Co-authored-by: Mark Stacey --- .../handlers/log-web3-usage.js | 11 +- app/scripts/lib/setupWeb3.js | 143 +++++++++++++++--- app/scripts/lib/web3-entities.json | 101 +++++++++++++ 3 files changed, 228 insertions(+), 27 deletions(-) create mode 100644 app/scripts/lib/web3-entities.json diff --git a/app/scripts/lib/rpc-method-middleware/handlers/log-web3-usage.js b/app/scripts/lib/rpc-method-middleware/handlers/log-web3-usage.js index 003e6fa8a..dbff526bb 100644 --- a/app/scripts/lib/rpc-method-middleware/handlers/log-web3-usage.js +++ b/app/scripts/lib/rpc-method-middleware/handlers/log-web3-usage.js @@ -35,18 +35,19 @@ const recordedWeb3Usage = {} * @param {LogWeb3UsageOptions} options */ function logWeb3UsageHandler(req, res, _next, end, { origin, sendMetrics }) { - const { action, name } = req.params[0] + const { action, path } = req.params[0] if (!recordedWeb3Usage[origin]) { recordedWeb3Usage[origin] = {} } - if (!recordedWeb3Usage[origin][name]) { - recordedWeb3Usage[origin][name] = true + if (!recordedWeb3Usage[origin][path]) { + recordedWeb3Usage[origin][path] = true sendMetrics({ - event: `Website Accessed window.web3`, + matomo: true, + event: `Website Used window.web3`, category: 'inpage_provider', - properties: { action, web3Property: name }, + properties: { action, web3Path: path }, eventContext: { referrer: { url: origin, diff --git a/app/scripts/lib/setupWeb3.js b/app/scripts/lib/setupWeb3.js index 248ca3fd1..922d29cf5 100644 --- a/app/scripts/lib/setupWeb3.js +++ b/app/scripts/lib/setupWeb3.js @@ -3,6 +3,7 @@ // TODO:deprecate:2020 // Delete this file +import web3Entitites from './web3-entities.json' import 'web3/dist/web3.min' const shouldLogUsage = ![ @@ -11,6 +12,10 @@ const shouldLogUsage = ![ 'metamask.io', ].includes(window.location.hostname) +/** + * To understand how we arrived at this implementation, please see: + * https://github.com/ethereum/web3.js/blob/0.20.7/DOCUMENTATION.md + */ export default function setupWeb3(log) { // export web3 as a global, checking for usage let reloadInProgress = false @@ -31,8 +36,122 @@ export default function setupWeb3(log) { value: web3.eth, }) + // Setup logging of nested property usage + if (shouldLogUsage) { + // web3 namespaces with common and uncommon dapp actions + const includedTopKeys = [ + 'eth', + 'db', + 'shh', + 'net', + 'personal', + 'bzz', + 'version', + ] + + // For each top-level property, create appropriate Proxy traps for all of + // their properties + includedTopKeys.forEach((topKey) => { + const applyTrapKeys = new Map() + const getTrapKeys = new Map() + + Object.keys(web3[topKey]).forEach((key) => { + const path = `web3.${topKey}.${key}` + + if (web3Entitites[path]) { + if (web3Entitites[path] === 'function') { + applyTrapKeys.set(key, path) + } else { + getTrapKeys.set(key, path) + } + } + }) + + // Create apply traps for function properties + for (const [key, path] of applyTrapKeys) { + web3[topKey][key] = new Proxy(web3[topKey][key], { + apply: (...params) => { + window.ethereum.request({ + method: 'metamask_logInjectedWeb3Usage', + params: [ + { + action: 'apply', + path, + }, + ], + }) + + // Call function normally + return Reflect.apply(...params) + }, + }) + } + + // Create get trap for non-function properties + web3[topKey] = new Proxy(web3[topKey], { + get: (web3Prop, key, ...params) => { + const name = stringifyKey(key) + + if (getTrapKeys.has(name)) { + window.ethereum.request({ + method: 'metamask_logInjectedWeb3Usage', + params: [ + { + action: 'get', + path: getTrapKeys.get(name), + }, + ], + }) + } + + // return value normally + return Reflect.get(web3Prop, key, ...params) + }, + }) + }) + } + + const topLevelFunctions = [ + 'isConnected', + 'setProvider', + 'reset', + 'sha3', + 'toHex', + 'toAscii', + 'fromAscii', + 'toDecimal', + 'fromDecimal', + 'fromWei', + 'toWei', + 'toBigNumber', + 'isAddress', + ] + + // apply-trap top-level functions + topLevelFunctions.forEach((key) => { + // This type check is probably redundant, but we've been burned before. + if (typeof web3[key] === 'function') { + web3[key] = new Proxy(web3[key], { + apply: (...params) => { + window.ethereum.request({ + method: 'metamask_logInjectedWeb3Usage', + params: [ + { + action: 'apply', + path: `web3.${key}`, + }, + ], + }) + + // Call function normally + return Reflect.apply(...params) + }, + }) + } + }) + const web3Proxy = new Proxy(web3, { - get: (_web3, key) => { + get: (...params) => { // get the time of use lastTimeUsed = Date.now() @@ -44,28 +163,8 @@ export default function setupWeb3(log) { hasBeenWarned = true } - if (shouldLogUsage) { - const name = stringifyKey(key) - window.ethereum.request({ - method: 'metamask_logInjectedWeb3Usage', - params: [{ action: 'get', name }], - }) - } - // return value normally - return _web3[key] - }, - set: (_web3, key, value) => { - const name = stringifyKey(key) - if (shouldLogUsage) { - window.ethereum.request({ - method: 'metamask_logInjectedWeb3Usage', - params: [{ action: 'set', name }], - }) - } - - // set value normally - _web3[key] = value + return Reflect.get(...params) }, }) diff --git a/app/scripts/lib/web3-entities.json b/app/scripts/lib/web3-entities.json new file mode 100644 index 000000000..0e9b435bc --- /dev/null +++ b/app/scripts/lib/web3-entities.json @@ -0,0 +1,101 @@ +{ + "web3.bzz.blockNetworkRead": "function", + "web3.bzz.download": "function", + "web3.bzz.get": "function", + "web3.bzz.getHive": "function", + "web3.bzz.getInfo": "function", + "web3.bzz.hive": "TRAP", + "web3.bzz.info": "TRAP", + "web3.bzz.modify": "function", + "web3.bzz.put": "function", + "web3.bzz.retrieve": "function", + "web3.bzz.store": "function", + "web3.bzz.swapEnabled": "function", + "web3.bzz.syncEnabled": "function", + "web3.bzz.upload": "function", + "web3.db.getHex": "function", + "web3.db.getString": "function", + "web3.db.putHex": "function", + "web3.db.putString": "function", + "web3.eth.accounts": "object", + "web3.eth.blockNumber": "TRAP", + "web3.eth.call": "function", + "web3.eth.coinbase": "object", + "web3.eth.compile": "object", + "web3.eth.estimateGas": "function", + "web3.eth.gasPrice": "TRAP", + "web3.eth.getAccounts": "function", + "web3.eth.getBalance": "function", + "web3.eth.getBlock": "function", + "web3.eth.getBlockNumber": "function", + "web3.eth.getBlockTransactionCount": "function", + "web3.eth.getBlockUncleCount": "function", + "web3.eth.getCode": "function", + "web3.eth.getCoinbase": "function", + "web3.eth.getCompilers": "function", + "web3.eth.getGasPrice": "function", + "web3.eth.getHashrate": "function", + "web3.eth.getMining": "function", + "web3.eth.getProtocolVersion": "function", + "web3.eth.getStorageAt": "function", + "web3.eth.getSyncing": "function", + "web3.eth.getTransaction": "function", + "web3.eth.getTransactionCount": "function", + "web3.eth.getTransactionFromBlock": "function", + "web3.eth.getTransactionReceipt": "function", + "web3.eth.getUncle": "function", + "web3.eth.getWork": "function", + "web3.eth.hashrate": "TRAP", + "web3.eth.iban": "function", + "web3.eth.mining": "TRAP", + "web3.eth.protocolVersion": "TRAP", + "web3.eth.sendIBANTransaction": "function", + "web3.eth.sendRawTransaction": "function", + "web3.eth.sendTransaction": "function", + "web3.eth.sign": "function", + "web3.eth.signTransaction": "function", + "web3.eth.submitWork": "function", + "web3.eth.syncing": "TRAP", + "web3.net.getListening": "function", + "web3.net.getPeerCount": "function", + "web3.net.listening": "TRAP", + "web3.net.peerCount": "TRAP", + "web3.personal.ecRecover": "function", + "web3.personal.getListAccounts": "function", + "web3.personal.importRawKey": "function", + "web3.personal.listAccounts": "TRAP", + "web3.personal.lockAccount": "function", + "web3.personal.newAccount": "function", + "web3.personal.sendTransaction": "function", + "web3.personal.sign": "function", + "web3.personal.unlockAccount": "function", + "web3.providers.HttpProvider": "function", + "web3.providers.IpcProvider": "function", + "web3.shh.addPrivateKey": "function", + "web3.shh.addSymKey": "function", + "web3.shh.deleteKeyPair": "function", + "web3.shh.deleteSymKey": "function", + "web3.shh.generateSymKeyFromPassword": "function", + "web3.shh.getPrivateKey": "function", + "web3.shh.getPublicKey": "function", + "web3.shh.getSymKey": "function", + "web3.shh.hasKeyPair": "function", + "web3.shh.hasSymKey": "function", + "web3.shh.info": "function", + "web3.shh.markTrustedPeer": "function", + "web3.shh.newKeyPair": "function", + "web3.shh.newSymKey": "function", + "web3.shh.post": "function", + "web3.shh.setMaxMessageSize": "function", + "web3.shh.setMinPoW": "function", + "web3.shh.version": "function", + "web3.version.api": "string", + "web3.version.ethereum": "TRAP", + "web3.version.getEthereum": "function", + "web3.version.getNetwork": "function", + "web3.version.getNode": "function", + "web3.version.getWhisper": "function", + "web3.version.network": "string", + "web3.version.node": "TRAP", + "web3.version.whisper": "TRAP" +}