From 1bbf80aa9a6ad9be87d007162d7899ce85da7a2d Mon Sep 17 00:00:00 2001 From: Adrian Smijulj Date: Tue, 18 Feb 2020 11:57:04 +0100 Subject: [PATCH] feat: improve CLI usage tracking (#723) --- packages/cli/cli.js | 22 +++---- packages/cli/create/index.js | 9 ++- packages/cli/package.json | 1 + packages/cli/sls/deploy.js | 3 + packages/cli/sls/disableTracking.js | 10 ++++ packages/cli/sls/enableTracking.js | 8 +++ packages/cli/sls/execute.js | 2 +- packages/cli/sls/template/serverless.js | 71 ++++++++++++++++------- packages/cli/sls/template/utils.js | 5 +- packages/http-handler-ssr/src/index.ts | 28 +++++++-- packages/tracking/index.js | 76 ++++++++++++++++++++----- packages/tracking/package.json | 1 + 12 files changed, 177 insertions(+), 59 deletions(-) create mode 100644 packages/cli/sls/disableTracking.js create mode 100644 packages/cli/sls/enableTracking.js diff --git a/packages/cli/cli.js b/packages/cli/cli.js index f1a8ec0557f..4c6c5eb40c1 100644 --- a/packages/cli/cli.js +++ b/packages/cli/cli.js @@ -133,26 +133,20 @@ yargs.command( ); yargs.command( - "disable-tracking", - "Disable tracking of Webiny stats.", + "enable-tracking", + "Enable tracking of Webiny stats.", () => {}, - () => { - const { setTracking } = require("./config"); - setTracking(false); - console.log("INFO: tracking of Webiny stats is now DISABLED!"); + async () => { + await require("./sls/enableTracking")(); } ); yargs.command( - "enable-tracking", - "Enable tracking of Webiny stats.", + "disable-tracking", + "Disable tracking of Webiny stats.", () => {}, - () => { - const { setTracking } = require("./config"); - setTracking(true); - console.log( - "INFO: tracking of Webiny stats is now ENABLED! Thank you for helping us with anonymous data šŸŽ‰" - ); + async () => { + await require("./sls/disableTracking")(); } ); diff --git a/packages/cli/create/index.js b/packages/cli/create/index.js index 182b6899fa4..ba944a3d0bf 100644 --- a/packages/cli/create/index.js +++ b/packages/cli/create/index.js @@ -9,10 +9,11 @@ const uuid = require("uuid/v4"); const loadJsonFile = require("load-json-file"); const ora = require("ora"); const writeJsonFile = require("write-json-file"); -const { trackProject } = require("@webiny/tracking"); +const { trackActivity } = require("@webiny/tracking"); const { version } = require(require.resolve("@webiny/cli/package.json")); const { getSuccessBanner } = require("./messages"); const { getPackageVersion } = require("./utils"); +const uniqueId = require('uniqid'); const globFiles = util.promisify(glob); @@ -33,6 +34,10 @@ module.exports = async ({ name, tag }) => { } console.log(`šŸ“¦ Creating a new Webiny project in ${green(root)}...`); + + const activityId = uniqueId(); + await trackActivity({ activityId, type: "create_project_start", cliVersion: version }); + fs.ensureDirSync(root); process.chdir(root); @@ -126,7 +131,7 @@ module.exports = async ({ name, tag }) => { await execa("yarn", [], { cwd: root }); spinner.succeed(`All dependencies installed successfully!`); - await trackProject({ cliVersion: version }); + await trackActivity({ activityId, type: "create_project_end", cliVersion: version }); console.log(await getSuccessBanner()); }; diff --git a/packages/cli/package.json b/packages/cli/package.json index 53a2ef9e53d..6e17c0180e9 100644 --- a/packages/cli/package.json +++ b/packages/cli/package.json @@ -34,6 +34,7 @@ "semver": "^7.1.2", "traverse": "^0.6.6", "update-notifier": "^3.0.1", + "uniqid": "^5.0.3", "uuid": "^3.3.3", "write-json-file": "^4.2.0", "yargs": "^14.0.0" diff --git a/packages/cli/sls/deploy.js b/packages/cli/sls/deploy.js index 1322619ee52..a6a251bcb05 100644 --- a/packages/cli/sls/deploy.js +++ b/packages/cli/sls/deploy.js @@ -95,7 +95,9 @@ module.exports = async inputs => { webinyJs )}, skipping processing of hooks.` ); + console.log(`\nšŸŽ‰ Done! Deploy finished in ${green(duration + "s")}.`); + return; } @@ -119,6 +121,7 @@ module.exports = async inputs => { } console.log(`\nšŸŽ‰ Done! Deploy finished in ${green(duration + "s")}.`); + notify({ message: `API deploy completed in ${duration}s.` }); if (output.cdn) { diff --git a/packages/cli/sls/disableTracking.js b/packages/cli/sls/disableTracking.js new file mode 100644 index 00000000000..8a9141062a1 --- /dev/null +++ b/packages/cli/sls/disableTracking.js @@ -0,0 +1,10 @@ +const { setTracking } = require("./../config"); +const { version } = require(require.resolve("@webiny/cli/package.json")); +const { trackActivity } = require("@webiny/tracking"); +const uniqueId = require('uniqid'); + +module.exports = async () => { + await trackActivity({ type: 'tracking-disabled', cliVersion: version, activityId: uniqueId() }); + setTracking(false); + console.log("INFO: tracking of Webiny stats is now DISABLED!"); +}; diff --git a/packages/cli/sls/enableTracking.js b/packages/cli/sls/enableTracking.js new file mode 100644 index 00000000000..56d6e224521 --- /dev/null +++ b/packages/cli/sls/enableTracking.js @@ -0,0 +1,8 @@ +const { setTracking } = require("./../config"); + +module.exports = async () => { + setTracking(true); + console.log( + "INFO: tracking of Webiny stats is now ENABLED! Thank you for helping us with anonymous data šŸŽ‰" + ); +}; diff --git a/packages/cli/sls/execute.js b/packages/cli/sls/execute.js index 8d97a4a694b..500697253d5 100644 --- a/packages/cli/sls/execute.js +++ b/packages/cli/sls/execute.js @@ -30,7 +30,7 @@ module.exports = async (inputs, method = "default") => { const component = new Template(`Webiny.${env}`, context); await component.init(); - const output = await component[method]({ env, debug, alias }); + const output = await component[method]({ env, debug, alias, what }); if (debug) { // Add an empty line after debug output for nicer output console.log(); diff --git a/packages/cli/sls/template/serverless.js b/packages/cli/sls/template/serverless.js index 734f35a4415..7d4f018c1d7 100644 --- a/packages/cli/sls/template/serverless.js +++ b/packages/cli/sls/template/serverless.js @@ -2,6 +2,9 @@ const fs = require("fs"); const path = require("path"); const { Component } = require("@serverless/core"); const { loadEnv } = require("../utils"); +const uniqueId = require("uniqid"); +const { trackActivity, trackError } = require("@webiny/tracking"); +const { version } = require(require.resolve("@webiny/cli/package.json")); const { getTemplate, @@ -52,37 +55,67 @@ class Template extends Component { } async deploy(inputs = {}) { - this.context.status("Deploying"); + const activityId = uniqueId(); + const { what } = inputs; + + await trackActivity({ + activityId, + type: `${what}_deploy_start`, + cliVersion: version, + context: this.context + }); - const template = await getTemplate(inputs); + try { + this.context.status("Deploying"); - const resolvedTemplate = resolveTemplate(inputs, template); + const template = await getTemplate(inputs); - this.context.debug("Collecting components from the template."); + const resolvedTemplate = resolveTemplate(inputs, template); - const allComponents = getAllComponents(resolvedTemplate); + this.context.debug("Collecting components from the template."); - const allComponentsWithDependencies = setDependencies(allComponents); + const allComponents = getAllComponents(resolvedTemplate); - const graph = createGraph(allComponentsWithDependencies); + const allComponentsWithDependencies = setDependencies(allComponents); - await syncState(allComponentsWithDependencies, this); + const graph = createGraph(allComponentsWithDependencies); - this.context.debug(`Executing the template's components graph.`); + await syncState(allComponentsWithDependencies, this); - const allComponentsWithOutputs = await executeGraph( - allComponentsWithDependencies, - graph, - this, - inputs - ); + this.context.debug(`Executing the template's components graph.`); - const outputs = getOutputs(allComponentsWithOutputs); + const allComponentsWithOutputs = await executeGraph( + allComponentsWithDependencies, + graph, + this, + inputs + ); - this.state.outputs = outputs; - await this.save(); + const outputs = getOutputs(allComponentsWithOutputs); - return outputs; + this.state.outputs = outputs; + await this.save(); + + await trackActivity({ + activityId, + type: `${what}_deploy_end`, + cliVersion: version, + context: this.context + }); + + return outputs; + } catch (e) { + await trackError({ + context: this.context, + activityId, + type: `${what}_deploy`, + cliVersion: version, + errorMessage: e.message, + errorStack: e.stack + }); + + throw e; + } } async deployAlias(alias, inputs) { diff --git a/packages/cli/sls/template/utils.js b/packages/cli/sls/template/utils.js index d015e723a5a..0a60c916c6c 100644 --- a/packages/cli/sls/template/utils.js +++ b/packages/cli/sls/template/utils.js @@ -249,10 +249,7 @@ const executeGraph = async (allComponents, graph, instance, inputs) => { await trackComponent({ context: instance.context, component: componentData.path }); } catch (err) { instance.context.log(`An error occurred during deployment of ${red(alias)}`); - console.log(); - console.log(err); - console.log(); - process.exit(1); + throw err; } graph.removeNode(alias); diff --git a/packages/http-handler-ssr/src/index.ts b/packages/http-handler-ssr/src/index.ts index 15d9546aad0..646affa9aef 100644 --- a/packages/http-handler-ssr/src/index.ts +++ b/packages/http-handler-ssr/src/index.ts @@ -3,18 +3,34 @@ import ssrServe from "./ssrServe"; import models from "./models"; import { withFields, boolean, number, string, fields } from "@webiny/commodo/fields"; -const OptionsModel = withFields({ - ssrFunction: string({ value: process.env.SSR_FUNCTION }), - cache: fields({ +const Animal = withFields({ + name: string({ + validate: value => { + if (!value) { + throw Error("A pet must have a name!"); + } + } + }), + age: number(), + isAwesome: boolean(), + about: fields({ value: {}, instanceOf: withFields({ - enabled: boolean({ value: false }), - ttl: number({ value: 80 }), - staleTtl: number({ value: 20 }) + type: string({ value: "cat" }), + dangerous: boolean({ value: true }) })() }) })(); +const animal = new Animal(); +animal.populate({ age: "7" }); // Throws data type error, cannot populate a string with number. + +animal.populate({ age: 7 }); +await animal.validate(); // Throws a validation error - name must be defined. + +animal.name = "Garfield"; +await animal.validate(); // All good. + export default rawOptions => { const options = new OptionsModel().populate(rawOptions); const plugins = [models(options), ssrServe(options)]; diff --git a/packages/tracking/index.js b/packages/tracking/index.js index 64693bbbcf4..a091030055f 100644 --- a/packages/tracking/index.js +++ b/packages/tracking/index.js @@ -2,23 +2,24 @@ const os = require("os"); const path = require("path"); const readJson = require("load-json-file"); const request = require("request"); +const get = require("lodash.get"); let config; const defaultLogger = () => {}; -const sendStats = data => { +const sendStats = (action, data) => { return new Promise(resolve => { request.post( { url: "https://stats.webiny.com/track", - json: data + json: { action, data } }, resolve ); }); }; -const loadConfig = async ({ logger = defaultLogger }) => { +const loadConfig = async ({ logger = defaultLogger } = {}) => { if (!config) { const dataPath = path.join(os.homedir(), ".webiny", "config"); try { @@ -45,10 +46,10 @@ const trackComponent = async ({ context, component, method = "deploy" }) => { const { name, version } = readJson.sync(path.join(path.dirname(component), "package.json")); context.debug(`Tracking component: ${name} (${method})`); - await sendStats({ + await sendStats("telemetry", { type: "component", user: config.id, - instance: context.instance.id, + instance: get(context, "instance.id") || "", component: name, version, method @@ -58,18 +59,66 @@ const trackComponent = async ({ context, component, method = "deploy" }) => { } }; -const trackProject = async ({ cliVersion }) => { +const trackActivity = async ({ cliVersion, type, activityId, context = {} }) => { + if (!cliVersion) { + throw new Error(`Cannot track activity - "cliVersion" not specified.`); + } + + if (!type) { + throw new Error(`Cannot track activity - "type" not specified.`); + } + + if (!activityId) { + throw new Error(`Cannot track activity - "activityId" not specified.`); + } + try { - await loadConfig(); + await loadConfig({ logger: context.debug }); if (config.tracking !== true) { return; } - await sendStats({ - type: "project", - user: config.id, - version: cliVersion + await sendStats("activities", { + version: cliVersion, + type, + activityId, + instance: get(context, "instance.id") || "", + user: config.id + }); + } catch (e) { + // Ignore errors + } +}; + +const trackError = async ({ cliVersion, type, errorMessage, errorStack, activityId, context = {} }) => { + if (!cliVersion) { + throw new Error("Cannot track activity - CLI version not specified."); + } + + if (!type) { + throw new Error("Cannot track activity - type not specified."); + } + + if (!errorMessage) { + throw new Error("Cannot track activity - CLI version not specified."); + } + + try { + await loadConfig({ logger: context.debug }); + + if (config.tracking !== true) { + return; + } + + await sendStats("errors", { + version: cliVersion, + type, + errorMessage, + errorStack, + activityId, + instance: get(context, "instance.id") || "", + user: config.id }); } catch (e) { // Ignore errors @@ -77,6 +126,7 @@ const trackProject = async ({ cliVersion }) => { }; module.exports = { - trackProject, - trackComponent + trackComponent, + trackActivity, + trackError }; diff --git a/packages/tracking/package.json b/packages/tracking/package.json index 9757b003e4a..30a6a40fe51 100644 --- a/packages/tracking/package.json +++ b/packages/tracking/package.json @@ -5,6 +5,7 @@ "license": "MIT", "dependencies": { "load-json-file": "^6.2.0", + "lodash.get": "^4.4.0", "request": "^2.88.0" }, "publishConfig": {