From fb1dce9dbbcb5778b7abbb46bec36f6d0adb08de Mon Sep 17 00:00:00 2001 From: Paulo Castro Date: Wed, 28 Aug 2019 02:14:19 +0100 Subject: [PATCH] Add missing oclif-based commands to mixpanel tracking Change-type: patch Signed-off-by: Paulo Castro --- automation/tsconfig.json | 2 +- lib/app-capitano.coffee | 14 ++++++++++++- lib/app-oclif.ts | 10 ++++++--- lib/app.ts | 4 ++-- lib/events.ts | 10 ++++----- lib/hooks/prerun/track.ts | 44 +++++++++++++++++++++++++++++++++++++++ package.json | 4 ++++ 7 files changed, 75 insertions(+), 13 deletions(-) create mode 100644 lib/hooks/prerun/track.ts diff --git a/automation/tsconfig.json b/automation/tsconfig.json index 919dd14a..83d916dc 100644 --- a/automation/tsconfig.json +++ b/automation/tsconfig.json @@ -4,7 +4,7 @@ "target": "es2017", "strict": true, "strictPropertyInitialization": false, - "noUnusedLocals": false, + "noUnusedLocals": true, "noUnusedParameters": true, "preserveConstEnums": true, "removeComments": true, diff --git a/lib/app-capitano.coffee b/lib/app-capitano.coffee index 91872370..a987fe29 100644 --- a/lib/app-capitano.coffee +++ b/lib/app-capitano.coffee @@ -154,5 +154,17 @@ exports.run = (argv) -> else capitanoExecuteAsync(cli) - Promise.all([events.trackCommand(cli), runCommand()]) + trackCommand = -> + getMatchCommandAsync = Promise.promisify(capitano.state.getMatchCommand) + getMatchCommandAsync(cli.command) + .then (command) -> + # cmdSignature is literally a string like, for example: + # "push " + # ("applicationOrDevice" is NOT replaced with its actual value) + # In case of failures like an inexistent or invalid command, + # command.signature.toString() returns '*' + cmdSignature = command.signature.toString() + events.trackCommand(cmdSignature) + + Promise.all([trackCommand(), runCommand()]) .catch(require('./errors').handleError) diff --git a/lib/app-oclif.ts b/lib/app-oclif.ts index 79163eee..98ff7d63 100644 --- a/lib/app-oclif.ts +++ b/lib/app-oclif.ts @@ -19,7 +19,7 @@ import { Main } from '@oclif/command'; import { ExitError } from '@oclif/errors'; import { AppOptions } from './app'; -import { handleError } from './errors'; +import { trackPromise } from './hooks/prerun/track'; class CustomMain extends Main { protected _helpOverride(): boolean { @@ -36,7 +36,7 @@ class CustomMain extends Main { * oclif CLI entrypoint */ export function run(command: string[], options: AppOptions) { - return CustomMain.run(command).then( + const runPromise = CustomMain.run(command).then( () => { if (!options.noFlush) { return require('@oclif/command/flush'); @@ -46,8 +46,12 @@ export function run(command: string[], options: AppOptions) { // oclif sometimes exits with ExitError code 0 (not an error) if (error instanceof ExitError && error.oclif.exit === 0) { return; + } else { + throw error; } - handleError(error); }, ); + return Promise.all([trackPromise, runPromise]).catch( + require('./errors').handleError, + ); } diff --git a/lib/app.ts b/lib/app.ts index fc75a655..7074f64a 100644 --- a/lib/app.ts +++ b/lib/app.ts @@ -20,7 +20,7 @@ import { exitWithExpectedError } from './utils/patterns'; export interface AppOptions { // Prevent the default behaviour of flushing stdout after running a command - noFlush: boolean; + noFlush?: boolean; } /** @@ -160,7 +160,7 @@ function isOclifCommand(argvSlice: string[]): [boolean, boolean] { * CLI entrypoint, but see also `bin/balena` and `bin/balena-dev` which * call this function. */ -export function run(cliArgs = process.argv, options: AppOptions): void { +export function run(cliArgs = process.argv, options: AppOptions = {}): void { // globalInit() must be called very early on (before other imports) because // it sets up Sentry error reporting, global HTTP proxy settings, balena-sdk // shared options, and performs node version requirement checks. diff --git a/lib/events.ts b/lib/events.ts index 3bb62635..4980a2c4 100644 --- a/lib/events.ts +++ b/lib/events.ts @@ -16,7 +16,6 @@ */ import BalenaSdk = require('balena-sdk'); import Promise = require('bluebird'); -import * as Capitano from 'capitano'; import _ = require('lodash'); import Mixpanel = require('mixpanel'); import Raven = require('raven'); @@ -24,7 +23,6 @@ import Raven = require('raven'); import packageJSON = require('../package.json'); const getBalenaSdk = _.once(() => BalenaSdk.fromSharedOptions()); -const getMatchCommandAsync = Promise.promisify(Capitano.state.getMatchCommand); const getMixpanel = _.once(() => { const settings = require('balena-settings-client'); return Mixpanel.init('00000000000000000000000000000000', { @@ -34,7 +32,7 @@ const getMixpanel = _.once(() => { }); }); -export function trackCommand(capitanoCli: Capitano.Cli) { +export function trackCommand(commandSignature: string) { const balena = getBalenaSdk(); return Promise.props({ balenaUrl: balena.settings.get('balenaUrl'), @@ -42,20 +40,20 @@ export function trackCommand(capitanoCli: Capitano.Cli) { mixpanel: getMixpanel(), }) .then(({ username, balenaUrl, mixpanel }) => { - return getMatchCommandAsync(capitanoCli.command).then(command => { + return Promise.try(() => { Raven.mergeContext({ user: { id: username, username, }, }); - // `command.signature.toString()` results in a string like, for example: + // commandSignature is a string like, for example: // "push " // That's literally so: "applicationOrDevice" is NOT replaced with // the actual application ID or device ID. The purpose is find out the // most / least used command verbs, so we can focus our development // effort where it is most beneficial to end users. - return mixpanel.track(`[CLI] ${command.signature.toString()}`, { + return mixpanel.track(`[CLI] ${commandSignature}`, { distinct_id: username, version: packageJSON.version, node: process.version, diff --git a/lib/hooks/prerun/track.ts b/lib/hooks/prerun/track.ts new file mode 100644 index 00000000..11722017 --- /dev/null +++ b/lib/hooks/prerun/track.ts @@ -0,0 +1,44 @@ +/** + * @license + * Copyright 2019 Balena Ltd. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import { Hook } from '@oclif/config'; + +// note: trackPromise is subject to a Bluebird.timeout, defined in events.ts +export let trackPromise: PromiseLike; + +/** + * This is an oclif 'prerun' hook. This hook runs after the command line is + * parsed by oclif, but before the command's run() function is called. + * See: https://oclif.io/docs/hooks + * + * This hook is used to track CLI command signatures with mixpanel. This + * is the oclif version of what is already done for Capitano commands. + * + * A command signature is something like "env add NAME [VALUE]". That's + * literally so: 'NAME' and 'VALUE' are NOT replaced with actual values. + */ +const hook: Hook<'prerun'> = async function(options) { + const events = await import('../../events'); + const usage: string | string[] | undefined = options.Command.usage; + const cmdSignature = + usage == null ? '*' : typeof usage === 'string' ? usage : usage.join(' '); + + // Intentionally do not await for the track promise here, in order to + // run the command tracking and the command itself in parallel. + trackPromise = events.trackCommand(cmdSignature); +}; + +export default hook; diff --git a/package.json b/package.json index 54d8816c..09ffc140 100644 --- a/package.json +++ b/package.json @@ -31,6 +31,7 @@ "assets": [ "build/actions-oclif", "build/auth/pages/*.ejs", + "build/hooks", "node_modules/resin-discoverable-services/services/**/*" ] }, @@ -71,6 +72,9 @@ "oclif": { "bin": "balena", "commands": "./build/actions-oclif", + "hooks": { + "prerun": "./build/hooks/prerun/track" + }, "macos": { "identifier": "io.balena.cli", "sign": "Developer ID Installer: Rulemotion Ltd (66H43P8FRG)"