Skip to content
This repository has been archived by the owner on Oct 4, 2024. It is now read-only.

Metrics events - add utils to support mixpanel integration and persisting user settings in shell. #323

Merged
merged 24 commits into from
Apr 24, 2020
Merged
Show file tree
Hide file tree
Changes from 14 commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
e2bc3c9
Initial stab at dev event tracking (E2E with mixpanel works)
janedegtiareva Apr 16, 2020
d986761
Clean up eventtracking util class
janedegtiareva Apr 17, 2020
b229756
Add error handling to the mixpanel calls
janedegtiareva Apr 17, 2020
c799108
Pull out settings keys into constants
janedegtiareva Apr 21, 2020
fd64c97
Fix lint errors
janedegtiareva Apr 21, 2020
96ddf5a
Remove extra test code from main login command.
janedegtiareva Apr 21, 2020
1025c64
Add example of tracking usage in viewState
janedegtiareva Apr 21, 2020
fc93a27
Undo accidental change in login
janedegtiareva Apr 21, 2020
83b5c51
Fix lint errors
janedegtiareva Apr 21, 2020
62b8288
Pull out saving settings into a separate util
janedegtiareva Apr 21, 2020
e306f5c
Add test settings file to init with on ci
janedegtiareva Apr 21, 2020
eec7065
Remove accountId from view state tracking (PII)
janedegtiareva Apr 21, 2020
1e0a592
Fix typo in test settings setup
janedegtiareva Apr 21, 2020
e6684af
Adding more events in tracking
janedegtiareva Apr 21, 2020
1f79bbd
Address code review comments
janedegtiareva Apr 22, 2020
f99491d
Generate test settings file on the fly from test script
janedegtiareva Apr 22, 2020
2a80c73
Remove copying testsettings step from the test script
janedegtiareva Apr 22, 2020
9ae4d96
Update text for dev event tracking opt-in question
janedegtiareva Apr 23, 2020
4df2336
Merge branch 'master' into metrics-events
janedegtiareva Apr 23, 2020
80ff9d2
Merge conflict cleanup
janedegtiareva Apr 23, 2020
ad77f3c
Fix typo in settings key in generated test settings file
janedegtiareva Apr 23, 2020
ab9a4b7
Remove tracking from create account temporarily
janedegtiareva Apr 23, 2020
cc93a4e
Fix merge error and readd event tracking to create account
janedegtiareva Apr 23, 2020
80caf92
Remove unused import (lint)
janedegtiareva Apr 23, 2020
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions commands/create-account.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
const exitOnError = require('../utils/exit-on-error');
const connect = require('../utils/connect');
const { KeyPair, utils } = require('near-api-js');
const eventtracking = require('../utils/eventtracking');

module.exports = {
command: 'create_account <accountId>',
Expand Down Expand Up @@ -31,6 +32,8 @@ module.exports = {
};

async function createAccount(options) {
await eventtracking.track(eventtracking.EVENT_ID_CREATE_ACCOUNT_START, {});
console.log("?")
janedegtiareva marked this conversation as resolved.
Show resolved Hide resolved
options.initialBalance = utils.format.parseNearAmount(options.initialBalance);
// NOTE: initialBalance is passed as part of config here
let near = await connect(options);
Expand All @@ -47,4 +50,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, {});
}
3 changes: 3 additions & 0 deletions commands/tx-status.js
Original file line number Diff line number Diff line change
Expand Up @@ -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 <hash>',
Expand All @@ -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(':');
Expand All @@ -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, {});
})
};
9 changes: 7 additions & 2 deletions index.js
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -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(
Expand Down Expand Up @@ -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);
Expand All @@ -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();
Expand All @@ -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) {
Expand Down
2 changes: 2 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -37,13 +37,15 @@
"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",
"rimraf": "^3.0.0",
"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"
},
Expand Down
2 changes: 2 additions & 0 deletions test/index.sh
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
#!/bin/bash
OVERALL_RESULT=0
mkdir ~/.near-config
cp ./test/testsettings ~/.near-config/settings
for test in ./test/test_*; do
echo ""
echo "Running $test"
Expand Down
1 change: 1 addition & 0 deletions test/testsettings
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
{"trackingEnaled":false}
janedegtiareva marked this conversation as resolved.
Show resolved Hide resolved
83 changes: 83 additions & 0 deletions utils/eventtracking.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
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 = 'trackingEnaled';
janedegtiareva marked this conversation as resolved.
Show resolved Hide resolved
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. {bold.yellow Would you like to opt in (y/n)?}`,
janedegtiareva marked this conversation as resolved.
Show resolved Hide resolved
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.trackingSessionId
};
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',
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

not used

EVENT_ID_DEPLOY: 'shell_deploy',
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

not used

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

that's already addressed in the part 2 of this effort which actually does events for all the things. can we let this be to avoid unnecessary churn?

EVENT_ID_DEV_DEPLOY: 'shell_dev_deploy',
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

not used

EVENT_ID_CREATE_ACCOUNT_START: 'shell_create_account_start',
EVENT_ID_CREATE_ACCOUNT_SUCCESS: 'shell_create_account_success'
};
42 changes: 42 additions & 0 deletions utils/settings.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
const fs = require('fs');
const homedir = require('os').homedir();
const path = require('path');


// Persistent shell settings

const getShellSettings = () => {
const nearPath = path.join(homedir, '.near-config');
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

should this be .near or .near-shell or .neardev?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

also let's make this a constant? same for settings.js

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

should this be .near or .near-shell or .neardev?

@ilblackdragon I recommended Jane put it here as it fits with the structure I laid out in the NEP.
https://github.com/nearprotocol/NEPs/blob/974887126800be47a58e968fac5b997db7d50f7d/text/0000-near-shell.md#settings-config-and-key-management

─ awesome-near-project  ⟵ NEAR dApp
 ├── .near-config       ⟵ Stores shell-experience settings and connection configuration
 │  ├── connections     ⟵ Contains files used to connect and deploy a contract (except keys)
 │  │  ├── default.js   ⟵ Default configuration is for development
 │  │  └── localnet.js  ⟵ Custom connection added by user for localnet
 │  └── settings.js     ⟵ Stores near-shell settings, how shell behaves when run in this project
 └── .near-credentials  ⟵ Previously "neardev" this directory contains private key inforamtion
    └── default         
       └── alice.json   ⟵ NEAR account "alice" has private key information here

(the same structure also appears in the home directory)
Based on the comment in the shell NEP, I understood that we were not going to put anything in ~/.near as that's reserved for validator data. We can modify the NEP to be .neardev if we want instead of .near-config. Currently neardev isn't a great name and holds private keys, which the NEP calls to change to a more informative directory .near-credentials that folks aren't likely to accidentally revision. Anyway, that's the thought process. Again, we can modify the NEP.

Copy link
Contributor Author

@janedegtiareva janedegtiareva Apr 22, 2020

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

moved string literals to const. @mikedotexe thanks for the context for dir structure!

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we need configs inside project directory?
I really think we should not store any credentials inside project directory - because that's the easiest way to lose them. E.g. #198
I also think settings should live in common directory for all projects.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@ilblackdragon we don't need credentials, but per-project config is necessary. At the very least, it's important for making sure that people have updated deps e.g. shell itself. I agree that creds and global config should be in a common dir for everything.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@potatodepaulo read the NEP and left comments there - near/NEPs#31 (review)
High level, I don't think there is need in local configuration folder, especially one that starts with . as those directories should be gitignored. package.json can contain settings that should be "clonnable".

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we limit the discussion to user defined settings here in order to get the PR in for RL1?

try {
if (!fs.existsSync(nearPath)) {
fs.mkdirSync(nearPath);
}
const shellSettingsPath = path.join(nearPath, 'settings');
mikedotexe marked this conversation as resolved.
Show resolved Hide resolved
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, '.near-config');
try {
if (!fs.existsSync(nearPath)) {
fs.mkdirSync(nearPath);
}
const shellSettingsPath = path.join(nearPath, 'settings');
fs.writeFileSync(shellSettingsPath, JSON.stringify(settings));
} catch (e) {
console.log(e);
}
};

module.exports = {
getShellSettings,
saveShellSettings
};
46 changes: 46 additions & 0 deletions yarn.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.