diff --git a/commands/create-account.js b/commands/create-account.js index ccafbdd8..839e9526 100644 --- a/commands/create-account.js +++ b/commands/create-account.js @@ -2,6 +2,7 @@ const exitOnError = require('../utils/exit-on-error'); const connect = require('../utils/connect'); const { KeyPair } = require('near-api-js'); +const eventtracking = require('../utils/eventtracking'); module.exports = { command: 'create_account ', @@ -31,6 +32,7 @@ module.exports = { }; async function createAccount(options) { + await eventtracking.track(eventtracking.EVENT_ID_CREATE_ACCOUNT_START, {}); // NOTE: initialBalance is passed as part of config here, parsed in middleware/initial-balance let near = await connect(options); let keyPair; @@ -46,4 +48,5 @@ async function createAccount(options) { await near.connection.signer.keyStore.setKey(options.networkId, options.accountId, keyPair); } console.log(`Account ${options.accountId} for network "${options.networkId}" was created.`); + await eventtracking.track(eventtracking.EVENT_ID_CREATE_ACCOUNT_SUCCESS, {}); } diff --git a/commands/tx-status.js b/commands/tx-status.js index f36730c5..49eae5fa 100644 --- a/commands/tx-status.js +++ b/commands/tx-status.js @@ -2,6 +2,7 @@ const exitOnError = require('../utils/exit-on-error'); const connect = require('../utils/connect'); const inspectResponse = require('../utils/inspect-response'); const bs58 = require('bs58'); +const eventtracking = require('../utils/eventtracking'); module.exports = { command: 'tx-status ', @@ -13,6 +14,7 @@ module.exports = { required: true }), handler: exitOnError(async (argv) => { + await eventtracking.track(eventtracking.EVENT_ID_TX_STATUS_START, {}); const near = await connect(argv); const hashParts = argv.hash.split(':'); @@ -33,5 +35,6 @@ module.exports = { const status = await near.connection.provider.txStatus(bs58.decode(hash), accountId); console.log(`Transaction ${accountId}:${hash}`); console.log(inspectResponse(status)); + await eventtracking.track(eventtracking.EVENT_ID_TX_STATUS_SUCCESS, {}); }) }; diff --git a/index.js b/index.js index a785ca52..4779925d 100644 --- a/index.js +++ b/index.js @@ -14,6 +14,7 @@ const verify = require('./utils/verify-account'); const capture = require('./utils/capture-login-success'); const inspectResponse = require('./utils/inspect-response'); +const eventtracking = require('./utils/eventtracking'); // TODO: Fix promisified wrappers to handle error properly @@ -100,7 +101,6 @@ exports.login = async function(options) { input: process.stdin, output: process.stdout }); - const getAccountFromConsole = async () => { return await new Promise((resolve) => { rl.question( @@ -128,7 +128,6 @@ exports.login = async function(options) { } rl.close(); capture.cancel(); - // verify the accountId if we captured it or ... try { await verify(accountId, keyPair, options); @@ -139,6 +138,7 @@ exports.login = async function(options) { }; exports.viewAccount = async function(options) { + await eventtracking.track(eventtracking.EVENT_ID_ACCOUNT_STATE_START, {}); let near = await connect(options); let account = await near.account(options.accountId); let state = await account.state(); @@ -147,23 +147,28 @@ exports.viewAccount = async function(options) { } console.log(`Account ${options.accountId}`); console.log(inspectResponse(state)); + await eventtracking.track(eventtracking.EVENT_ID_ACCOUNT_STATE_SUCCESS, {}); }; exports.deleteAccount = async function(options) { + await eventtracking.track(eventtracking.EVENT_ID_DELETE_ACCOUNT_START, {}); console.log( `Deleting account. Account id: ${options.accountId}, node: ${options.nodeUrl}, helper: ${options.helperUrl}, beneficiary: ${options.beneficiaryId}`); const near = await connect(options); const account = await near.account(options.accountId); await account.deleteAccount(options.beneficiaryId); console.log(`Account ${options.accountId} for network "${options.networkId}" was deleted.`); + await eventtracking.track(eventtracking.EVENT_ID_DELETE_ACCOUNT_SUCCESS, {}); }; exports.keys = async function(options) { + await eventtracking.track(eventtracking.EVENT_ID_ACCOUNT_KEYS_START, {}); let near = await connect(options); let account = await near.account(options.accountId); let accessKeys = await account.getAccessKeys(); console.log(`Keys for account ${options.accountId}`); console.log(inspectResponse(accessKeys)); + await eventtracking.track(eventtracking.EVENT_ID_ACCOUNT_KEYS_SUCCESS, {}); }; exports.sendMoney = async function(options) { diff --git a/package.json b/package.json index 613bd51c..34fbb714 100644 --- a/package.json +++ b/package.json @@ -37,6 +37,7 @@ "flagged-respawn": "^1.0.1", "is-ci": "^2.0.0", "jest-environment-node": "^25.1.0", + "mixpanel": "^0.11.0", "ncp": "^2.0.0", "near-api-js": "^0.23.1", "open": "^7.0.1", @@ -44,6 +45,7 @@ "stoppable": "^1.1.0", "tcp-port-used": "^1.0.1", "update-notifier": "^4.0.0", + "uuid": "^7.0.3", "v8flags": "^3.1.3", "yargs": "^15.0.1" }, diff --git a/test/index.sh b/test/index.sh index f8655e33..0586909f 100755 --- a/test/index.sh +++ b/test/index.sh @@ -1,6 +1,8 @@ #!/bin/bash export NODE_ENV=${NODE_ENV:-test} OVERALL_RESULT=0 +mkdir ~/.near-config +echo '{"trackingEnabled":false}' > ~/.near-config/settings.json for test in ./test/test_*; do echo "" echo "Running $test" diff --git a/utils/eventtracking.js b/utils/eventtracking.js new file mode 100644 index 00000000..98ce3fa3 --- /dev/null +++ b/utils/eventtracking.js @@ -0,0 +1,85 @@ +const MIXPANEL_TOKEN = '9aa8926fbcb03eb5d6ce787b5e8fa6eb'; +var mixpanel = require('mixpanel').init(MIXPANEL_TOKEN); + +const uuid = require('uuid'); +const chalk = require('chalk'); // colorize output +const readline = require('readline'); +const settings = require('./settings'); + +const TRACKING_ENABLED_KEY = 'trackingEnabled'; +const TRACKING_SESSION_ID_KEY = 'trackingSessionId'; + +const track = async (eventType, eventProperties) => { + const shellSettings = settings.getShellSettings(); + // if the appropriate option is not in settings, ask now and save settings. + if (!(TRACKING_ENABLED_KEY in shellSettings)) { + const rl = readline.createInterface({ + input: process.stdin, + output: process.stdout + }); + const getEventTrackingConsent = async () => { + for (var attempts = 0; attempts < 10; attempts++) { + const answer = await new Promise((resolve) => { + + rl.question( + chalk`We would like to collect data on near-shell usage to improve developer experience.` + + chalk ` We will never send private information. We only collect which commands are run via an anonymous identifier.` + + chalk`{bold.yellow Would you like to opt in (y/n)?}`, + async (consentToEventTracking) => { + if (consentToEventTracking == 'y' || consentToEventTracking == 'Y') { + resolve(true); + } + else if (consentToEventTracking == 'n' || consentToEventTracking == 'N') { + resolve(false); + } + resolve(undefined); + }); + }); + if (answer !== undefined) { + return answer; + } + } + return false; // If they can't figure it out in this many attempts, just opt out + }; + + shellSettings[TRACKING_ENABLED_KEY] = await getEventTrackingConsent(); + shellSettings[TRACKING_SESSION_ID_KEY] = shellSettings[TRACKING_ENABLED_KEY] ? uuid.v4() : undefined; + rl.close(); + settings.saveShellSettings(shellSettings); + } + + if (!shellSettings[TRACKING_ENABLED_KEY]) { + return; + } + + try { + const mixPanelProperties = { + distinct_id: shellSettings[TRACKING_SESSION_ID_KEY] + }; + Object.assign(mixPanelProperties, eventProperties); + mixpanel.track(eventType, mixPanelProperties); + } + catch (e) { + console.log('Warning: problem while sending developer event tracking data. This is not critical. Error: ', e); + } +}; + +module.exports = { + track, + + // Event ids used in mixpanel. Note that we want to mention shell to make it very easy to tell that an event came from shell, + // since mixpanel might be used for other components as well. + EVENT_ID_ACCOUNT_STATE_START: 'shell_account_state_start', + EVENT_ID_ACCOUNT_STATE_SUCCESS: 'shell_account_state_success', + EVENT_ID_DELETE_ACCOUNT_START: 'shell_delete_account_start', + EVENT_ID_DELETE_ACCOUNT_SUCCESS: 'shell_delete_account_success', + EVENT_ID_ACCOUNT_KEYS_START: 'shell_account_keys_start', + EVENT_ID_ACCOUNT_KEYS_SUCCESS: 'shell_account_keys_success', + EVENT_ID_TX_STATUS_START: 'shell_tx_status_start', + EVENT_ID_TX_STATUS_SUCCESS: 'shell_tx_status_success', + EVENT_ID_LOGIN: 'shell_login', + EVENT_ID_DEPLOY: 'shell_deploy', + EVENT_ID_DEV_DEPLOY: 'shell_dev_deploy', + EVENT_ID_CREATE_ACCOUNT_START: 'shell_create_account_start', + EVENT_ID_CREATE_ACCOUNT_SUCCESS: 'shell_create_account_success' +}; \ No newline at end of file diff --git a/utils/settings.js b/utils/settings.js new file mode 100644 index 00000000..0189c033 --- /dev/null +++ b/utils/settings.js @@ -0,0 +1,44 @@ +const fs = require('fs'); +const homedir = require('os').homedir(); +const path = require('path'); + + +// Persistent shell settings +const SETTINGS_FILE_NAME = 'settings.json'; +const SETTINGS_DIR = '.near-config'; + +const getShellSettings = () => { + const nearPath = path.join(homedir, SETTINGS_DIR); + try { + if (!fs.existsSync(nearPath)) { + fs.mkdirSync(nearPath); + } + const shellSettingsPath = path.join(nearPath, SETTINGS_FILE_NAME); + if (!fs.existsSync(shellSettingsPath)) { + return {}; + } else { + return JSON.parse(fs.readFileSync(shellSettingsPath, 'utf8')); + } + } catch (e) { + console.log(e); + } + return {}; +}; + +const saveShellSettings = (settings) => { + const nearPath = path.join(homedir, SETTINGS_DIR); + try { + if (!fs.existsSync(nearPath)) { + fs.mkdirSync(nearPath); + } + const shellSettingsPath = path.join(nearPath, SETTINGS_FILE_NAME); + fs.writeFileSync(shellSettingsPath, JSON.stringify(settings)); + } catch (e) { + console.log(e); + } +}; + +module.exports = { + getShellSettings, + saveShellSettings +}; \ No newline at end of file diff --git a/yarn.lock b/yarn.lock index 4a53f237..88758424 100644 --- a/yarn.lock +++ b/yarn.lock @@ -580,6 +580,13 @@ acorn@^7.1.0, acorn@^7.1.1: resolved "https://registry.yarnpkg.com/acorn/-/acorn-7.1.1.tgz#e35668de0b402f359de515c5482a1ab9f89a69bf" integrity sha512-add7dgA5ppRPxCFJoAGfMDi7PIBXq1RtGo7BhbLaxwrXPOmw8gq48Y9ozT01hUKy9byMjlR20EJhu5zlkErEkg== +agent-base@^4.3.0: + version "4.3.0" + resolved "https://registry.yarnpkg.com/agent-base/-/agent-base-4.3.0.tgz#8165f01c436009bccad0b1d122f05ed770efc6ee" + integrity sha512-salcGninV0nPrwpGNn4VTXBb1SOuXQBiqbrNXoeizJsHrsL6ERFM2Ne3JUSBWRE6aeNJI2ROP/WEEIDUiDe3cg== + dependencies: + es6-promisify "^5.0.0" + ajv@^6.10.0, ajv@^6.10.2, ajv@^6.5.5: version "6.12.0" resolved "https://registry.yarnpkg.com/ajv/-/ajv-6.12.0.tgz#06d60b96d87b8454a5adaba86e7854da629db4b7" @@ -1215,6 +1222,13 @@ debug@^2.2.0, debug@^2.3.3: dependencies: ms "2.0.0" +debug@^3.1.0: + version "3.2.6" + resolved "https://registry.yarnpkg.com/debug/-/debug-3.2.6.tgz#e83d17de16d8a7efb7717edbe5fb10135eee629b" + integrity sha512-mel+jf7nrtEl5Pn1Qx46zARXKDpBbvzezse7p7LqINmdoIk8PYP5SySaxEmYv6TZ0JyEKA1hsCId6DIhgITtWQ== + dependencies: + ms "^2.1.1" + debug@^4.0.1, debug@^4.1.0, debug@^4.1.1: version "4.1.1" resolved "https://registry.yarnpkg.com/debug/-/debug-4.1.1.tgz#3b72260255109c6b589cee050f1d516139664791" @@ -1368,6 +1382,18 @@ error-polyfill@^0.1.2: o3 "^1.0.3" u3 "^0.1.0" +es6-promise@^4.0.3: + version "4.2.8" + resolved "https://registry.yarnpkg.com/es6-promise/-/es6-promise-4.2.8.tgz#4eb21594c972bc40553d276e510539143db53e0a" + integrity sha512-HJDGx5daxeIvxdBxvG2cb9g4tEvwIk3i8+nhX0yGrYmZUzbkdg8QbDevheDB8gd0//uPj4c1EQua8Q+MViT0/w== + +es6-promisify@^5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/es6-promisify/-/es6-promisify-5.0.0.tgz#5109d62f3e56ea967c4b63505aef08291c8a5203" + integrity sha1-UQnWLz5W6pZ8S2NQWu8IKRyKUgM= + dependencies: + es6-promise "^4.0.3" + escape-goat@^2.0.0: version "2.1.1" resolved "https://registry.yarnpkg.com/escape-goat/-/escape-goat-2.1.1.tgz#1b2dc77003676c457ec760b2dc68edb648188675" @@ -1956,6 +1982,14 @@ http-signature@~1.2.0: jsprim "^1.2.2" sshpk "^1.7.0" +https-proxy-agent@3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/https-proxy-agent/-/https-proxy-agent-3.0.0.tgz#0106efa5d63d6d6f3ab87c999fa4877a3fd1ff97" + integrity sha512-y4jAxNEihqvBI5F3SaO2rtsjIOnnNA8sEbuiP+UhJZJHeM2NRm6c09ax2tgqme+SgUUvjao2fJXF4h3D6Cb2HQ== + dependencies: + agent-base "^4.3.0" + debug "^3.1.0" + human-signals@^1.1.1: version "1.1.1" resolved "https://registry.yarnpkg.com/human-signals/-/human-signals-1.1.1.tgz#c5b1cd14f50aeae09ab6c59fe63ba3395fe4dfa3" @@ -3011,6 +3045,13 @@ mixin-deep@^1.2.0: for-in "^1.0.2" is-extendable "^1.0.1" +mixpanel@^0.11.0: + version "0.11.0" + resolved "https://registry.yarnpkg.com/mixpanel/-/mixpanel-0.11.0.tgz#22f2351f9bef81a7da519aff55ef580e48417b52" + integrity sha512-TS7AkCmfC+vGshlCOjEcITFoFxlt5fdSEqmN+d+pTXAhE5v+jPQW2uUcn9W+Oq4NVXz+kdskU09dsm9vmNl0ig== + dependencies: + https-proxy-agent "3.0.0" + mkdirp@^0.5.1: version "0.5.5" resolved "https://registry.yarnpkg.com/mkdirp/-/mkdirp-0.5.5.tgz#d91cefd62d1436ca0f41620e251288d420099def" @@ -4385,6 +4426,11 @@ uuid@^3.3.2: resolved "https://registry.yarnpkg.com/uuid/-/uuid-3.4.0.tgz#b23e4358afa8a202fe7a100af1f5f883f02007ee" integrity sha512-HjSDRw6gZE5JMggctHBcjVak08+KEVhSIiDzFnT9S9aegmp85S/bReBVTb4QTFaRNptJ9kuYaNhnbNEOkbKb/A== +uuid@^7.0.3: + version "7.0.3" + resolved "https://registry.yarnpkg.com/uuid/-/uuid-7.0.3.tgz#c5c9f2c8cf25dc0a372c4df1441c41f5bd0c680b" + integrity sha512-DPSke0pXhTZgoF/d+WSt2QaKMCFSfx7QegxEWT+JOuHF5aWrKEn0G+ztjuJg/gG8/ItK+rbPCD/yNv8yyih6Cg== + v8-compile-cache@^2.0.3: version "2.1.0" resolved "https://registry.yarnpkg.com/v8-compile-cache/-/v8-compile-cache-2.1.0.tgz#e14de37b31a6d194f5690d67efc4e7f6fc6ab30e"