Skip to content

Kddimitrov/track command options #4181

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 9 commits into from
Dec 4, 2018
1 change: 1 addition & 0 deletions lib/bootstrap.ts
Original file line number Diff line number Diff line change
Expand Up @@ -128,6 +128,7 @@ $injector.require("bundleValidatorHelper", "./helpers/bundle-validator-helper");
$injector.require("androidBundleValidatorHelper", "./helpers/android-bundle-validator-helper");
$injector.require("liveSyncCommandHelper", "./helpers/livesync-command-helper");
$injector.require("deployCommandHelper", "./helpers/deploy-command-helper");
$injector.require("optionsTracker", "./helpers/options-track-helper");

$injector.requirePublicClass("localBuildService", "./services/local-build-service");
$injector.requirePublicClass("liveSyncService", "./services/livesync/livesync-service");
Expand Down
4 changes: 4 additions & 0 deletions lib/common/declarations.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1269,6 +1269,10 @@ interface IDashedOption {
* Type of the option. It can be string, boolean, Array, etc.
*/
type: string;
/**
* Option has sensitive value
*/
hasSensitiveValue: boolean;
/**
* Shorthand option passed on the command line with `-` sign, for example `-v`
*/
Expand Down
4 changes: 3 additions & 1 deletion lib/common/services/commands-service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,8 @@ export class CommandsService implements ICommandsService {
private $resources: IResourceLoader,
private $staticConfig: Config.IStaticConfig,
private $helpService: IHelpService,
private $extensibilityService: IExtensibilityService) {
private $extensibilityService: IExtensibilityService,
private $optionsTracker: IOptionsTracker) {
}

public allCommands(opts: { includeDevCommands: boolean }): string[] {
Expand Down Expand Up @@ -63,6 +64,7 @@ export class CommandsService implements ICommandsService {
}

await analyticsService.trackInGoogleAnalytics(googleAnalyticsPageData);
await this.$optionsTracker.trackOptions(this.$options);
}

const shouldExecuteHooks = !this.$staticConfig.disableCommandHooks && (command.enableHooks === undefined || command.enableHooks === true);
Expand Down
3 changes: 2 additions & 1 deletion lib/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -149,7 +149,8 @@ export const enum TrackActionNames {
LiveSync = "LiveSync",
RunSetupScript = "Run Setup Script",
CheckLocalBuildSetup = "Check Local Build Setup",
CheckEnvironmentRequirements = "Check Environment Requirements"
CheckEnvironmentRequirements = "Check Environment Requirements",
Options = "Options"
}

export const AnalyticsEventLabelDelimiter = "__";
Expand Down
4 changes: 4 additions & 0 deletions lib/declarations.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -910,6 +910,10 @@ interface IAndroidBundleValidatorHelper {
validateRuntimeVersion(projectData: IProjectData): void
}

interface IOptionsTracker {
trackOptions(options: IOptions): Promise<void>
}

interface INativeScriptCloudExtensionService {
/**
* Installs nativescript-cloud extension
Expand Down
80 changes: 80 additions & 0 deletions lib/helpers/options-track-helper.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
import * as path from "path";
import { TrackActionNames } from "../constants";

export class OptionsTracker {
public static PASSWORD_DETECTION_STRING = "password";
public static PRIVATE_REPLACE_VALUE = "private";
public static PATH_REPLACE_VALUE = "_localpath";
public static SIZE_EXEEDED_REPLACE_VALUE = "sizeExceeded";

constructor(
private $analyticsService: IAnalyticsService) {
}

public async trackOptions(options: IOptions) {
const trackObject = this.getTrackObject(options);

await this.$analyticsService.trackEventActionInGoogleAnalytics({
action: TrackActionNames.Options,
additionalData: JSON.stringify(trackObject)
});
}

private getTrackObject(options: IOptions): IDictionary<any> {
const optionsArgvCopy = _.cloneDeep(options.argv);

return this.sanitizeTrackObject(optionsArgvCopy, options);
}

private sanitizeTrackObject(data: IDictionary<any>, options?: IOptions): IDictionary<any> {
const shorthands = options ? options.shorthands : [];
const optionsDefinitions = options ? options.options : {};

_.forEach(data, (value, key) => {
if (this.shouldSkipProperty(key, value, shorthands, optionsDefinitions)) {
delete data[key];
} else {
if (options && optionsDefinitions[key] && optionsDefinitions[key].hasSensitiveValue !== false) {
value = OptionsTracker.PRIVATE_REPLACE_VALUE;
} else if (key.toLowerCase().indexOf(OptionsTracker.PASSWORD_DETECTION_STRING) >= 0) {
value = OptionsTracker.PRIVATE_REPLACE_VALUE;
} else if (_.isString(value) && value !== path.basename(value)) {
value = OptionsTracker.PATH_REPLACE_VALUE;
} else if (_.isObject(value) && !_.isArray(value)) {
value = this.sanitizeTrackObject(value);
}

data[key] = value;
}
});

return data;
}

private shouldSkipProperty(key: string, value: any, shorthands: string[] = [], options: IDictionary<IDashedOption> = {}): Boolean {
if (shorthands.indexOf(key) >= 0) {
return true;
}

if (key.indexOf("-") >= 0) {
return true;
}

if (key === "_") {
return true;
}

const optionDef = options[key];
if (optionDef && optionDef.type === OptionType.Boolean) {
if (optionDef.default !== true && value === false || optionDef.default === true && value === true) {
return true;
}
}

if (_.isUndefined(value)) {
return true;
}
}
}

$injector.register("optionsTracker", OptionsTracker);
172 changes: 87 additions & 85 deletions lib/options.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,15 +8,15 @@ export class Options {
private optionsWhiteList = ["ui", "recursive", "reporter", "require", "timeout", "_", "$0"]; // These options shouldn't be validated
public argv: IYargArgv;
private globalOptions: IDictionary<IDashedOption> = {
log: { type: OptionType.String },
verbose: { type: OptionType.Boolean, alias: "v" },
version: { type: OptionType.Boolean },
help: { type: OptionType.Boolean, alias: "h" },
profileDir: { type: OptionType.String },
analyticsClient: { type: OptionType.String },
path: { type: OptionType.String, alias: "p" },
log: { type: OptionType.String, hasSensitiveValue: false },
verbose: { type: OptionType.Boolean, alias: "v", hasSensitiveValue: false },
version: { type: OptionType.Boolean, hasSensitiveValue: false },
help: { type: OptionType.Boolean, alias: "h", hasSensitiveValue: false },
profileDir: { type: OptionType.String, hasSensitiveValue: true },
analyticsClient: { type: OptionType.String, hasSensitiveValue: false },
path: { type: OptionType.String, alias: "p", hasSensitiveValue: true },
// This will parse all non-hyphenated values as strings.
_: { type: OptionType.String }
_: { type: OptionType.String, hasSensitiveValue: false }
};

public options: IDictionary<IDashedOption>;
Expand All @@ -41,83 +41,85 @@ export class Options {

private get commonOptions(): IDictionary<IDashedOption> {
return {
ipa: { type: OptionType.String },
frameworkPath: { type: OptionType.String },
frameworkName: { type: OptionType.String },
framework: { type: OptionType.String },
frameworkVersion: { type: OptionType.String },
forDevice: { type: OptionType.Boolean },
provision: { type: OptionType.Object },
client: { type: OptionType.Boolean, default: true },
env: { type: OptionType.Object },
production: { type: OptionType.Boolean },
debugTransport: { type: OptionType.Boolean },
keyStorePath: { type: OptionType.String },
keyStorePassword: { type: OptionType.String, },
keyStoreAlias: { type: OptionType.String },
keyStoreAliasPassword: { type: OptionType.String },
ignoreScripts: { type: OptionType.Boolean },
disableNpmInstall: { type: OptionType.Boolean },
compileSdk: { type: OptionType.Number },
port: { type: OptionType.Number },
copyTo: { type: OptionType.String },
platformTemplate: { type: OptionType.String },
js: { type: OptionType.Boolean },
javascript: { type: OptionType.Boolean },
ng: { type: OptionType.Boolean },
angular: { type: OptionType.Boolean },
vue: { type: OptionType.Boolean },
vuejs: { type: OptionType.Boolean },
tsc: { type: OptionType.Boolean },
ts: { type: OptionType.Boolean },
typescript: { type: OptionType.Boolean },
yarn: { type: OptionType.Boolean },
androidTypings: { type: OptionType.Boolean },
bundle: { type: OptionType.String },
all: { type: OptionType.Boolean },
teamId: { type: OptionType.Object },
syncAllFiles: { type: OptionType.Boolean, default: false },
chrome: { type: OptionType.Boolean },
inspector: { type: OptionType.Boolean },
clean: { type: OptionType.Boolean },
watch: { type: OptionType.Boolean, default: true },
background: { type: OptionType.String },
username: { type: OptionType.String },
pluginName: { type: OptionType.String },
hmr: { type: OptionType.Boolean },
collection: { type: OptionType.String, alias: "c" },
json: { type: OptionType.Boolean },
avd: { type: OptionType.String },
config: { type: OptionType.Array },
insecure: { type: OptionType.Boolean, alias: "k" },
debug: { type: OptionType.Boolean, alias: "d" },
timeout: { type: OptionType.String },
device: { type: OptionType.String },
availableDevices: { type: OptionType.Boolean },
appid: { type: OptionType.String },
geny: { type: OptionType.String },
debugBrk: { type: OptionType.Boolean },
debugPort: { type: OptionType.Number },
start: { type: OptionType.Boolean },
stop: { type: OptionType.Boolean },
ddi: { type: OptionType.String }, // the path to developer disk image
justlaunch: { type: OptionType.Boolean },
file: { type: OptionType.String },
force: { type: OptionType.Boolean, alias: "f" },
companion: { type: OptionType.Boolean },
emulator: { type: OptionType.Boolean },
sdk: { type: OptionType.String },
template: { type: OptionType.String },
certificate: { type: OptionType.String },
certificatePassword: { type: OptionType.String },
release: { type: OptionType.Boolean, alias: "r" },
var: { type: OptionType.Object },
default: { type: OptionType.Boolean },
count: { type: OptionType.Number },
analyticsLogFile: { type: OptionType.String },
hooks: { type: OptionType.Boolean, default: true },
link: { type: OptionType.Boolean, default: false },
aab: { type: OptionType.Boolean }
ipa: { type: OptionType.String, hasSensitiveValue: false },
frameworkPath: { type: OptionType.String, hasSensitiveValue: false },
frameworkName: { type: OptionType.String, hasSensitiveValue: false },
framework: { type: OptionType.String, hasSensitiveValue: false },
frameworkVersion: { type: OptionType.String, hasSensitiveValue: false },
forDevice: { type: OptionType.Boolean, hasSensitiveValue: false },
provision: { type: OptionType.Object, hasSensitiveValue: true },
client: { type: OptionType.Boolean, default: true, hasSensitiveValue: false },
env: { type: OptionType.Object, hasSensitiveValue: false },
production: { type: OptionType.Boolean, hasSensitiveValue: false },
debugTransport: { type: OptionType.Boolean, hasSensitiveValue: false },
keyStorePath: { type: OptionType.String, hasSensitiveValue: false },
keyStorePassword: { type: OptionType.String, hasSensitiveValue: true },
keyStoreAlias: { type: OptionType.String, hasSensitiveValue: true },
keyStoreAliasPassword: { type: OptionType.String, hasSensitiveValue: true },
ignoreScripts: { type: OptionType.Boolean, hasSensitiveValue: false },
disableNpmInstall: { type: OptionType.Boolean, hasSensitiveValue: false },
compileSdk: { type: OptionType.Number, hasSensitiveValue: false },
port: { type: OptionType.Number, hasSensitiveValue: false },
copyTo: { type: OptionType.String, hasSensitiveValue: false },
platformTemplate: { type: OptionType.String, hasSensitiveValue: false },
js: { type: OptionType.Boolean, hasSensitiveValue: false },
javascript: { type: OptionType.Boolean, hasSensitiveValue: false },
ng: { type: OptionType.Boolean, hasSensitiveValue: false },
angular: { type: OptionType.Boolean, hasSensitiveValue: false },
vue: { type: OptionType.Boolean, hasSensitiveValue: false },
vuejs: { type: OptionType.Boolean, hasSensitiveValue: false },
tsc: { type: OptionType.Boolean, hasSensitiveValue: false },
ts: { type: OptionType.Boolean, hasSensitiveValue: false },
typescript: { type: OptionType.Boolean, hasSensitiveValue: false },
yarn: { type: OptionType.Boolean, hasSensitiveValue: false },
androidTypings: { type: OptionType.Boolean, hasSensitiveValue: false },
bundle: { type: OptionType.String, hasSensitiveValue: false },
all: { type: OptionType.Boolean, hasSensitiveValue: false },
teamId: { type: OptionType.Object, hasSensitiveValue: true },
syncAllFiles: { type: OptionType.Boolean, default: false, hasSensitiveValue: false },
chrome: { type: OptionType.Boolean, hasSensitiveValue: false },
inspector: { type: OptionType.Boolean, hasSensitiveValue: false },
clean: { type: OptionType.Boolean, hasSensitiveValue: false },
watch: { type: OptionType.Boolean, default: true, hasSensitiveValue: false },
background: { type: OptionType.String, hasSensitiveValue: false },
username: { type: OptionType.String, hasSensitiveValue: true },
pluginName: { type: OptionType.String, hasSensitiveValue: false },
hmr: { type: OptionType.Boolean, hasSensitiveValue: false },
collection: { type: OptionType.String, alias: "c", hasSensitiveValue: false },
json: { type: OptionType.Boolean, hasSensitiveValue: false },
avd: { type: OptionType.String, hasSensitiveValue: true },
// check not used
config: { type: OptionType.Array, hasSensitiveValue: false },
insecure: { type: OptionType.Boolean, alias: "k", hasSensitiveValue: false },
debug: { type: OptionType.Boolean, alias: "d", hasSensitiveValue: false },
timeout: { type: OptionType.String, hasSensitiveValue: false },
device: { type: OptionType.String, hasSensitiveValue: true },
availableDevices: { type: OptionType.Boolean, hasSensitiveValue: true },
appid: { type: OptionType.String, hasSensitiveValue: true },
geny: { type: OptionType.String, hasSensitiveValue: true },
debugBrk: { type: OptionType.Boolean, hasSensitiveValue: false },
debugPort: { type: OptionType.Number, hasSensitiveValue: false },
start: { type: OptionType.Boolean, hasSensitiveValue: false },
stop: { type: OptionType.Boolean, hasSensitiveValue: false },
ddi: { type: OptionType.String, hasSensitiveValue: true }, // the path to developer disk image
justlaunch: { type: OptionType.Boolean, hasSensitiveValue: false },
file: { type: OptionType.String, hasSensitiveValue: true },
force: { type: OptionType.Boolean, alias: "f", hasSensitiveValue: false },
// remove legacy
companion: { type: OptionType.Boolean, hasSensitiveValue: false },
emulator: { type: OptionType.Boolean, hasSensitiveValue: false },
sdk: { type: OptionType.String, hasSensitiveValue: false },
template: { type: OptionType.String, hasSensitiveValue: true },
certificate: { type: OptionType.String, hasSensitiveValue: true },
certificatePassword: { type: OptionType.String, hasSensitiveValue: true },
release: { type: OptionType.Boolean, alias: "r", hasSensitiveValue: false },
var: { type: OptionType.Object, hasSensitiveValue: true },
default: { type: OptionType.Boolean, hasSensitiveValue: false },
count: { type: OptionType.Number, hasSensitiveValue: false },
analyticsLogFile: { type: OptionType.String, hasSensitiveValue: true },
hooks: { type: OptionType.Boolean, default: true, hasSensitiveValue: false },
link: { type: OptionType.Boolean, default: false, hasSensitiveValue: false },
aab: { type: OptionType.Boolean, hasSensitiveValue: false }
};
}

Expand Down
6 changes: 3 additions & 3 deletions test/options.ts
Original file line number Diff line number Diff line change
Expand Up @@ -166,7 +166,7 @@ describe("options", () => {
process.argv.push("--test1");
process.argv.push("value");
const options = createOptions(testInjector);
options.validateOptions({ test1: { type: OptionType.String } });
options.validateOptions({ test1: { type: OptionType.String, hasSensitiveValue: false } });
process.argv.pop();
process.argv.pop();
assert.isFalse(isExecutionStopped);
Expand All @@ -175,7 +175,7 @@ describe("options", () => {
it("does not break execution when valid commandSpecificOptions are passed and user specifies globally valid option", () => {
const options = createOptions(testInjector);
process.argv.push("--version");
options.validateOptions({ test1: { type: OptionType.String } });
options.validateOptions({ test1: { type: OptionType.String, hasSensitiveValue: false } });
process.argv.pop();
assert.isFalse(isExecutionStopped);
});
Expand Down Expand Up @@ -253,7 +253,7 @@ describe("options", () => {
it("does not break execution when dashed option with two dashes is passed", () => {
process.argv.push("--special-dashed-v");
const options = createOptions(testInjector);
options.validateOptions({ specialDashedV: { type: OptionType.Boolean } });
options.validateOptions({ specialDashedV: { type: OptionType.Boolean, hasSensitiveValue: false } });
process.argv.pop();
assert.isFalse(isExecutionStopped, "Dashed options should be validated in specific way. Make sure validation allows yargs specific behavior:" +
"Dashed options (special-dashed-v) are added to yargs.argv in two ways: special-dashed-v and specialDashedV");
Expand Down
3 changes: 3 additions & 0 deletions test/platform-commands.ts
Original file line number Diff line number Diff line change
Expand Up @@ -179,6 +179,9 @@ function createTestInjector() {
testInjector.register("pacoteService", {
extractPackage: async (packageName: string, destinationDirectory: string, options?: IPacoteExtractOptions): Promise<void> => undefined
});
testInjector.register("optionsTracker", {
trackOptions: () => Promise.resolve(null)
});
testInjector.register("usbLiveSyncService", ({}));

return testInjector;
Expand Down
3 changes: 3 additions & 0 deletions test/plugins-service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,9 @@ function createTestInjector() {
testInjector.register("logger", stubs.LoggerStub);
testInjector.register("staticConfig", StaticConfig);
testInjector.register("hooksService", stubs.HooksServiceStub);
testInjector.register("optionsTracker", {
trackOptions: () => Promise.resolve(null)
});
testInjector.register("commandsService", CommandsService);
testInjector.register("commandsServiceProvider", {
registerDynamicSubCommands: () => { /* intentionally empty body */ }
Expand Down