diff --git a/bin/commands/info.js b/bin/commands/info.js index 31876fb8..264cdeb8 100644 --- a/bin/commands/info.js +++ b/bin/commands/info.js @@ -1,8 +1,10 @@ 'use strict'; -var config = require('../helpers/config'); -var request = require('request') -var logger = require("../helpers/logger"); -var Constant = require("../helpers/constants") +const request = require('request'); + +const config = require("../helpers/config"), + logger = require("../helpers/logger").winstonLogger, + Constants = require("../helpers/constants"), + util = require("../helpers/util"); module.exports = function info(args) { return buildInfo(args) @@ -10,46 +12,83 @@ module.exports = function info(args) { function buildInfo(args) { let bsConfigPath = process.cwd() + args.cf; - logger.log(`Reading config from ${args.cf}`); - var bsConfig = require(bsConfigPath); - - let buildId = args._[1] - - let options = { - url: config.buildUrl + buildId, - method: 'GET', - auth: { - user: bsConfig.auth.username, - password: bsConfig.auth.access_key - } - } - - request(options, function (err, resp, body) { - if (err) { - logger.log(Constant.userMessages.BUILD_INFO_FAILED); - } else { - let build = null - try { - build = JSON.parse(body) - } catch (error) { - build = null - } - - if (resp.statusCode != 200) { - if (build) { - logger.error(`${Constant.userMessages.BUILD_INFO_FAILED} with error: \n${JSON.stringify(build, null, 2)}`); - } else { - logger.error(Constant.userMessages.BUILD_INFO_FAILED); + + util.validateBstackJson(bsConfigPath).then(function (bsConfig) { + util.setUsageReportingFlag(bsConfig, args.disableUsageReporting); + + let buildId = args._[1]; + + let options = { + url: config.buildUrl + buildId, + method: "GET", + auth: { + user: bsConfig.auth.username, + password: bsConfig.auth.access_key, + }, + headers: { + "User-Agent": util.getUserAgent(), + }, + }; + + request(options, function (err, resp, body) { + let message = null; + let messageType = null; + let errorCode = null; + + if (err) { + message = Constants.userMessages.BUILD_INFO_FAILED; + messageType = Constants.messageTypes.ERROR; + errorCode = 'api_failed_build_info'; + + logger.info(message); + } else { + let build = null; + try { + build = JSON.parse(body); + } catch (error) { + build = null; } - } else if(resp.statusCode == 299) { - if(build) { - logger.log(build.message); + + if (resp.statusCode == 299) { + messageType = Constants.messageTypes.INFO; + errorCode = "api_deprecated"; + + if (build) { + message = build.message; + logger.info(message); + } else { + message = Constants.userMessages.API_DEPRECATED; + logger.info(message); + } + } else if (resp.statusCode != 200) { + messageType = Constants.messageTypes.ERROR; + errorCode = "api_failed_build_info"; + + if (build) { + message = `${ + Constants.userMessages.BUILD_INFO_FAILED + } with error: \n${JSON.stringify(build, null, 2)}`; + logger.error(message); + if (build.message === "Unauthorized") errorCode = "api_auth_failed"; + } else { + message = Constants.userMessages.BUILD_INFO_FAILED; + logger.error(message); + } } else { - logger.log(Constants.userMessages.API_DEPRECATED); + messageType = Constants.messageTypes.SUCCESS; + message = `Build info for build id: \n ${JSON.stringify( + build, + null, + 2 + )}`; + logger.info(message); } - } else { - logger.log(`Build info for build id: \n ${JSON.stringify(build, null, 2)}`) } - } - }) + util.sendUsageReport(bsConfig, args, message, messageType, errorCode); + }) + }).catch(function (err) { + logger.error(err); + util.setUsageReportingFlag(null, args.disableUsageReporting); + util.sendUsageReport(null, args, err.message, Constants.messageTypes.ERROR, util.getErrorCodeFromErr(err)); + }) } diff --git a/bin/commands/init.js b/bin/commands/init.js index d75c69ef..5cedfd55 100644 --- a/bin/commands/init.js +++ b/bin/commands/init.js @@ -1,7 +1,8 @@ 'use strict'; -var fileHelpers = require('../helpers/fileHelpers'); -const Constants = require('../helpers/constants'); -var logger = require("../helpers/logger"); +const fileHelpers = require("../helpers/fileHelpers"), + Constants = require("../helpers/constants"), + logger = require("../helpers/logger").winstonLogger, + util = require("../helpers/util"); module.exports = function init(args) { return createBrowserStackConfig(args) @@ -21,12 +22,16 @@ function createBrowserStackConfig(args) { }; function allDone() { - logger.log(Constants.userMessages.CONFIG_FILE_CREATED); + let message = Constants.userMessages.CONFIG_FILE_CREATED + logger.info(message); + util.sendUsageReport(null, args, message, Constants.messageTypes.SUCCESS, null); } return fileHelpers.fileExists(config.path, function(exists){ if (exists) { - logger.error(Constants.userMessages.CONFIG_FILE_EXISTS); + let message = Constants.userMessages.CONFIG_FILE_EXISTS; + logger.error(message); + util.sendUsageReport(null, args, message, Constants.messageTypes.ERROR, 'bstack_json_already_exists'); } else { fileHelpers.write(config, null, allDone); } diff --git a/bin/commands/runs.js b/bin/commands/runs.js index 37a45641..4b1a90ce 100644 --- a/bin/commands/runs.js +++ b/bin/commands/runs.js @@ -1,12 +1,14 @@ 'use strict'; -var archiver = require("../helpers/archiver"); -var zipUploader = require("../helpers/zipUpload"); -var build = require("../helpers/build"); -var logger = require("../helpers/logger"); -var config = require('../helpers/config'); -var capabilityHelper = require("../helpers/capabilityHelper"); -var fs = require('fs'); -const Constants = require('../helpers/constants'); +const fs = require('fs'); + +const archiver = require("../helpers/archiver"), + zipUploader = require("../helpers/zipUpload"), + build = require("../helpers/build"), + logger = require("../helpers/logger").winstonLogger, + config = require("../helpers/config"), + capabilityHelper = require("../helpers/capabilityHelper"), + Constants = require("../helpers/constants"), + util = require("../helpers/util"); module.exports = function run(args) { return runCypress(args); @@ -15,48 +17,69 @@ module.exports = function run(args) { function deleteZip() { fs.unlink(config.fileName, function (err) { if(err) { - logger.log(Constants.userMessages.ZIP_DELETE_FAILED); + logger.info(Constants.userMessages.ZIP_DELETE_FAILED); } else { - logger.log(Constants.userMessages.ZIP_DELETED); - } + logger.info(Constants.userMessages.ZIP_DELETED); + } }); } function runCypress(args) { let bsConfigPath = process.cwd() + args.cf; - logger.log(`Reading config from ${args.cf}`); - var bsConfig = require(bsConfigPath); - - // Validate browserstack.json - capabilityHelper.validate(bsConfig).then(function (validated) { - logger.log(validated); - // Archive the spec files - archiver.archive(bsConfig.run_settings, config.fileName).then(function (data) { - // Uploaded zip file - zipUploader.zipUpload(bsConfig, config.fileName).then(function (zip) { - // Create build - build.createBuild(bsConfig, zip).then(function (data) { - return; + + util.validateBstackJson(bsConfigPath).then(function (bsConfig) { + util.setUsageReportingFlag(bsConfig, args.disableUsageReporting); + + // Validate browserstack.json values + capabilityHelper.validate(bsConfig).then(function (validated) { + logger.info(validated); + + // Archive the spec files + archiver.archive(bsConfig.run_settings, config.fileName).then(function (data) { + + // Uploaded zip file + zipUploader.zipUpload(bsConfig, config.fileName).then(function (zip) { + + // Create build + build.createBuild(bsConfig, zip).then(function (message) { + logger.info(message); + util.sendUsageReport(bsConfig, args, message, Constants.messageTypes.SUCCESS, null); + return; + }).catch(function (err) { + // Build creation failed + logger.error(err); + util.sendUsageReport(bsConfig, args, err, Constants.messageTypes.ERROR, 'build_failed'); + }); }).catch(function (err) { - // Build creation failed - logger.error(Constants.userMessages.BUILD_FAILED) + // Zip Upload failed + logger.error(err) + logger.error(Constants.userMessages.ZIP_UPLOAD_FAILED) + util.sendUsageReport(bsConfig, args, `${err}\n${Constants.userMessages.ZIP_UPLOAD_FAILED}`, Constants.messageTypes.ERROR, 'zip_upload_failed'); + }).finally(function () { + deleteZip(); }); }).catch(function (err) { - // Zip Upload failed - logger.error(err) - logger.error(Constants.userMessages.ZIP_UPLOAD_FAILED) - }).finally(function () { - deleteZip(); + // Zipping failed + logger.error(err); + logger.error(Constants.userMessages.FAILED_TO_ZIP); + util.sendUsageReport(bsConfig, args, `${err}\n${Constants.userMessages.FAILED_TO_ZIP}`, Constants.messageTypes.ERROR, 'zip_creation_failed'); + try { + deleteZip(); + } catch (err) { + util.sendUsageReport(bsConfig, args, Constants.userMessages.ZIP_DELETE_FAILED, Constants.messageTypes.ERROR, 'zip_deletion_failed'); + } }); }).catch(function (err) { - // Zipping failed - logger.error(err) - logger.error(Constants.userMessages.FAILED_TO_ZIP) - deleteZip(); + // browerstack.json is not valid + logger.error(err); + logger.error(Constants.validationMessages.NOT_VALID); + + let error_code = util.getErrorCodeFromMsg(err); + util.sendUsageReport(bsConfig, args, `${err}\n${Constants.validationMessages.NOT_VALID}`, Constants.messageTypes.ERROR, error_code); }); }).catch(function (err) { - // browerstack.json is not valid - logger.error(err) - logger.error(Constants.validationMessages.NOT_VALID) - }); + logger.error(err); + util.setUsageReportingFlag(null, args.disableUsageReporting); + util.sendUsageReport(null, args, err.message, Constants.messageTypes.ERROR, util.getErrorCodeFromErr(err)); + }) } diff --git a/bin/commands/stop.js b/bin/commands/stop.js index cb82b74b..57088a6d 100644 --- a/bin/commands/stop.js +++ b/bin/commands/stop.js @@ -1,8 +1,10 @@ 'use strict'; -var config = require('../helpers/config'); -var request = require('request') -var logger = require("../helpers/logger"); -var Constant = require("../helpers/constants") +const request = require('request'); + +const config = require("../helpers/config"), + logger = require("../helpers/logger").winstonLogger, + Constants = require("../helpers/constants"), + util = require("../helpers/util"); module.exports = function stop(args) { return buildStop(args) @@ -10,46 +12,79 @@ module.exports = function stop(args) { function buildStop(args) { let bsConfigPath = process.cwd() + args.cf; - logger.log(`Reading config from ${args.cf}`); - var bsConfig = require(bsConfigPath); - - let buildId = args._[1] - - let options = { - url: config.buildStopUrl + buildId, - method: 'POST', - auth: { - user: bsConfig.auth.username, - password: bsConfig.auth.access_key - } - } - - request(options, function (err, resp, body) { - if (err) { - logger.log(Constant.userMessages.BUILD_STOP_FAILED); - } else { - let build = null - try { - build = JSON.parse(body) - } catch (error) { - build = null - } - if (resp.statusCode != 200) { - if (build) { - logger.error(`${Constant.userMessages.BUILD_STOP_FAILED} with error: \n${JSON.stringify(build, null, 2)}`); - } else { - logger.error(Constant.userMessages.BUILD_STOP_FAILED); + util.validateBstackJson(bsConfigPath).then(function (bsConfig) { + util.setUsageReportingFlag(bsConfig, args.disableUsageReporting); + + let buildId = args._[1]; + + let options = { + url: config.buildStopUrl + buildId, + method: "POST", + auth: { + user: bsConfig.auth.username, + password: bsConfig.auth.access_key, + }, + headers: { + "User-Agent": util.getUserAgent(), + }, + }; + + request(options, function (err, resp, body) { + let message = null; + let messageType = null; + let errorCode = null; + + if (err) { + message = Constants.userMessages.BUILD_STOP_FAILED; + messageType = Constants.messageTypes.ERROR; + errorCode = 'api_failed_build_stop'; + + logger.info(message); + } else { + let build = null + try { + build = JSON.parse(body) + } catch (error) { + build = null } - } else if (resp.statusCode == 299) { - if (build) { - logger.log(build.message); + + if (resp.statusCode == 299) { + messageType = Constants.messageTypes.INFO; + errorCode = "api_deprecated"; + + if (build) { + message = build.message; + logger.info(message); + } else { + message = Constants.userMessages.API_DEPRECATED; + logger.info(message); + } + } else if (resp.statusCode != 200) { + messageType = Constants.messageTypes.ERROR; + errorCode = "api_failed_build_stop"; + + if (build) { + message = `${ + Constants.userMessages.BUILD_STOP_FAILED + } with error: \n${JSON.stringify(build, null, 2)}`; + logger.error(message); + if (build.message === "Unauthorized") errorCode = "api_auth_failed"; + } else { + message = Constants.userMessages.BUILD_STOP_FAILED; + logger.error(message); + } } else { - logger.log(Constants.userMessages.API_DEPRECATED); + messageType = Constants.messageTypes.SUCCESS; + message = `${JSON.stringify(build, null, 2)}`; + logger.info(message); } - } else { - logger.log(`${JSON.stringify(build, null, 2)}`) } - } + util.sendUsageReport(bsConfig, args, message, messageType, errorCode); + }) + }).catch(function (err) { + logger.error(err); + util.setUsageReportingFlag(null, args.disableUsageReporting); + util.sendUsageReport(null, args, err.message, Constants.messageTypes.ERROR, util.getErrorCodeFromErr(err)); }) } diff --git a/bin/helpers/archiver.js b/bin/helpers/archiver.js index 3f9fea9c..630506c0 100644 --- a/bin/helpers/archiver.js +++ b/bin/helpers/archiver.js @@ -1,7 +1,8 @@ +'use strict'; +const fs = require("fs"); -const fs = require('fs'), - archiver = require('archiver'), - logger = require("./logger"); +const archiver = require("archiver"), + logger = require("./logger").winstonLogger; const archiveSpecs = (runSettings, filePath) => { return new Promise(function (resolve, reject) { @@ -15,22 +16,22 @@ const archiveSpecs = (runSettings, filePath) => { archive.on('warning', function (err) { if (err.code === 'ENOENT') { - logger.log(err) + logger.info(err); } else { - reject(err) + reject(err); } }); output.on('close', function () { - resolve("Zipping completed") + resolve("Zipping completed"); }); output.on('end', function () { - logger.log('Data has been drained'); + logger.info('Data has been drained'); }); archive.on('error', function (err) { - reject(err) + reject(err); }); archive.pipe(output); diff --git a/bin/helpers/build.js b/bin/helpers/build.js index bc504a0d..9deb7dc5 100644 --- a/bin/helpers/build.js +++ b/bin/helpers/build.js @@ -1,8 +1,10 @@ -var request = require('request') -var logger = require("./logger") -var config = require('./config'); -var capabilityHelper = require("../helpers/capabilityHelper"); -const Constants = require('../helpers/constants'); +'use strict'; +const request = require('request'); + +const config = require('./config'), + capabilityHelper = require("../helpers/capabilityHelper"), + Constants = require('../helpers/constants'), + util =require('../helpers/util'); const createBuild = (bsConfig, zip) => { return new Promise(function (resolve, reject) { @@ -14,36 +16,37 @@ const createBuild = (bsConfig, zip) => { password: bsConfig.auth.access_key }, headers: { - 'Content-Type': 'application/json' + 'Content-Type': 'application/json', + "User-Agent": util.getUserAgent(), }, body: data } request.post(options, function (err, resp, body) { if (err) { - reject(err) + reject(err); } else { - let build = null + let build = null; try { - build = JSON.parse(body) + build = JSON.parse(body); } catch (error) { - build = null + build = null; } - if (resp.statusCode != 201) { + + if (resp.statusCode == 299) { if (build) { - logger.error(`${Constants.userMessages.BUILD_FAILED} Error: ${build.message}`); + resolve(build.message); } else { - logger.error(Constants.userMessages.BUILD_FAILED); + reject(Constants.userMessages.API_DEPRECATED); } - } else if(resp.statusCode == 299){ - if(build) { - logger.log(build.message); + } else if (resp.statusCode != 201) { + if (build) { + reject(`${Constants.userMessages.BUILD_FAILED} Error: ${build.message}`); } else { - logger.log(Constants.userMessages.API_DEPRECATED); + reject(Constants.userMessages.BUILD_FAILED); } } else { - logger.log(build.message) - logger.log(`${Constants.userMessages.BUILD_CREATED} with build id: ${build.build_id}`); + resolve(`${build.message}! ${Constants.userMessages.BUILD_CREATED} with build id: ${build.build_id}`); } resolve(build); } diff --git a/bin/helpers/capabilityHelper.js b/bin/helpers/capabilityHelper.js index 9a1ff965..769a74c0 100644 --- a/bin/helpers/capabilityHelper.js +++ b/bin/helpers/capabilityHelper.js @@ -1,6 +1,5 @@ -const logger = require("./logger"), - Constants = require('./constants'), - glob = require("glob"); +const logger = require("./logger").winstonLogger, + Constants = require("./constants"); const caps = (bsConfig, zip) => { return new Promise(function (resolve, reject) { @@ -21,17 +20,17 @@ const caps = (bsConfig, zip) => { }); obj.devices = osBrowserArray if (obj.devices.length == 0) reject(Constants.validationMessages.EMPTY_BROWSER_LIST); - logger.log(`Browser list: ${osBrowserArray.toString()}`); + logger.info(`Browser list: ${osBrowserArray.toString()}`); // Test suite obj.test_suite = zip.zip_url.split("://")[1] if (!obj.test_suite || 0 === obj.test_suite.length) reject("Test suite is empty"); - logger.log(`Test suite: bs://${obj.test_suite}`); + logger.info(`Test suite: bs://${obj.test_suite}`); // Local obj.local = false; if (bsConfig.connection_settings.local === true) obj.local = true; - logger.log(`Local is set to: ${obj.local}`); + logger.info(`Local is set to: ${obj.local}`); // Local Identifier obj.localIdentifier = null; @@ -51,11 +50,11 @@ const caps = (bsConfig, zip) => { //callback url obj.callbackURL = bsConfig.run_settings.callback_url - if (obj.callbackURL) logger.log(`callback url is : ${obj.callbackURL}`); + if (obj.callbackURL) logger.info(`callback url is : ${obj.callbackURL}`); //projectNotifyURL obj.projectNotifyURL = bsConfig.run_settings.project_notify_URL - if (obj.projectNotifyURL) logger.log(`Project notify URL is: ${obj.projectNotifyURL}`); + if (obj.projectNotifyURL) logger.info(`Project notify URL is: ${obj.projectNotifyURL}`); var data = JSON.stringify(obj); resolve(data); diff --git a/bin/helpers/config.js b/bin/helpers/config.js index 75eb084b..21897e46 100644 --- a/bin/helpers/config.js +++ b/bin/helpers/config.js @@ -11,5 +11,7 @@ config.rails_host = hosts[config.env].rails_host; config.cypress_v1 = `${config.rails_host}/automate/cypress/v1`; config.buildUrl = `${config.cypress_v1}/builds/`; config.buildStopUrl = `${config.cypress_v1}/builds/stop/`; +config.usageReportingUrl = `https://eds.browserstack.com:443/send_event_cy_internal`; config.fileName = "tests.zip"; + module.exports = config; diff --git a/bin/helpers/constants.js b/bin/helpers/constants.js index 6bf362a1..03558069 100644 --- a/bin/helpers/constants.js +++ b/bin/helpers/constants.js @@ -8,22 +8,24 @@ const userMessages = { CONFIG_FILE_CREATED: "BrowserStack Config File created, you can now run browserstack-cypress --config-file run", CONFIG_FILE_EXISTS: "File already exists, delete the browserstack.json file manually. skipping...", ZIP_DELETE_FAILED: "Could not delete local file.", - ZIP_DELETED: "File deleted successfully.", + ZIP_DELETED: "Zip file deleted successfully.", API_DEPRECATED: "This version of API is deprecated, please use latest version of API.", FAILED_TO_ZIP: "Failed to zip files." }; const validationMessages = { - INCORRECT_AUTH_PARAMS: "Incorrect auth params.", - EMPTY_BROWSER_LIST: "Browser list is empty", - EMPTY_TEST_SUITE: "Test suite is empty", - EMPTY_BROWSERSTACK_JSON: "Empty browserstack.json", - EMPTY_RUN_SETTINGS: "Empty run settings", - EMPTY_SPEC_FILES: "No spec files specified in run_settings", - VALIDATED: "browserstack.json file is validated", - NOT_VALID: "browerstack.json is not valid", - INVALID_EXTENSION: "Invalid files, please remove these files and try again." + INCORRECT_AUTH_PARAMS: "Incorrect auth params.", + EMPTY_BROWSER_LIST: "Browser list is empty", + EMPTY_TEST_SUITE: "Test suite is empty", + EMPTY_BROWSERSTACK_JSON: "Empty browserstack.json", + EMPTY_RUN_SETTINGS: "Empty run settings", + EMPTY_SPEC_FILES: "No spec files specified in run_settings", + VALIDATED: "browserstack.json file is validated", + NOT_VALID: "browerstack.json is not valid", + NOT_VALID_JSON: "browerstack.json is not a valid json", + INVALID_EXTENSION: "Invalid files, please remove these files and try again.", }; + const cliMessages = { VERSION: { INFO: "shows version information", @@ -47,11 +49,24 @@ const cliMessages = { INFO: "Run your tests on BrowserStack.", DESC: "Path to BrowserStack config", CONFIG_DEMAND: "config file is required" + }, + COMMON: { + DISABLE_USAGE_REPORTING: "Disable usage reporting" } } +const messageTypes = { + SUCCESS: "success", + ERROR: "error", + INFO: "info", + WARNING: "warning", + UNKNOWN: "unknown", + NULL: null +} + module.exports = Object.freeze({ - userMessages, - cliMessages, - validationMessages -}) + userMessages, + cliMessages, + validationMessages, + messageTypes, +}); diff --git a/bin/helpers/fileHelpers.js b/bin/helpers/fileHelpers.js index 700521a8..1aadcbf6 100644 --- a/bin/helpers/fileHelpers.js +++ b/bin/helpers/fileHelpers.js @@ -1,7 +1,9 @@ 'use strict'; -var fs = require('fs-extra'); -var path = require('path'); -var mkdirp = require('mkdirp') +const fs = require('fs-extra'), + path = require('path'), + mkdirp = require('mkdirp'); + +const logger = require('./logger').winstonLogger; exports.isEmpty = function(path, cb) { fs.readdir(path, function(err, files) { @@ -33,7 +35,7 @@ exports.isFile = function(path, cb) { exports.mkdir = function(dir, cb) { mkdirp(dir, '0755', function(err) { if (err) throw err; - console.log('Creating directory: ./' + path.relative(process.cwd(), dir)) + logger.info("Creating directory: ./" + path.relative(process.cwd(), dir)); cb && cb() }) } @@ -41,7 +43,7 @@ exports.mkdir = function(dir, cb) { exports.write = function(f, message, cb) { message = message || 'Creating'; fs.writeFile(f.path, f.file, function() { - console.log(message + ' file: ./' + path.relative(process.cwd(), f.path)); + logger.info(message + " file: ./" + path.relative(process.cwd(), f.path)); cb && cb() }); } diff --git a/bin/helpers/logger.js b/bin/helpers/logger.js index 57b83266..7563fce3 100644 --- a/bin/helpers/logger.js +++ b/bin/helpers/logger.js @@ -1,12 +1,32 @@ -function log(message) { - var timestamp = '[' + new Date().toLocaleString() + '] '; - console.log(timestamp + " " + message); -} +const winston = require('winston'), + fs = require("fs"), + path = require("path"); -function error(message) { - var timestamp = '[' + new Date().toLocaleString() + '] '; - console.log(timestamp + " [ERROR] " + message); +const logDir = "log"; // directory path for logs +if (!fs.existsSync(logDir)) { + // Create the directory if it does not exist + fs.mkdirSync(logDir); } -exports.log = log -exports.error = error +const winstonLoggerParams = { + transports: [ + new winston.transports.Console({ + colorize: true, + timestamp: function () { + return `[${new Date().toLocaleString()}]`; + }, + prettyPrint: true, + }), + ], +}; + +const winstonFileLoggerParams = { + transports: [ + new winston.transports.File({ + filename: path.join(logDir, "/usage.log"), + }), + ], +}; + +exports.winstonLogger = new winston.Logger(winstonLoggerParams); +exports.fileLogger = new winston.Logger(winstonFileLoggerParams); diff --git a/bin/helpers/usageReporting.js b/bin/helpers/usageReporting.js new file mode 100644 index 00000000..9f2a8088 --- /dev/null +++ b/bin/helpers/usageReporting.js @@ -0,0 +1,229 @@ +'use strict'; +const cp = require("child_process"), + os = require("os"), + request = require("requestretry"), + fs = require('fs'), + path = require('path'); + +const config = require('./config'), + fileLogger = require('./logger').fileLogger; + +function get_version(package_name) { + try { + let options = { stdio: 'pipe' }; + return cp.execSync(`${package_name} --version`, options).toString().trim(); + } catch (err) { + return null; + } +} + +function npm_version() { + return get_version('npm'); +} + +function _os() { + return os.platform(); +} + +function os_version() { + return os.release(); +} + +function local_cypress_version(bsConfig) { + // 1. check version of Cypress installed in local project + // 2. check version of Cypress installed globally if not present in project + + if (bsConfig) { + let version = get_version(path.join(bsConfig.run_settings.cypress_proj_dir, 'node_modules', '.bin', 'cypress')); + if (!version) { + version = get_version('cypress'); + } + return version; + } else { + return get_version('cypress'); + } +} + +function bstack_json_found_in_pwd() { + try { + if (fs.existsSync(path.join(process.cwd(), 'browserstack.json'))) { + //file exists + return true; + } + else { + return false; + } + } catch (err) { + return null; + } +} + +function cypress_json_found_in_pwd() { + try { + if (fs.existsSync(path.join(process.cwd(), 'cypress.json'))) { + //file exists + return true; + } + else { + return false; + } + } catch (err) { + return null; + } +} + +function npm_global_path() { + return cp.execSync('npm root -g', { stdio: 'pipe' }).toString().trim(); +} + +function cli_version_and_path(bsConfig) { + // 1. check version of Cypress installed in local project + // 2. check version of Cypress installed globally if not present in project + + if (bsConfig) { + let _path = path.join(bsConfig.run_settings.cypress_proj_dir, 'node_modules', 'browserstack-cypress'); + let version = get_version(_path); + if (!version) { + version = get_version('browserstack-cypress'); + + if (!version) { + // return path = null if version is null + return { + version: null, + path: null + }; + } + return { + version: version, + path: npm_global_path(), + }; + } + return { + version: version, + path: _path, + }; + } else { + let version = get_version('browserstack-cypress'); + + if (!version) { + // return path = null if version is null + return { + version: null, + path: null, + }; + } + return { + version: version, + path: npm_global_path(), + }; + } +} + +function ci_environment() { + var env = process.env; + // Jenkins + if ((typeof env.JENKINS_URL === "string" && env.JENKINS_URL.length > 0) || (typeof env.JENKINS_HOME === "string" && env.JENKINS_HOME.length > 0)) { + return "Jenkins"; + } + // CircleCI + if (env.CI === "true" && env.CIRCLECI === "true") { + return "CircleCI"; + } + // Travis CI + if (env.CI === "true" && env.TRAVIS === "true") { + return "Travis CI"; + } + // Codeship + if (env.CI === "true" && env.CI_NAME === "codeship") { + return "Codeship"; + } + // Bitbucket + if (env.BITBUCKET_BRANCH && env.BITBUCKET_COMMIT) { + return "Bitbucket"; + } + // Drone + if (env.CI === "true" && env.DRONE === "true") { + return "Drone"; + } + // Semaphore + if (env.CI === "true" && env.SEMAPHORE === "true") { + return "Semaphore"; + } + // GitLab + if (env.CI === "true" && env.GITLAB_CI === "true") { + return "GitLab"; + } + // Buildkite + if (env.CI === "true" && env.BUILDKITE === "true") { + return "Buildkite"; + } + // Visual Studio Team Services + if (env.TF_BUILD === "True") { + return "Visual Studio Team Services"; + } + // if no matches, return null + return null; +} + +function isUsageReportingEnabled() { + return process.env.DISABLE_USAGE_REPORTING; +} + +function send(args) { + if (isUsageReportingEnabled() === "true") return; + + let bsConfig = args.bstack_config; + let cli_details = cli_version_and_path(bsConfig); + + delete args.bstack_config; + + const payload = { + event_type: "cypress_cli_stats", + data: { + os: _os(), + os_version: os_version(), + bstack_json_found_in_pwd: bstack_json_found_in_pwd(), + cypress_json_found_in_pwd: cypress_json_found_in_pwd(), + cli_version: cli_details.version, + cli_path: cli_details.path, + npm_version: npm_version(), + local_cypress_version: local_cypress_version(bsConfig), + ci_environment: ci_environment(), + event_timestamp: new Date().toLocaleString(), + ...args, + }, + }; + + const options = { + headers: { + "Content-Type": "text/json", + }, + method: "POST", + url: config.usageReportingUrl, + body: payload, + json: true, + maxAttempts: 10, // (default) try 3 times + retryDelay: 2000, // (default) wait for 2s before trying again + retrySrategy: request.RetryStrategies.HTTPOrNetworkError, // (default) retry on 5xx or network errors + }; + + fileLogger.info(`Sending ${JSON.stringify(payload)} to ${config.usageReportingUrl}`); + request(options, function (error, res, body) { + if (error) { + //write err response to file + fileLogger.error(JSON.stringify(error)); + return; + } + // write response file + let response = { + attempts: res.attempts, + statusCode: res.statusCode, + body: body + }; + fileLogger.info(`${JSON.stringify(response)}`); + }); +} + +module.exports = { + send +} diff --git a/bin/helpers/util.js b/bin/helpers/util.js new file mode 100644 index 00000000..8128f9f2 --- /dev/null +++ b/bin/helpers/util.js @@ -0,0 +1,75 @@ +'use strict'; +const os = require("os"); + +const usageReporting = require('./usageReporting'), + logger = require('./logger').winstonLogger, + Constants = require('./constants'); + +exports.validateBstackJson = (bsConfigPath) => { + return new Promise(function(resolve, reject){ + try { + logger.info(`Reading config from ${bsConfigPath}`); + let bsConfig = require(bsConfigPath); + resolve(bsConfig); + } + catch (e) { + reject(e); + } + }); +} + +exports.getErrorCodeFromMsg = (errMsg) => { + let errorCode = null; + switch (errMsg) { + case Constants.validationMessages.EMPTY_BROWSERSTACK_JSON: + errorCode = "bstack_json_invalid_empty"; + break; + case Constants.validationMessages.INCORRECT_AUTH_PARAMS: + errorCode = "bstack_json_invalid_missing_keys"; + break; + case Constants.validationMessages.EMPTY_BROWSER_LIST: + errorCode = "bstack_json_invalid_no_browsers"; + break; + case Constants.validationMessages.EMPTY_RUN_SETTINGS: + errorCode = "bstack_json_invalid_no_run_settings"; + break; + case Constants.validationMessages.EMPTY_SPEC_FILES: + errorCode = "bstack_json_invalid_values"; + break; + } + return errorCode; +} + +exports.getErrorCodeFromErr = (err) => { + let errorCode = null; + if (err.code === 'SyntaxError') { + errorCode = 'bstack_json_parse_error'; + } else if (err.code === 'EACCES') { + errorCode = 'bstack_json_no_permission'; + } else { + errorCode = 'bstack_json_invalid_unknown'; + } + return errorCode +} + +exports.sendUsageReport = (bsConfig, args, message, message_type, error_code) => { + usageReporting.send({ + cli_args: args, + message: message, + message_type: message_type, + error_code: error_code, + bstack_config: bsConfig + }); +} + +exports.setUsageReportingFlag = (bsConfig, disableUsageReporting) => { + if (disableUsageReporting === undefined && bsConfig && bsConfig['disable_usage_reporting'] != undefined) { + process.env.DISABLE_USAGE_REPORTING = bsConfig['disable_usage_reporting']; + } else { + process.env.DISABLE_USAGE_REPORTING = disableUsageReporting; + } +} + +exports.getUserAgent = () => { + return `BStack-Cypress-CLI/1.x (${os.arch()}/${os.platform()}/${os.release()})`; +} diff --git a/bin/helpers/zipUpload.js b/bin/helpers/zipUpload.js index 89f7c1b8..3c3931d0 100644 --- a/bin/helpers/zipUpload.js +++ b/bin/helpers/zipUpload.js @@ -1,8 +1,10 @@ -var config = require('./config'); -var request = require('request') -var fs = require('fs'); -var logger = require("./logger") -const Constants = require("./constants") +'use strict'; +const config = require("./config"), + request = require("request"), + fs = require("fs"), + logger = require("./logger").winstonLogger, + Constants = require("./constants"), + util = require("./util"); const uploadCypressZip = (bsConfig, filePath) => { return new Promise(function (resolve, reject) { @@ -16,9 +18,13 @@ const uploadCypressZip = (bsConfig, filePath) => { file: fs.createReadStream(filePath), filetype: 'zip', filename: 'tests' + }, + headers: { + "User-Agent": util.getUserAgent(), } } + let responseData = null; request.post(options, function (err, resp, body) { if (err) { reject(err); @@ -35,7 +41,7 @@ const uploadCypressZip = (bsConfig, filePath) => { reject(Constants.userMessages.ZIP_UPLOADER_NOT_REACHABLE); } } else { - logger.log(`Zip uploaded with url: ${responseData.zip_url}`); + logger.info(`Zip uploaded with url: ${responseData.zip_url}`); resolve(responseData); } } diff --git a/bin/runner.js b/bin/runner.js index 2985dbfc..2b65587b 100755 --- a/bin/runner.js +++ b/bin/runner.js @@ -1,8 +1,8 @@ #!/usr/bin/env node - -const yargs = require('yargs') -var logger = require("./helpers/logger"); -const Constants = require('./helpers/constants'); +'use strict'; +const yargs = require('yargs'), + logger = require("./helpers/logger").winstonLogger, + Constants = require('./helpers/constants'); function checkCommands(yargs, argv, numRequired) { if (argv._.length < numRequired) { @@ -22,16 +22,22 @@ var argv = yargs .demand(1, Constants.cliMessages.VERSION.DEMAND) .command('init', Constants.cliMessages.INIT.INFO, function(yargs) { argv = yargs - .usage('usage: $0 init [options]') - .options('p', { - alias: 'path', - default: false, - description: Constants.cliMessages.INIT.DESC, - type: 'string' + .usage("usage: $0 init [options]") + .options({ + 'p': { + alias: "path", + default: false, + description: Constants.cliMessages.INIT.DESC, + type: "string", + }, + 'disable-usage-reporting': { + default: undefined, + description: Constants.cliMessages.COMMON.DISABLE_USAGE_REPORTING, + type: "boolean" + }, }) - .help('help') - .wrap(null) - .argv + .help("help") + .wrap(null).argv; if (checkCommands(yargs, argv, 1)) { return require('./commands/init')(argv); @@ -41,20 +47,27 @@ var argv = yargs argv = yargs .usage('usage: $0 ') .demand(1, Constants.cliMessages.BUILD.DEMAND) - .options('cf', { - alias: 'config-file', - describe: Constants.cliMessages.BUILD.DESC, - default: '/browserstack.json', - type: 'string', - nargs: 1, - demand: true, - demand: Constants.cliMessages.BUILD.CONFIG_DEMAND + .options({ + 'cf': { + alias: 'config-file', + describe: Constants.cliMessages.BUILD.DESC, + default: '/browserstack.json', + type: 'string', + nargs: 1, + demand: true, + demand: Constants.cliMessages.BUILD.CONFIG_DEMAND + }, + 'disable-usage-reporting': { + default: undefined, + description: Constants.cliMessages.COMMON.DISABLE_USAGE_REPORTING, + type: "boolean" + }, }) .help('help') .wrap(null) .argv if (checkCommands(yargs, argv, 1)) { - logger.log(Constants.cliMessages.BUILD.INFO_MESSAGE + argv._[1]); + logger.info(Constants.cliMessages.BUILD.INFO_MESSAGE + argv._[1]); return require('./commands/info')(argv); } }) @@ -62,34 +75,48 @@ var argv = yargs argv = yargs .usage('usage: $0 ') .demand(1, Constants.cliMessages.BUILD.DEMAND) - .options('cf', { - alias: 'config-file', - describe: Constants.cliMessages.BUILD.DESC, - default: '/browserstack.json', - type: 'string', - nargs: 1, - demand: true, - demand: Constants.cliMessages.BUILD.CONFIG_DEMAND + .options({ + 'cf': { + alias: 'config-file', + describe: Constants.cliMessages.BUILD.DESC, + default: '/browserstack.json', + type: 'string', + nargs: 1, + demand: true, + demand: Constants.cliMessages.BUILD.CONFIG_DEMAND + }, + 'disable-usage-reporting': { + default: undefined, + description: Constants.cliMessages.COMMON.DISABLE_USAGE_REPORTING, + type: "boolean" + }, }) .help('help') .wrap(null) .argv if (checkCommands(yargs, argv, 1)) { - logger.log(Constants.cliMessages.BUILD.STOP_MESSAGE + argv._[1]); + logger.info(Constants.cliMessages.BUILD.STOP_MESSAGE + argv._[1]); return require('./commands/stop')(argv); } }) .command('run', Constants.cliMessages.RUN.INFO, function(yargs) { argv = yargs .usage('usage: $0 build') - .options('cf', { - alias: 'config-file', - describe: Constants.cliMessages.RUN.DESC, - default: '/browserstack.json', - type: 'string', - nargs: 1, - demand: true, - demand: Constants.cliMessages.RUN.CONFIG_DEMAND + .options({ + 'cf': { + alias: 'config-file', + describe: Constants.cliMessages.RUN.DESC, + default: '/browserstack.json', + type: 'string', + nargs: 1, + demand: true, + demand: Constants.cliMessages.RUN.CONFIG_DEMAND + }, + 'disable-usage-reporting': { + default: undefined, + description: Constants.cliMessages.COMMON.DISABLE_USAGE_REPORTING, + type: "boolean" + }, }) .help('help') .wrap(null) diff --git a/bin/templates/configTemplate.js b/bin/templates/configTemplate.js index cbeae6cb..bf137d65 100644 --- a/bin/templates/configTemplate.js +++ b/bin/templates/configTemplate.js @@ -19,7 +19,8 @@ module.exports = function () { "connection_settings": { "local": false, "local_identifier": null - } + }, + "disable_usage_reporting": false } var EOL = require('os').EOL var file = [ diff --git a/package.json b/package.json index 29363418..91158511 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "browserstack-cypress-cli", - "version": "1.1.0", + "version": "1.1.3", "description": "BrowserStack Cypress CLI for Cypress integration with BrowserStack's remote devices.", "main": "index.js", "scripts": { @@ -16,6 +16,8 @@ "fs-extra": "^8.1.0", "mkdirp": "^1.0.3", "request": "^2.88.0", + "requestretry": "^4.1.0", + "winston": "^2.3.1", "yargs": "^14.2.2" }, "repository": {