feat: medusa-telemetry (#328)

* feat: adds a telemetry package to collect anonymous usage data

* fix: update telemetry host

* fix: adds medusa telemetry --disable

* fix: add tracking of link,login,new

* fix: interactively collect db credentials

* fix: require seed file

* fix: removes tracking from reporter
This commit is contained in:
Sebastian Rindom
2021-08-05 12:23:05 +02:00
committed by GitHub
parent f07cc0fa40
commit cfe19f7f9d
29 changed files with 6509 additions and 58 deletions
+1
View File
@@ -52,6 +52,7 @@
"joi-objectid": "^3.0.1",
"meant": "^1.0.1",
"medusa-core-utils": "^0.1.27",
"medusa-telemetry": "^0.0.1",
"netrc-parser": "^3.1.6",
"open": "^8.0.6",
"ora": "^5.4.1",
+6 -12
View File
@@ -3,11 +3,14 @@ const inquirer = require("inquirer")
const open = require("open")
const execa = require("execa")
const resolveCwd = require(`resolve-cwd`)
const { track } = require("medusa-telemetry")
const { getToken } = require("../util/token-store")
const logger = require("../reporter").default
module.exports = {
link: async argv => {
track("CLI_LINK", { args: argv })
const port = process.env.PORT || 9000
const appHost =
process.env.MEDUSA_APP_HOST || "https://app.medusa-commerce.com"
@@ -15,18 +18,6 @@ module.exports = {
const apiHost =
process.env.MEDUSA_API_HOST || "https://api.medusa-commerce.com"
function resolveLocalCommand(command) {
try {
const cmdPath = resolveCwd.silent(
`@medusajs/medusa/dist/commands/${command}`
)
return require(cmdPath).default
} catch (err) {
console.log("Could not find local user command.")
process.exit(1)
}
}
// Checks if there is already a token from a previous log in; this is
// necessary to redirect the customer to the page where local linking is
// done
@@ -88,6 +79,7 @@ module.exports = {
}
logger.success(linkActivity, "Local project linked")
track("CLI_LINK_COMPLETED")
console.log()
console.log(
@@ -125,6 +117,8 @@ module.exports = {
`Could not open browser go to: ${appHost}/local-link?lurl=http://localhost:9000&ltoken=${auth.user.id}`
)
})
track("CLI_LINK_BROWSER_OPENED")
})
if (argv.develop) {
@@ -1,6 +1,7 @@
const axios = require("axios").default
const open = require("open")
const inquirer = require("inquirer")
const { track } = require("medusa-telemetry")
const logger = require("../reporter").default
const { setToken } = require("../util/token-store")
@@ -12,6 +13,7 @@ const { setToken } = require("../util/token-store")
*/
module.exports = {
login: async _ => {
track("CLI_LOGIN")
const apiHost =
process.env.MEDUSA_API_HOST || "https://api.medusa-commerce.com"
@@ -80,9 +82,11 @@ module.exports = {
})
if (user) {
track("CLI_LOGIN_SUCCEEDED")
logger.success(spinner, "Log in succeeded.")
setToken(auth.password)
} else {
track("CLI_LOGIN_FAILED")
logger.failure(spinner, "Log in failed.")
}
},
+165 -22
View File
@@ -10,8 +10,11 @@ import hostedGitInfo from "hosted-git-info"
import isValid from "is-valid-path"
import sysPath from "path"
import prompts from "prompts"
import { Pool } from "pg"
import url from "url"
import { createDatabase } from "pg-god"
import { track } from "medusa-telemetry"
import inquirer from "inquirer"
import reporter from "../reporter"
import { getPackageManager, setPackageManager } from "../util/package-manager"
@@ -76,7 +79,7 @@ const createInitialGitCommit = async (rootPath, starterUrl) => {
// use execSync instead of spawn to handle git clients using
// pgp signatures (with password)
try {
execSync(`git commit -m "Initial commit from gatsby: (${starterUrl})"`, {
execSync(`git commit -m "Initial commit from medusa: (${starterUrl})"`, {
cwd: rootPath,
})
} catch {
@@ -91,6 +94,8 @@ const install = async rootPath => {
const prevDir = process.cwd()
reporter.info(`Installing packages...`)
console.log() // Add some space
process.chdir(rootPath)
const npmConfigUserAgent = process.env.npm_config_user_agent
@@ -145,6 +150,7 @@ const copy = async (starterPath, rootPath) => {
await fs.copy(starterPath, rootPath, { filter: ignored })
reporter.success(copyActivity, `Created starter directory layout`)
console.log() // Add some space
await install(rootPath)
@@ -242,8 +248,122 @@ const defaultDBCreds = {
host: "localhost",
}
const verifyPgCreds = async creds => {
const pool = new Pool(creds)
return new Promise((resolve, reject) => {
pool.query("SELECT NOW()", (err, res) => {
pool.end()
if (err) {
reject(err)
} else {
resolve(res)
}
})
})
}
const interactiveDbCreds = async (dbName, dbCreds = {}) => {
const credentials = Object.assign({}, defaultDBCreds, dbCreds)
let collecting = true
while (collecting) {
const result = await inquirer
.prompt([
{
type: "list",
name: "continueWithDefault",
message: `
Will attempt to setup database "${dbName}" with credentials:
user: ${credentials.user}
password: ***
database: ${credentials.database}
port: ${credentials.port}
host: ${credentials.host}
Do you wish to continue with these credentials?
`,
choices: [`Continue`, `Change credentials`, `Skip database setup`],
},
{
type: "input",
when: ({ continueWithDefault }) =>
continueWithDefault === `Change credentials`,
name: "user",
default: credentials.user,
message: `DB user`,
},
{
type: "password",
when: ({ continueWithDefault }) =>
continueWithDefault === `Change credentials`,
name: "password",
default: credentials.password,
message: `DB password`,
},
{
type: "number",
when: ({ continueWithDefault }) =>
continueWithDefault === `Change credentials`,
name: "port",
default: credentials.port,
message: `DB port`,
},
{
type: "input",
when: ({ continueWithDefault }) =>
continueWithDefault === `Change credentials`,
name: "host",
default: credentials.host,
message: `DB host`,
},
{
type: "input",
when: ({ continueWithDefault }) =>
continueWithDefault === `Change credentials`,
name: "database",
default: credentials.database,
message: `DB database`,
},
])
.then(async answers => {
const collectedCreds = Object.assign({}, credentials, {
user: answers.user,
password: answers.password,
host: answers.host,
port: answers.port,
database: answers.database,
})
switch (answers.continueWithDefault) {
case "Continue": {
const done = await verifyPgCreds(credentials).catch(_ => false)
if (done) {
return credentials
}
return false
}
case "Change credentials": {
const done = await verifyPgCreds(collectedCreds).catch(_ => false)
if (done) {
return collectedCreds
}
return false
}
default:
return null
}
})
if (result !== false) {
return result
}
console.log("\n\nCould not verify DB credentials - please try again\n\n")
}
}
const setupDB = async (dbName, dbCreds = {}) => {
const credentials = Object.assign(defaultDBCreds, dbCreds)
const credentials = Object.assign({}, defaultDBCreds, dbCreds)
const dbActivity = reporter.activity(`Setting up database "${dbName}"...`)
await createDatabase(
@@ -257,7 +377,7 @@ const setupDB = async (dbName, dbCreds = {}) => {
reporter.success(dbActivity, `Created database "${dbName}"`)
})
.catch(err => {
if ((err.name = "PDG_ERR::DuplicateDatabase")) {
if (err.name === "PDG_ERR::DuplicateDatabase") {
reporter.success(
dbActivity,
`Database ${dbName} already exists; skipping setup`
@@ -273,23 +393,26 @@ const setupDB = async (dbName, dbCreds = {}) => {
}
const setupEnvVars = async (rootPath, dbName, dbCreds = {}) => {
const credentials = Object.assign(defaultDBCreds, dbCreds)
const credentials = Object.assign({}, defaultDBCreds, dbCreds)
let dbUrl = ""
if (
credentials.user !== defaultDBCreds.user ||
credentials.password !== defaultDBCreds.password
) {
dbUrl = `postgres://${credentials.user}:${credentials.password}@${credentials.host}:${credentials.port}/${dbName}`
} else {
dbUrl = `postgres://${credentials.host}:${credentials.port}/${dbName}`
}
const templatePath = sysPath.join(rootPath, ".env.template")
const destination = sysPath.join(rootPath, ".env")
if (existsSync(templatePath)) {
fs.renameSync(templatePath, destination)
fs.appendFileSync(
destination,
`DATABASE_URL=postgres://${credentials.user}:${credentials.password}@${credentials.host}:${credentials.port}/${dbName}\n`
)
} else {
reporter.info(`No .env.template found. Creating .env.`)
fs.appendFileSync(
destination,
`DATABASE_URL=postgres://${credentials.user}:${credentials.password}@${credentials.host}:${credentials.port}/${dbName}\n`
)
}
fs.appendFileSync(destination, `DATABASE_URL=${dbUrl}\n`)
}
const runMigrations = async rootPath => {
@@ -354,6 +477,8 @@ const attemptSeed = async rootPath => {
* Main function that clones or copies the starter.
*/
export const newStarter = async args => {
track("CLI_NEW")
const {
starter,
root,
@@ -361,6 +486,7 @@ export const newStarter = async args => {
skipMigrations,
skipEnv,
seed,
useDefaults,
dbUser,
dbDatabase,
dbPass,
@@ -430,21 +556,38 @@ export const newStarter = async args => {
await copy(starterPath, rootPath)
}
if (!skipDb) {
await setupDB(root, dbCredentials)
track("CLI_NEW_LAYOUT_COMPLETED")
let creds = dbCredentials
if (!useDefaults && !skipDb && !skipEnv) {
creds = await interactiveDbCreds(root, dbCredentials)
}
if (!skipEnv) {
await setupEnvVars(rootPath, root, dbCredentials)
}
if (creds === null) {
reporter.info("Skipping automatic database setup")
} else {
if (!skipDb) {
track("CLI_NEW_SETUP_DB")
await setupDB(root, creds)
}
if (!skipMigrations) {
await runMigrations(rootPath)
}
if (!skipEnv) {
track("CLI_NEW_SETUP_ENV")
await setupEnvVars(rootPath, root, creds)
}
if (seed) {
await attemptSeed(rootPath)
if (!skipMigrations) {
track("CLI_NEW_RUN_MIGRATIONS")
await runMigrations(rootPath)
}
if (seed) {
track("CLI_NEW_SEED_DB")
await attemptSeed(rootPath)
}
}
successMessage(rootPath)
track("CLI_NEW_SUCCEEDED")
}
+33 -3
View File
@@ -2,10 +2,12 @@ const path = require(`path`)
const resolveCwd = require(`resolve-cwd`)
const yargs = require(`yargs`)
const existsSync = require(`fs-exists-cached`).sync
const { setTelemetryEnabled } = require("medusa-telemetry")
const { getLocalMedusaVersion } = require(`./util/version`)
const { didYouMean } = require(`./did-you-mean`)
const reporter = require("./reporter").default
const { newStarter } = require("./commands/new")
const { whoami } = require("./commands/whoami")
const { login } = require("./commands/login")
@@ -61,7 +63,6 @@ function buildLocalCommands(cli, isLocalProject) {
const localCmd = resolveLocalCommand(command)
const args = { ...argv, ...projectInfo, useYarn }
// report.verbose(`running command: ${command}`)
return handler ? handler(args, localCmd) : localCmd(args)
}
}
@@ -75,6 +76,12 @@ function buildLocalCommands(cli, isLocalProject) {
describe: `If flag is set the command will attempt to seed the database after setup.`,
default: false,
})
.option(`y`, {
type: `boolean`,
alias: "useDefaults",
describe: `If flag is set the command will not interactively collect database credentials`,
default: false,
})
.option(`skip-db`, {
type: `boolean`,
describe: `If flag is set the command will not attempt to complete database setup`,
@@ -118,6 +125,28 @@ function buildLocalCommands(cli, isLocalProject) {
desc: `Create a new Medusa project.`,
handler: handlerP(newStarter),
})
.command({
command: `telemetry`,
describe: `Enable or disable collection of anonymous usage data.`,
builder: yargs =>
yargs
.option(`enable`, {
type: `boolean`,
description: `Enable telemetry (default)`,
})
.option(`disable`, {
type: `boolean`,
description: `Disable telemetry`,
}),
handler: handlerP(({ enable, disable }) => {
const enabled = Boolean(enable) || !disable
setTelemetryEnabled(enabled)
reporter.info(
`Telemetry collection ${enabled ? `enabled` : `disabled`}`
)
}),
})
.command({
command: `seed`,
desc: `Migrates and populates the database with the provided file.`,
@@ -126,6 +155,7 @@ function buildLocalCommands(cli, isLocalProject) {
alias: `seed-file`,
type: `string`,
describe: `Path to the file where the seed is defined.`,
required: true,
}).option(`m`, {
alias: `migrate`,
type: `boolean`,
@@ -364,8 +394,8 @@ module.exports = argv => {
const suggestion = arg ? didYouMean(arg, availableCommands) : ``
cli.showHelp()
// report.log(suggestion)
// report.log(msg)
reporter.info(suggestion)
reporter.info(msg)
})
.parse(argv.slice(2))
}
+37 -5
View File
@@ -2,6 +2,7 @@ import stackTrace from "stack-trace"
import { ulid } from "ulid"
import winston from "winston"
import ora from "ora"
import { track } from "medusa-telemetry"
const LOG_LEVEL = process.env.LOG_LEVEL || "silly"
const NODE_ENV = process.env.NODE_ENV || "development"
@@ -41,11 +42,17 @@ export class Reporter {
this.ora_ = activityLogger
}
panic = error => {
panic = data => {
this.loggerInstance_.log({
level: "error",
details: error,
details: data,
message: data.error && data.error.message,
})
track("PANIC_ERROR_REACHED", {
id: data.id,
})
process.exit(1)
}
@@ -83,13 +90,14 @@ export class Reporter {
* @returns {string} the id of the activity; this should be passed to do
* further operations on the activity such as success, failure, progress.
*/
activity = message => {
activity = (message, config = {}) => {
const id = ulid()
if (NODE_ENV === "development" && this.shouldLog("info")) {
const activity = this.ora_(message).start()
this.activities_[id] = {
activity,
config,
start: Date.now(),
}
@@ -97,10 +105,12 @@ export class Reporter {
} else {
this.activities_[id] = {
start: Date.now(),
config,
}
this.loggerInstance_.log({
activity_id: id,
level: "info",
config,
message,
})
@@ -166,15 +176,16 @@ export class Reporter {
* at the error level.
* @param {string} activityId - the id of the activity as returned by activity
* @param {string} message - the message to log
* @returns {object} data about the activity
*/
failure = (activityId, message) => {
const time = Date.now()
const toLog = {
level: "error",
message,
}
if (typeof activityId === "string" && this.activities_[activityId]) {
const time = Date.now()
const activity = this.activities_[activityId]
if (activity.activity) {
activity.activity.fail(`${message} ${time - activity.start}`)
@@ -186,6 +197,16 @@ export class Reporter {
} else {
this.loggerInstance_.log(toLog)
}
if (this.activities_[activityId]) {
const activity = this.activities_[activityId]
return {
...activity,
duration: time - activity.start,
}
}
return null
}
/**
@@ -194,15 +215,16 @@ export class Reporter {
* at the info level.
* @param {string} activityId - the id of the activity as returned by activity
* @param {string} message - the message to log
* @returns {object} data about the activity
*/
success = (activityId, message) => {
const time = Date.now()
const toLog = {
level: "info",
message,
}
if (typeof activityId === "string" && this.activities_[activityId]) {
const time = Date.now()
const activity = this.activities_[activityId]
if (activity.activity) {
activity.activity.succeed(`${message} ${time - activity.start}ms`)
@@ -214,6 +236,16 @@ export class Reporter {
} else {
this.loggerInstance_.log(toLog)
}
if (this.activities_[activityId]) {
const activity = this.activities_[activityId]
return {
...activity,
duration: time - activity.start,
}
}
return null
}
/**