Skip to content

Commit

Permalink
feat: add telemetry with metric client
Browse files Browse the repository at this point in the history
  • Loading branch information
kakha urigashvili authored and RonWang committed Feb 24, 2020
1 parent 284b933 commit e4e72e2
Show file tree
Hide file tree
Showing 76 changed files with 1,663 additions and 1,515 deletions.
162 changes: 162 additions & 0 deletions lib/clients/metric-client/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,162 @@
const uuid = require('uuid/v4');
const axios = require('axios');

const MetricActionResult = {
SUCCESS: 'Success',
FAILURE: 'Failure'
};

class MetricAction {
/**
* @constructor
* @param {string} name - The action name.
* @param {string} type - The action type.
*/
constructor(name, type) {
this.endTime = null;
this.failureMessage = '';
this.name = name;
this.result = null;
this.startTime = new Date();
this.type = type;
this.id = uuid();
this._ended = false;
}

/**
* Closes action
* @param {Error|string} [error=null] error - Error object or string indicating error.
*/
end(error = null) {
if (this._ended) return;

// if Error object extract error message,
// otherwise error message string or null was passed as a parameter
const errorMessage = error && error instanceof Error ? error.message : error;

this.result = errorMessage ? MetricActionResult.FAILURE : MetricActionResult.SUCCESS;
this.failureMessage = errorMessage || '';
this.endTime = new Date();
this._ended = true;
}

/**
* Implementation of custom toJSON method to modify serialization with JSON.stringify
*/
toJSON() {
return {
end_time: this.endTime,
failure_message: this.failureMessage,
name: this.name,
result: this.result,
start_time: this.startTime,
type: this.type,
id: this.id
};
}
}

class MetricClient {
/**
* A metric client options
* @typedef {Object} MetricClientOptions
* @property {string} version - The application version. Typically, version form package.json
* @property {string} machineId - The machine id
* @property {boolean} newUser - is new user
* @property {string} clientId - The client id. Typically, application name. For example, ask cli.
* @property {string} serverUrl - The server url where to send metrics data.
* @property {number} sendTimeout - The send timeout to send data to the metrics server.
*/

/**
* @constructor
* @param {MetricClientOptions} options - The options for constructor
*/
constructor(options) {
const { version, machineId, newUser, clientId, serverUrl, sendTimeout, enabled } = options;
this.httpClient = axios.create({
timeout: sendTimeout || 3000,
headers: { 'Content-Type': 'text/plain' }
});
this.serverUrl = serverUrl;
this.postRetries = 3;
this.enabled = enabled !== false;
this.data = {
version,
machineId,
timeStarted: new Date(),
newUser,
timeUploaded: null,
clientId,
actions: []
};
}

/**
* Starts action
* @param {string} name - The action name
* @param {string} type - The action type
* @return {MetricAction}
*/
startAction(name, type) {
const action = new MetricAction(name, type);
this.data.actions.push(action);
return action;
}

/**
* Returns current data store in the metric client
* @return {{version: string, machineId: string, timeStarted: Date,
* newUser: boolean, timeUploaded: Date|null, clientId: string, actions: MetricAction[]}}
*/
getData() {
return this.data;
}

/**
* Sends data to the metric server
* @param {Error|string} [error=null] error - Error object or string indicating error.
* @returns {Promise<{success: boolean}>}
*/
sendData(error = null) {
if (!this.enabled) {
this.data.actions = [];
return new Promise(resolve => resolve({ success: true }));
}
this.data.actions.forEach(action => action.end(error));
return this._upload()
.then(() => {
this.data.actions = [];
return { success: true };
})
.catch(() => ({ success: false }));
}

/**
* Implementation of custom toJSON method to modify serialization with JSON.stringify
*/
toJSON() {
return {
version: this.data.version,
machine_id: this.data.machineId,
time_started: this.data.timeStarted,
new_user: this.data.newUser,
time_uploaded: this.data.timeUploaded,
client_id: this.data.clientId,
actions: this.data.actions
};
}

_upload() {
this.data.timeUploaded = new Date();
const payload = JSON.stringify({ payload: this });
const postPromise = () => this.httpClient.post(this.serverUrl, payload);
return this._retry(this.postRetries, postPromise);
}

_retry(retries, fn) {
return fn().catch(err => (retries > 1 ? this._retry(retries - 1, fn) : Promise.reject(err)));
}
}

module.exports = { MetricClient, MetricActionResult };
15 changes: 11 additions & 4 deletions lib/commands/abstract-command.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ const {
} = require('@src/commands/option-validator');
const AppConfig = require('@src/model/app-config');
const CONSTANTS = require('@src/utils/constants');
const metricClient = require('@src/utils/metrics');
const Messenger = require('@src/view/messenger');

/**
Expand Down Expand Up @@ -64,10 +65,12 @@ class AbstractCommand {
}

_registerAction(commander) {
commander.action((...args) => {
commander.action((...args) => new Promise((resolve) => {
// set Messenger debug preferrance
Messenger.getInstance().doDebug = args[0].debug;

metricClient.startAction(args[0]._name, 'command');

// validate options
try {
this._validateOptions(args[0]);
Expand All @@ -82,14 +85,18 @@ class AbstractCommand {
}
} catch (err) {
Messenger.getInstance().error(err);
resolve();
this.exit(1);
return;
}

// execute handler logic of each command; quit execution
this.handle(...args, (error) => {
this.exit(error ? 1 : 0);
metricClient.sendData(error).then(() => {
resolve();
this.exit(error ? 1 : 0);
});
});
});
}));
}

_registerOptions(commander) {
Expand Down
2 changes: 1 addition & 1 deletion lib/commands/api/catalog/list-catalogs/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,7 @@ function getCatalogList(smapiClient, cmd, vendorId, callback) {
return callback(err);
}
if (response.statusCode >= 300) {
callback(jsonView.toString(response.body), null);
return callback(jsonView.toString(response.body), null);
}
callback(null, response.body);
});
Expand Down
44 changes: 44 additions & 0 deletions lib/model/metric-config.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
const fs = require('fs-extra');
const jsonfile = require('jsonfile');
const os = require('os');
const path = require('path');
const uuid = require('uuid/v4');
const { FILE_PATH, CONFIGURATION, METRICS } = require('@src/utils/constants');

const askFolderPath = path.join(os.homedir(), FILE_PATH.ASK.HIDDEN_FOLDER);
const defaultMetricFilePath = path.join(askFolderPath, FILE_PATH.ASK.METRIC_FILE);

class MetricConfig {
/**
* Constructor for MetricConfig class
* @param {string} filePath
*/
constructor(filePath = defaultMetricFilePath) {
// making file path if not exists
if (!fs.existsSync(filePath)) {
fs.ensureDirSync(askFolderPath);
jsonfile.writeFileSync(filePath, { machineId: uuid(), createdAt: new Date() }, { spaces: CONFIGURATION.JSON_DISPLAY_INDENT });
}
this.data = JSON.parse(fs.readFileSync(filePath));
}

/**
* Gets machineId property
* @returns {string}
*/
get machineId() {
return this.data.machineId;
}

/**
* Returns boolean indicating if user is new
* @returns {boolean}
*/
isNewUser() {
const { createdAt } = this.data;
const daysDiff = (new Date().getTime() - new Date(createdAt).getTime()) / (1000 * 3600 * 24);
return daysDiff <= METRICS.NEW_USER_LENGTH_DAYS;
}
}

module.exports = MetricConfig;
8 changes: 7 additions & 1 deletion lib/utils/constants.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,10 @@
const DEFAULT_LIST_MAX_RESULT = 50;

module.exports.METRICS = {
ENDPOINT: '', // TODO add the official endpoint when we have it
NEW_USER_LENGTH_DAYS: 3
};

module.exports.SKILL = {
RESOURCES: {
MANIFEST: 'manifest',
Expand Down Expand Up @@ -102,7 +107,8 @@ module.exports.FILE_PATH = {
},
ASK: {
HIDDEN_FOLDER: '.ask',
PROFILE_FILE: 'cli_config'
PROFILE_FILE: 'cli_config',
METRIC_FILE: 'cli_metric'
}
};

Expand Down
18 changes: 18 additions & 0 deletions lib/utils/metrics.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
const { MetricClient } = require('@src/clients/metric-client');
// const MetricConfig = require('@src/model/metric-config');
const { METRICS } = require('@src/utils/constants');
const { name, version } = require('./../../package.json');

// TODO enable when we have configure command prompting for telemetry
// const metricConfig = new MetricConfig();

const metricClient = new MetricClient({
version,
machineId: '', // metricConfig.machineId,
newUser: false, // metricConfig.isNewUser(),
clientId: name,
serverUrl: METRICS.ENDPOINT,
enabled: false // TODO make it dependent on configure command
});

module.exports = metricClient;
6 changes: 5 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -38,9 +38,10 @@
"async": "^2.1.5",
"aws-profile-handler": "2.0.3",
"aws-sdk": "^2.288.0",
"axios": "^0.19.2",
"bunyan": "^1.8.12",
"chalk": "2.4.2",
"commander": "^2.9.0",
"commander": "^4.1.1",
"date-fns": "^2.7.0",
"folder-hash": "^3.0.0",
"fs-extra": "^2.1.0",
Expand All @@ -62,12 +63,15 @@
"simple-git": "^1.82.0",
"simple-oauth2": "1.0.3",
"tmp": "^0.1.0",
"uuid": "^3.4.0",
"valid-url": "^1.0.9"
},
"devDependencies": {
"@commitlint/cli": "^8.2.0",
"@commitlint/config-conventional": "^8.2.0",
"chai": "^3.5.0",
"chai-json-schema": "^1.5.1",
"chai-uuid": "^1.0.6",
"coveralls": "^3.0.2",
"eslint": "^6.8.0",
"eslint-config-airbnb-base": "^14.0.0",
Expand Down
Loading

0 comments on commit e4e72e2

Please sign in to comment.