Skip to content

Commit 97b1db8

Browse files
committed
chore(telemetry): better pro telemetry
1 parent 8d77945 commit 97b1db8

10 files changed

Lines changed: 309 additions & 70 deletions

File tree

packages/cli-utils/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,7 @@
4848
"ci-info": "^1.0.0",
4949
"cross-spawn": "^4.0.2",
5050
"dargs": "^5.1.0",
51+
"dev-null": "^0.1.1",
5152
"inquirer": "^3.0.6",
5253
"leek": "0.0.24",
5354
"lodash": "^4.17.4",

packages/cli-utils/src/definitions.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ export interface LoggerOptions {
2121
}
2222

2323
export interface ILogger {
24-
level: string;
24+
level: LogLevel;
2525
prefix: string | (() => string);
2626
stream: NodeJS.WritableStream;
2727

@@ -308,6 +308,7 @@ export interface IShell {
308308
export interface ITelemetry {
309309
sendCommand(command: string, args: string[]): Promise<void>;
310310
sendError(error: any, type: string): Promise<void>;
311+
resetToken(): Promise<void>;
311312
}
312313

313314
export interface ConfigFile {

packages/cli-utils/src/index.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -182,7 +182,7 @@ export async function generateIonicEnvironment(plugin: RootPlugin, pargv: string
182182
const client = new Client(configData.urls.api);
183183
const session = configData.backend === BACKEND_LEGACY ? new CloudSession(config, project, client) : new ProSession(config, project, client);
184184
const hooks = new HookEngine();
185-
const telemetry = new Telemetry(config, plugin.version);
185+
const telemetry = new Telemetry({ config, client, plugin, project, session });
186186
const shell = new Shell(tasks, log);
187187

188188
registerHooks(hooks);
Lines changed: 109 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,109 @@
1+
import { CommandMetadata } from '../';
2+
import { Command } from '../command';
3+
4+
describe('@ionic/cli-utils Command', () => {
5+
6+
@CommandMetadata({
7+
name: 'foo',
8+
type: 'global',
9+
description: '',
10+
})
11+
class FooCommand extends Command {}
12+
13+
@CommandMetadata({
14+
name: 'bar',
15+
type: 'global',
16+
description: '',
17+
inputs: [
18+
{
19+
name: 'arg1',
20+
description: '',
21+
},
22+
{
23+
name: 'arg2',
24+
description: '',
25+
},
26+
{
27+
name: 'arg3',
28+
description: '',
29+
private: true,
30+
},
31+
],
32+
options: [
33+
{
34+
name: 'opt1',
35+
description: '',
36+
type: Boolean,
37+
},
38+
{
39+
name: 'opt2',
40+
description: '',
41+
},
42+
{
43+
name: 'opt3',
44+
description: '',
45+
default: 'default',
46+
},
47+
{
48+
name: 'opt4',
49+
description: '',
50+
private: true,
51+
},
52+
{
53+
name: 'opt5',
54+
description: '',
55+
aliases: ['o'],
56+
},
57+
],
58+
})
59+
class BarCommand extends Command {}
60+
61+
describe('getCleanInputsForTelemetry', () => {
62+
63+
// TODO: aliases can be intelligently removed
64+
65+
it('should be empty with no inputs', async () => {
66+
const foo = new FooCommand();
67+
const results = await foo.getCleanInputsForTelemetry([], {});
68+
expect(results).toEqual([]);
69+
});
70+
71+
it('should include additional, unknown arguments', async () => {
72+
const foo = new FooCommand();
73+
const results = await foo.getCleanInputsForTelemetry(['a', 'b', 'c'], {});
74+
expect(results).toEqual(['a', 'b', 'c']);
75+
});
76+
77+
it('should include additional, unknown options', async () => {
78+
const foo = new FooCommand();
79+
const results = await foo.getCleanInputsForTelemetry([], { opt1: true, opt2: 'cow' });
80+
expect(results).toEqual(['--opt1', '--opt2=cow']);
81+
});
82+
83+
it('should include known arguments and options', async () => {
84+
const bar = new BarCommand();
85+
const results = await bar.getCleanInputsForTelemetry(['a', 'b'], { opt1: true, opt2: 'cow', opt3: 'not default' });
86+
expect(results).toEqual(['a', 'b', '--opt1', '--opt2=cow', '--opt3="not default"']);
87+
});
88+
89+
it('should exclude options with default values', async () => {
90+
const bar = new BarCommand();
91+
const results = await bar.getCleanInputsForTelemetry([], { opt3: 'default' });
92+
expect(results).toEqual([]);
93+
});
94+
95+
it('should exclude private arguments and options', async () => {
96+
const bar = new BarCommand();
97+
const results = await bar.getCleanInputsForTelemetry(['a', 'b', 'c'], { opt4: 'private!' });
98+
expect(results).toEqual(['a', 'b']);
99+
});
100+
101+
it('should exclude aliases', async () => {
102+
const bar = new BarCommand();
103+
const results = await bar.getCleanInputsForTelemetry([], { o: 'wow', opt5: 'wow' });
104+
expect(results).toEqual(['--opt5=wow']);
105+
});
106+
107+
});
108+
109+
});

packages/cli-utils/src/lib/command/__tests__/utils.ts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,16 @@ describe('@ionic/cli-utils', () => {
3737
expect(result).toEqual(['--cat', 'meow', '--dog', 'bark', '--flag1']);
3838
});
3939

40+
it('should parse out string option from minimist result and not wrap strings with spaces in double quotes without flag', () => {
41+
const result = minimistOptionsToArray({ _: [], cat: 'meow meow meow' });
42+
expect(result).toEqual(['--cat=meow meow meow']);
43+
});
44+
45+
it('should parse out string option from minimist result and wrap strings with spaces in double quotes with flag provided', () => {
46+
const result = minimistOptionsToArray({ _: [], cat: 'meow meow meow' }, { useDoubleQuotes: true });
47+
expect(result).toEqual(['--cat="meow meow meow"']);
48+
});
49+
4050
});
4151

4252
describe('metadataToMinimistOptions', () => {

packages/cli-utils/src/lib/command/command.ts

Lines changed: 65 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import { isCommandPreRun } from '../../guards';
1212
import { FatalException } from '../errors';
1313
import { validators } from '../validators';
1414
import { minimistOptionsToArray, validateInputs } from './utils';
15+
import { Logger } from '../utils/logger';
1516

1617
export class Command implements ICommand {
1718
public env: IonicEnvironment;
@@ -31,10 +32,21 @@ export class Command implements ICommand {
3132
}
3233
}
3334

34-
async runcmd(pargv: string[]): Promise<void> {
35+
async runcmd(pargv: string[], opts: { showLogs?: boolean } = {}): Promise<void> {
36+
const env = { ...this.env };
37+
38+
if (typeof opts.showLogs === 'undefined') {
39+
opts.showLogs = true;
40+
}
41+
42+
if (!opts.showLogs) {
43+
const DevNull = require('dev-null'); // TODO
44+
env.log = new Logger({ level: this.env.log.level, stream: new DevNull(), prefix: this.env.log.prefix });
45+
}
46+
3547
await this.runwrap(async () => {
36-
this.env.log.msg(`> ${chalk.green([this.env.namespace.name, ...pargv].map(a => a.includes(' ') ? `"${a}"` : a).join(' '))}`);
37-
return this.env.namespace.runCommand(this.env, pargv);
48+
env.log.msg(`> ${chalk.green([env.namespace.name, ...pargv].map(a => a.includes(' ') ? `"${a}"` : a).join(' '))}`);
49+
return this.env.namespace.runCommand(env, pargv);
3850
}, { exit0: false });
3951
}
4052

@@ -75,56 +87,67 @@ export class Command implements ICommand {
7587
}
7688
}
7789

78-
await Promise.all([
79-
(async () => {
80-
// TODO: get telemetry for commands that aborted above
81-
if (config.telemetry !== false) {
82-
let cmdInputs: CommandLineInputs = [];
90+
const runPromise = (async () => {
91+
await this.runwrap(() => this.run(inputs, options));
92+
})();
8393

84-
if (this.metadata.name === 'help') {
85-
cmdInputs = inputs;
86-
} else {
87-
cmdInputs = this.getCleanInputsForTelemetry(inputs, options);
88-
}
94+
const telemetryPromise = (async () => {
95+
if (config.telemetry !== false) {
96+
let cmdInputs: CommandLineInputs = [];
8997

90-
await this.env.telemetry.sendCommand(`ionic ${this.metadata.fullName}`, cmdInputs);
98+
if (this.metadata.name === 'login' || this.metadata.name === 'logout') {
99+
await runPromise;
100+
} else if (this.metadata.name === 'help') {
101+
cmdInputs = inputs;
102+
} else {
103+
cmdInputs = await this.getCleanInputsForTelemetry(inputs, options);
91104
}
92-
})(),
93-
(async () => {
94-
await this.runwrap(() => this.run(inputs, options));
95-
})()
96-
]);
105+
106+
await this.env.telemetry.sendCommand(`ionic ${this.metadata.fullName}`, cmdInputs);
107+
}
108+
})();
109+
110+
await Promise.all([runPromise, telemetryPromise]);
97111
}
98112

99113
exit(msg: string, code: number = 1): FatalException {
100114
return new FatalException(msg, code);
101115
}
102116

103-
getCleanInputsForTelemetry(inputs: CommandLineInputs, options: CommandLineOptions) {
104-
if (this.metadata.inputs) {
105-
const mdi = this.metadata.inputs;
106-
inputs = inputs
107-
.filter((input, i) => {
108-
return mdi[i] && !mdi[i].private;
117+
async getCleanInputsForTelemetry(inputs: CommandLineInputs, options: CommandLineOptions): Promise<string[]> {
118+
const initialOptions: CommandLineOptions = { _: [] };
119+
120+
const filteredInputs = inputs.filter((input, i) => !this.metadata.inputs || (this.metadata.inputs[i] && !this.metadata.inputs[i].private));
121+
const filteredOptions = Object.keys(options)
122+
.filter(optionName => {
123+
const metadataOption = this.metadata.options && this.metadata.options.find((o) => {
124+
return o.name === optionName || (typeof o.aliases !== 'undefined' && o.aliases.includes(optionName));
109125
});
110-
}
111126

112-
if (this.metadata.options) {
113-
const mdo = this.metadata.options;
114-
options = Object.keys(options)
115-
.filter(optionName => {
116-
const metadataOptionFound = mdo.find((mdOption) => (
117-
mdOption.name === optionName || (mdOption.aliases || []).includes(optionName)
118-
));
119-
return metadataOptionFound ? !metadataOptionFound.private : true;
120-
})
121-
.reduce((allOptions, optionName) => {
122-
allOptions[optionName] = options[optionName];
123-
return allOptions;
124-
}, <CommandLineOptions>{});
125-
}
127+
if (metadataOption && metadataOption.aliases && metadataOption.aliases.includes(optionName)) {
128+
return false; // exclude aliases
129+
}
130+
131+
if (!metadataOption) {
132+
return true; // include unknown options
133+
}
134+
135+
if (metadataOption.private) {
136+
return false; // exclude private options
137+
}
138+
139+
if (typeof metadataOption.default !== 'undefined' && metadataOption.default === options[optionName]) {
140+
return false; // exclude options that match their default value (means it wasn't supplied by user)
141+
}
142+
143+
return true;
144+
})
145+
.reduce((allOptions, optionName) => {
146+
allOptions[optionName] = options[optionName];
147+
return allOptions;
148+
}, initialOptions);
126149

127-
let optionInputs = minimistOptionsToArray(options);
128-
return inputs.concat(optionInputs);
150+
const optionInputs = minimistOptionsToArray(filteredOptions, { useDoubleQuotes: true });
151+
return filteredInputs.concat(optionInputs);
129152
}
130153
}

packages/cli-utils/src/lib/command/utils.ts

Lines changed: 16 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -20,16 +20,28 @@ const typeDefaults: CommandOptionTypeDefaults = new Map<CommandOptionType, Comma
2020
.set(String, null)
2121
.set(Boolean, false);
2222

23-
export function minimistOptionsToArray(options: CommandLineOptions, dargsOptions: dargsType.Opts = {}): string[] {
23+
export interface MinimistOptionsToArrayOptions extends dargsType.Opts {
24+
useDoubleQuotes?: boolean;
25+
}
26+
27+
export function minimistOptionsToArray(options: CommandLineOptions, fnOptions: MinimistOptionsToArrayOptions = {}): string[] {
2428
const dargs = load('dargs');
2529

26-
if (typeof dargsOptions.ignoreFalse === 'undefined') {
27-
dargsOptions.ignoreFalse = true;
30+
if (typeof fnOptions.ignoreFalse === 'undefined') {
31+
fnOptions.ignoreFalse = true;
32+
}
33+
34+
if (fnOptions.useDoubleQuotes) {
35+
fnOptions.useEquals = true;
2836
}
2937

30-
const results = dargs(options, dargsOptions);
38+
let results = dargs(options, fnOptions);
3139
results.splice(results.length - options._.length); // take out arguments
3240

41+
if (fnOptions.useDoubleQuotes) {
42+
results = results.map(r => r.replace(/^(\-\-[A-Za-z0-9-]+)=(.+\s+.+)$/, '$1="$2"'));
43+
}
44+
3345
return results;
3446
}
3547

packages/cli-utils/src/lib/config.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -171,7 +171,7 @@ export class Config extends BaseConfig<ConfigFile> {
171171
results.tokens.appUser = {};
172172
}
173173

174-
if (typeof results.backend === 'undefined') {
174+
if (typeof results.backend !== 'string') {
175175
results.backend = BACKEND_LEGACY;
176176
}
177177

0 commit comments

Comments
 (0)