Merge pull request #1410 from balena-io/oclif-mixpanel

Add missing oclif-based commands to mixpanel tracking
This commit is contained in:
Paulo Castro 2019-09-02 01:55:02 +01:00 committed by GitHub
commit 596d1bdc21
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
7 changed files with 75 additions and 13 deletions

View File

@ -4,7 +4,7 @@
"target": "es2017", "target": "es2017",
"strict": true, "strict": true,
"strictPropertyInitialization": false, "strictPropertyInitialization": false,
"noUnusedLocals": false, "noUnusedLocals": true,
"noUnusedParameters": true, "noUnusedParameters": true,
"preserveConstEnums": true, "preserveConstEnums": true,
"removeComments": true, "removeComments": true,

View File

@ -154,5 +154,17 @@ exports.run = (argv) ->
else else
capitanoExecuteAsync(cli) 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>"
# ("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) .catch(require('./errors').handleError)

View File

@ -19,7 +19,7 @@ import { Main } from '@oclif/command';
import { ExitError } from '@oclif/errors'; import { ExitError } from '@oclif/errors';
import { AppOptions } from './app'; import { AppOptions } from './app';
import { handleError } from './errors'; import { trackPromise } from './hooks/prerun/track';
class CustomMain extends Main { class CustomMain extends Main {
protected _helpOverride(): boolean { protected _helpOverride(): boolean {
@ -36,7 +36,7 @@ class CustomMain extends Main {
* oclif CLI entrypoint * oclif CLI entrypoint
*/ */
export function run(command: string[], options: AppOptions) { export function run(command: string[], options: AppOptions) {
return CustomMain.run(command).then( const runPromise = CustomMain.run(command).then(
() => { () => {
if (!options.noFlush) { if (!options.noFlush) {
return require('@oclif/command/flush'); 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) // oclif sometimes exits with ExitError code 0 (not an error)
if (error instanceof ExitError && error.oclif.exit === 0) { if (error instanceof ExitError && error.oclif.exit === 0) {
return; return;
} else {
throw error;
} }
handleError(error);
}, },
); );
return Promise.all([trackPromise, runPromise]).catch(
require('./errors').handleError,
);
} }

View File

@ -20,7 +20,7 @@ import { exitWithExpectedError } from './utils/patterns';
export interface AppOptions { export interface AppOptions {
// Prevent the default behaviour of flushing stdout after running a command // 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 * CLI entrypoint, but see also `bin/balena` and `bin/balena-dev` which
* call this function. * 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 // globalInit() must be called very early on (before other imports) because
// it sets up Sentry error reporting, global HTTP proxy settings, balena-sdk // it sets up Sentry error reporting, global HTTP proxy settings, balena-sdk
// shared options, and performs node version requirement checks. // shared options, and performs node version requirement checks.

View File

@ -16,7 +16,6 @@
*/ */
import BalenaSdk = require('balena-sdk'); import BalenaSdk = require('balena-sdk');
import Promise = require('bluebird'); import Promise = require('bluebird');
import * as Capitano from 'capitano';
import _ = require('lodash'); import _ = require('lodash');
import Mixpanel = require('mixpanel'); import Mixpanel = require('mixpanel');
import Raven = require('raven'); import Raven = require('raven');
@ -24,7 +23,6 @@ import Raven = require('raven');
import packageJSON = require('../package.json'); import packageJSON = require('../package.json');
const getBalenaSdk = _.once(() => BalenaSdk.fromSharedOptions()); const getBalenaSdk = _.once(() => BalenaSdk.fromSharedOptions());
const getMatchCommandAsync = Promise.promisify(Capitano.state.getMatchCommand);
const getMixpanel = _.once<any>(() => { const getMixpanel = _.once<any>(() => {
const settings = require('balena-settings-client'); const settings = require('balena-settings-client');
return Mixpanel.init('00000000000000000000000000000000', { return Mixpanel.init('00000000000000000000000000000000', {
@ -34,7 +32,7 @@ const getMixpanel = _.once<any>(() => {
}); });
}); });
export function trackCommand(capitanoCli: Capitano.Cli) { export function trackCommand(commandSignature: string) {
const balena = getBalenaSdk(); const balena = getBalenaSdk();
return Promise.props({ return Promise.props({
balenaUrl: balena.settings.get('balenaUrl'), balenaUrl: balena.settings.get('balenaUrl'),
@ -42,20 +40,20 @@ export function trackCommand(capitanoCli: Capitano.Cli) {
mixpanel: getMixpanel(), mixpanel: getMixpanel(),
}) })
.then(({ username, balenaUrl, mixpanel }) => { .then(({ username, balenaUrl, mixpanel }) => {
return getMatchCommandAsync(capitanoCli.command).then(command => { return Promise.try(() => {
Raven.mergeContext({ Raven.mergeContext({
user: { user: {
id: username, id: username,
username, username,
}, },
}); });
// `command.signature.toString()` results in a string like, for example: // commandSignature is a string like, for example:
// "push <applicationOrDevice>" // "push <applicationOrDevice>"
// That's literally so: "applicationOrDevice" is NOT replaced with // That's literally so: "applicationOrDevice" is NOT replaced with
// the actual application ID or device ID. The purpose is find out the // the actual application ID or device ID. The purpose is find out the
// most / least used command verbs, so we can focus our development // most / least used command verbs, so we can focus our development
// effort where it is most beneficial to end users. // effort where it is most beneficial to end users.
return mixpanel.track(`[CLI] ${command.signature.toString()}`, { return mixpanel.track(`[CLI] ${commandSignature}`, {
distinct_id: username, distinct_id: username,
version: packageJSON.version, version: packageJSON.version,
node: process.version, node: process.version,

44
lib/hooks/prerun/track.ts Normal file
View File

@ -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<void>;
/**
* 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;

View File

@ -31,6 +31,7 @@
"assets": [ "assets": [
"build/actions-oclif", "build/actions-oclif",
"build/auth/pages/*.ejs", "build/auth/pages/*.ejs",
"build/hooks",
"node_modules/resin-discoverable-services/services/**/*" "node_modules/resin-discoverable-services/services/**/*"
] ]
}, },
@ -71,6 +72,9 @@
"oclif": { "oclif": {
"bin": "balena", "bin": "balena",
"commands": "./build/actions-oclif", "commands": "./build/actions-oclif",
"hooks": {
"prerun": "./build/hooks/prerun/track"
},
"macos": { "macos": {
"identifier": "io.balena.cli", "identifier": "io.balena.cli",
"sign": "Developer ID Installer: Rulemotion Ltd (66H43P8FRG)" "sign": "Developer ID Installer: Rulemotion Ltd (66H43P8FRG)"