diff --git a/packages/cli/package.json b/packages/cli/package.json index f369111372..8dfd665bd9 100644 --- a/packages/cli/package.json +++ b/packages/cli/package.json @@ -19,6 +19,7 @@ "inquirer": "^8.0.0", "lookpath": "^1.1.0", "pkg": "^4.4.9", + "posthog-node": "^1.0.7", "randomstring": "^1.1.5" }, "devDependencies": { diff --git a/packages/cli/src/analytics/Client.js b/packages/cli/src/analytics/Client.js new file mode 100644 index 0000000000..d97fb1abfa --- /dev/null +++ b/packages/cli/src/analytics/Client.js @@ -0,0 +1,38 @@ +const PostHog = require("posthog-node") +const { + BUDIBASE_POSTHOG_URL, + BUDIBASE_POSTHOG_TOKEN, + AnalyticsEvents, +} = require("../constants") +const ConfigManager = require("../structures/ConfigManager") + +class AnalyticsClient { + constructor() { + this.client = new PostHog(BUDIBASE_POSTHOG_TOKEN, { + host: BUDIBASE_POSTHOG_URL, + }) + this.configManager = new ConfigManager() + } + + capture(event) { + if (this.manager.config.analyticsDisabled) return + + this.client.capture(event) + } + + enable() { + this.configManager.removeKey("analyticsDisabled") + this.client.capture({ event: AnalyticsEvents.OptIn, distinctId: "cli" }) + } + + disable() { + this.client.capture({ event: AnalyticsEvents.OptOut, distinctId: "cli" }) + this.configManager.setValue("analyticsDisabled", true) + } + + status() { + return this.configManager.config.analyticsDisabled ? "disabled" : "enabled" + } +} + +module.exports = AnalyticsClient diff --git a/packages/cli/src/analytics/index.js b/packages/cli/src/analytics/index.js new file mode 100644 index 0000000000..3d8ea8c5a7 --- /dev/null +++ b/packages/cli/src/analytics/index.js @@ -0,0 +1,63 @@ +const Command = require("../structures/Command") +const { CommandWords } = require("../constants") +const { success, error } = require("../utils") +const AnalyticsClient = require("./Client") + +const client = new AnalyticsClient() + +async function optOut() { + try { + // opt them out + client.disable() + console.log( + success( + "Successfully opted out of budibase analytics. You can opt in at any time by running 'budi analytics opt-in'" + ) + ) + } catch (err) { + console.log( + error( + "Error opting out of budibase analytics. Please try again later.", + err + ) + ) + } +} + +async function optIn() { + try { + // opt them in + client.enable() + console.log( + success( + "Successfully opted in to budibase analytics. Thank you for helping us make budibase better!" + ) + ) + } catch (err) { + console.log( + error("Error opting in to budibase analytics. Please try again later.") + ) + } +} + +async function status() { + try { + console.log(success(`Budibase analytics ${client.status()}`)) + } catch (err) { + console.log( + error("Error fetching analytics status. Please try again later.") + ) + } +} + +const command = new Command(`${CommandWords.ANALYTICS}`) + .addHelp("Control the analytics you send to budibase.") + .addSubOption("--optin", "Opt in to sending analytics to budibase", optIn) + .addSubOption("--optout", "Opt out of sending analytics to budibase.", optOut) + .addSubOption( + "--status", + "Check whether you are currently opted in to budibase analytics.", + status + ) + +exports.command = command diff --git a/packages/cli/src/constants.js b/packages/cli/src/constants.js index c8da56fa5c..a5d24514b8 100644 --- a/packages/cli/src/constants.js +++ b/packages/cli/src/constants.js @@ -1,5 +1,6 @@ exports.CommandWords = { HOSTING: "hosting", + ANALYTICS: "analytics", HELP: "help", } @@ -7,3 +8,12 @@ exports.InitTypes = { QUICK: "quick", DIGITAL_OCEAN: "do", } + +exports.AnalyticsEvents = { + OptOut: "analytics_opt_out", + OptIn: "analytics_opt_in", + SelfHostInit: "hosting_init", +} + +exports.BUDIBASE_POSTHOG_URL = "https://posthog.budi.live" +exports.BUDIBASE_POSTHOG_TOKEN = process.env.BUDIBASE_POSTHOG_TOKEN diff --git a/packages/cli/src/hosting/index.js b/packages/cli/src/hosting/index.js index 710397f301..60d9f13e80 100644 --- a/packages/cli/src/hosting/index.js +++ b/packages/cli/src/hosting/index.js @@ -1,5 +1,5 @@ const Command = require("../structures/Command") -const { CommandWords, InitTypes } = require("../constants") +const { CommandWords, InitTypes, AnalyticsEvents } = require("../constants") const { lookpath } = require("lookpath") const { downloadFile, @@ -13,6 +13,7 @@ const fs = require("fs") const compose = require("docker-compose") const makeEnv = require("./makeEnv") const axios = require("axios") +const AnalyticsClient = require("../analytics/Client") const BUDIBASE_SERVICES = ["app-service", "worker-service"] const ERROR_FILE = "docker-error.log" @@ -22,6 +23,8 @@ const FILE_URLS = [ ] const DO_USER_DATA_URL = "http://169.254.169.254/metadata/v1/user-data" +const client = new AnalyticsClient() + async function downloadFiles() { const promises = [] for (let url of FILE_URLS) { @@ -70,6 +73,13 @@ async function init(type) { return } } + client.capture({ + distinctId: "cli", + event: AnalyticsEvents.SelfHostInit, + properties: { + type, + }, + }) await downloadFiles() const config = isQuick ? makeEnv.QUICK_CONFIG : {} if (type === InitTypes.DIGITAL_OCEAN) { diff --git a/packages/cli/src/options.js b/packages/cli/src/options.js index 0d0d65b723..1cafbf9269 100644 --- a/packages/cli/src/options.js +++ b/packages/cli/src/options.js @@ -1,5 +1,6 @@ +const analytics = require("./analytics") const hosting = require("./hosting") exports.getCommands = () => { - return [hosting.command] + return [hosting.command, analytics.command] } diff --git a/packages/cli/src/structures/ConfigManager.js b/packages/cli/src/structures/ConfigManager.js new file mode 100644 index 0000000000..04b7875b57 --- /dev/null +++ b/packages/cli/src/structures/ConfigManager.js @@ -0,0 +1,50 @@ +const fs = require("fs") +const path = require("path") +const os = require("os") +const { error } = require("../utils") + +class ConfigManager { + constructor() { + this.path = path.join(os.homedir(), ".budibase.json") + if (!fs.existsSync(this.path)) { + fs.writeFileSync(this.path, "{}") + } + } + + get config() { + try { + return JSON.parse(fs.readFileSync(this.path, "utf8")) + } catch (err) { + console.log( + error( + "Error parsing configuration file. Please check your .budibase.json is valid." + ) + ) + return {} + } + } + + set config(json) { + fs.writeFileSync(this.path, JSON.stringify(json)) + } + + getValue(key) { + return this.config[key] + } + + setValue(key, value) { + const updated = { + ...this.config, + [key]: value, + } + this.config = updated + } + + removeKey(key) { + const updated = { ...this.config } + delete updated[key] + this.config = updated + } +} + +module.exports = ConfigManager diff --git a/packages/cli/src/utils.js b/packages/cli/src/utils.js index 05bb8d4991..f61636389d 100644 --- a/packages/cli/src/utils.js +++ b/packages/cli/src/utils.js @@ -42,7 +42,7 @@ exports.info = info => { } exports.logErrorToFile = (file, error) => { - fs.writeFileSync(path.resolve(`./${file}`), `Budiase Error\n${error}`) + fs.writeFileSync(path.resolve(`./${file}`), `Budibase Error\n${error}`) } exports.parseEnv = env => { diff --git a/packages/cli/yarn.lock b/packages/cli/yarn.lock index 298fb78061..7bb4eaa367 100644 --- a/packages/cli/yarn.lock +++ b/packages/cli/yarn.lock @@ -181,6 +181,13 @@ aws4@^1.8.0: resolved "https://registry.yarnpkg.com/aws4/-/aws4-1.11.0.tgz#d61f46d83b2519250e2784daf5b09479a8b41c59" integrity sha512-xh1Rl34h6Fi1DC2WWKfxUTVqRsNnr6LsKz2+hfwDxQJWmrx8+c7ylaqBMcHfl1U1r2dsifOvKX3LQuLNZ+XSvA== +axios-retry@^3.1.9: + version "3.1.9" + resolved "https://registry.yarnpkg.com/axios-retry/-/axios-retry-3.1.9.tgz#6c30fc9aeb4519aebaec758b90ef56fa03fe72e8" + integrity sha512-NFCoNIHq8lYkJa6ku4m+V1837TP6lCa7n79Iuf8/AqATAHYB0ISaAS1eyIenDOfHOLtym34W65Sjke2xjg2fsA== + dependencies: + is-retry-allowed "^1.1.0" + axios@^0.21.1: version "0.21.1" resolved "https://registry.yarnpkg.com/axios/-/axios-0.21.1.tgz#22563481962f4d6bde9a76d516ef0e5d3c09b2b8" @@ -260,6 +267,11 @@ chardet@^0.7.0: resolved "https://registry.yarnpkg.com/chardet/-/chardet-0.7.0.tgz#90094849f0937f2eedc2425d0d28a9e5f0cbad9e" integrity sha512-mT8iDcrh03qDGRRmoA2hmBJnxpllMR+0/0qlzjqZES6NdiWDcZkCNAk4rPFZ9Q85r27unkiNNg8ZOiwZXBHwcA== +charenc@0.0.2: + version "0.0.2" + resolved "https://registry.yarnpkg.com/charenc/-/charenc-0.0.2.tgz#c0a1d2f3a7092e03774bfa83f14c0fc5790a8667" + integrity sha1-wKHS86cJLgN3S/qD8UwPxXkKhmc= + cli-cursor@^3.1.0: version "3.1.0" resolved "https://registry.yarnpkg.com/cli-cursor/-/cli-cursor-3.1.0.tgz#264305a7ae490d1d03bf0c9ba7c925d1753af307" @@ -308,6 +320,11 @@ commander@^7.1.0: resolved "https://registry.yarnpkg.com/commander/-/commander-7.1.0.tgz#f2eaecf131f10e36e07d894698226e36ae0eb5ff" integrity sha512-pRxBna3MJe6HKnBGsDyMv8ETbptw3axEdYHoqNh7gu5oDcew8fs0xnivZGm06Ogk8zGAJ9VX+OPEr2GXEQK4dg== +component-type@^1.2.1: + version "1.2.1" + resolved "https://registry.yarnpkg.com/component-type/-/component-type-1.2.1.tgz#8a47901700238e4fc32269771230226f24b415a9" + integrity sha1-ikeQFwAjjk/DIml3EjAibyS0Fak= + concat-map@0.0.1: version "0.0.1" resolved "https://registry.yarnpkg.com/concat-map/-/concat-map-0.0.1.tgz#d8a96bd77fd68df7793a73036a3ba0d5405d477b" @@ -327,6 +344,11 @@ cross-spawn@^7.0.2: shebang-command "^2.0.0" which "^2.0.1" +crypt@0.0.2: + version "0.0.2" + resolved "https://registry.yarnpkg.com/crypt/-/crypt-0.0.2.tgz#88d7ff7ec0dfb86f713dc87bbb42d044d3e6c41b" + integrity sha1-iNf/fsDfuG9xPch7u0LQRNPmxBs= + dashdash@^1.12.0: version "1.14.1" resolved "https://registry.yarnpkg.com/dashdash/-/dashdash-1.14.1.tgz#853cfa0f7cbe2fed5de20326b8dd581035f6e2f0" @@ -832,6 +854,11 @@ into-stream@^5.1.1: from2 "^2.3.0" p-is-promise "^3.0.0" +is-buffer@~1.1.6: + version "1.1.6" + resolved "https://registry.yarnpkg.com/is-buffer/-/is-buffer-1.1.6.tgz#efaa2ea9daa0d7ab2ea13a97b2b8ad51fefbe8be" + integrity sha512-NcdALwpXkTm5Zvvbk7owOUSvVvBKDgKP5/ewfXEznmQFfs4ZRmanOeKBTjRVjka3QFoN6XJ+9F3USqfHqTaU5w== + is-core-module@^2.2.0: version "2.2.0" resolved "https://registry.yarnpkg.com/is-core-module/-/is-core-module-2.2.0.tgz#97037ef3d52224d85163f5597b2b63d9afed981a" @@ -861,6 +888,11 @@ is-number@^7.0.0: resolved "https://registry.yarnpkg.com/is-number/-/is-number-7.0.0.tgz#7535345b896734d5f80c4d06c50955527a14f12b" integrity sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng== +is-retry-allowed@^1.1.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/is-retry-allowed/-/is-retry-allowed-1.2.0.tgz#d778488bd0a4666a3be8a1482b9f2baafedea8b4" + integrity sha512-RUbUeKwvm3XG2VYamhJL1xFktgjvPzL0Hq8C+6yrWIswDy3BIXGqCxhxkc30N9jqK311gVU137K8Ei55/zVJRg== + is-typedarray@~1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/is-typedarray/-/is-typedarray-1.0.0.tgz#e479c80858df0c1b11ddda6940f96011fcda4a9a" @@ -881,6 +913,11 @@ isstream@~0.1.2: resolved "https://registry.yarnpkg.com/isstream/-/isstream-0.1.2.tgz#47e63f7af55afa6f92e1500e690eb8b8529c099a" integrity sha1-R+Y/evVa+m+S4VAOaQ64uFKcCZo= +join-component@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/join-component/-/join-component-1.1.0.tgz#b8417b750661a392bee2c2537c68b2a9d4977cd5" + integrity sha1-uEF7dQZho5K+4sJTfGiyqdSXfNU= + js-tokens@^4.0.0: version "4.0.0" resolved "https://registry.yarnpkg.com/js-tokens/-/js-tokens-4.0.0.tgz#19203fb59991df98e3a287050d4647cdeaf32499" @@ -974,6 +1011,15 @@ lru-cache@^6.0.0: dependencies: yallist "^4.0.0" +md5@^2.3.0: + version "2.3.0" + resolved "https://registry.yarnpkg.com/md5/-/md5-2.3.0.tgz#c3da9a6aae3a30b46b7b0c349b87b110dc3bda4f" + integrity sha512-T1GITYmFaKuO91vxyoQMFETst+O71VUPEU3ze5GNzDm0OWdP8v1ziTaAEPUr/3kLsY3Sftgz242A1SetQiDL7g== + dependencies: + charenc "0.0.2" + crypt "0.0.2" + is-buffer "~1.1.6" + merge2@^1.3.0: version "1.4.1" resolved "https://registry.yarnpkg.com/merge2/-/merge2-1.4.1.tgz#4368892f885e907455a6fd7dc55c0c9d404990ae" @@ -1028,6 +1074,11 @@ ms@2.1.2: resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.2.tgz#d09d1f357b443f493382a8eb3ccd183872ae6009" integrity sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w== +ms@^2.1.3: + version "2.1.3" + resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.3.tgz#574c8138ce1d2b5861f0b44579dbadd60c6615b2" + integrity sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA== + multistream@^2.1.1: version "2.1.1" resolved "https://registry.yarnpkg.com/multistream/-/multistream-2.1.1.tgz#629d3a29bd76623489980d04519a2c365948148c" @@ -1172,6 +1223,20 @@ pkg@^4.4.9: resolve "^1.15.1" stream-meter "^1.0.4" +posthog-node@^1.0.7: + version "1.0.7" + resolved "https://registry.yarnpkg.com/posthog-node/-/posthog-node-1.0.7.tgz#a7a9525eebff23312117e57cff3ddac82afb2262" + integrity sha512-KTCwyU+PU1eAQtjy5ZSJ47mrxv2d/mMkxo+vvV5c+YqfE4mBAY1UPEPMv1nElb5Vq0UnxvyQXaUnOn8d8Xr6Eg== + dependencies: + axios "^0.21.1" + axios-retry "^3.1.9" + component-type "^1.2.1" + join-component "^1.1.0" + md5 "^2.3.0" + ms "^2.1.3" + remove-trailing-slash "^0.1.1" + uuid "^8.3.2" + prelude-ls@^1.2.1: version "1.2.1" resolved "https://registry.yarnpkg.com/prelude-ls/-/prelude-ls-1.2.1.tgz#debc6489d7a6e6b0e7611888cec880337d316396" @@ -1242,6 +1307,11 @@ regexpp@^3.1.0: resolved "https://registry.yarnpkg.com/regexpp/-/regexpp-3.1.0.tgz#206d0ad0a5648cffbdb8ae46438f3dc51c9f78e2" integrity sha512-ZOIzd8yVsQQA7j8GCSlPGXwg5PfmA1mrq0JP4nGhh54LaKN3xdai/vHUDu74pKwV8OxseMS65u2NImosQcSD0Q== +remove-trailing-slash@^0.1.1: + version "0.1.1" + resolved "https://registry.yarnpkg.com/remove-trailing-slash/-/remove-trailing-slash-0.1.1.tgz#be2285a59f39c74d1bce4f825950061915e3780d" + integrity sha512-o4S4Qh6L2jpnCy83ysZDau+VORNvnFw07CKSAymkd6ICNVEPisMyzlc00KlvvicsxKck94SEwhDnMNdICzO+tA== + request-progress@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/request-progress/-/request-progress-3.0.0.tgz#4ca754081c7fec63f505e4faa825aa06cd669dbe" @@ -1593,6 +1663,11 @@ uuid@^3.3.2: resolved "https://registry.yarnpkg.com/uuid/-/uuid-3.4.0.tgz#b23e4358afa8a202fe7a100af1f5f883f02007ee" integrity sha512-HjSDRw6gZE5JMggctHBcjVak08+KEVhSIiDzFnT9S9aegmp85S/bReBVTb4QTFaRNptJ9kuYaNhnbNEOkbKb/A== +uuid@^8.3.2: + version "8.3.2" + resolved "https://registry.yarnpkg.com/uuid/-/uuid-8.3.2.tgz#80d5b5ced271bb9af6c445f21a1a04c606cefbe2" + integrity sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg== + v8-compile-cache@^2.0.3: version "2.3.0" resolved "https://registry.yarnpkg.com/v8-compile-cache/-/v8-compile-cache-2.3.0.tgz#2de19618c66dc247dcfb6f99338035d8245a2cee"