-
Notifications
You must be signed in to change notification settings - Fork 54
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat: add telemetry with metric client
- Loading branch information
Showing
76 changed files
with
1,663 additions
and
1,515 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 }; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.