Skip to content

Commit d6198c6

Browse files
authored
Merge pull request #4181 from NativeScript/kddimitrov/track-command-options
Kddimitrov/track command options
2 parents 51fc9c5 + 26435cf commit d6198c6

10 files changed

+190
-90
lines changed

lib/bootstrap.ts

+1
Original file line numberDiff line numberDiff line change
@@ -128,6 +128,7 @@ $injector.require("bundleValidatorHelper", "./helpers/bundle-validator-helper");
128128
$injector.require("androidBundleValidatorHelper", "./helpers/android-bundle-validator-helper");
129129
$injector.require("liveSyncCommandHelper", "./helpers/livesync-command-helper");
130130
$injector.require("deployCommandHelper", "./helpers/deploy-command-helper");
131+
$injector.require("optionsTracker", "./helpers/options-track-helper");
131132

132133
$injector.requirePublicClass("localBuildService", "./services/local-build-service");
133134
$injector.requirePublicClass("liveSyncService", "./services/livesync/livesync-service");

lib/common/declarations.d.ts

+4
Original file line numberDiff line numberDiff line change
@@ -1269,6 +1269,10 @@ interface IDashedOption {
12691269
* Type of the option. It can be string, boolean, Array, etc.
12701270
*/
12711271
type: string;
1272+
/**
1273+
* Option has sensitive value
1274+
*/
1275+
hasSensitiveValue: boolean;
12721276
/**
12731277
* Shorthand option passed on the command line with `-` sign, for example `-v`
12741278
*/

lib/common/services/commands-service.ts

+3-1
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,8 @@ export class CommandsService implements ICommandsService {
3030
private $resources: IResourceLoader,
3131
private $staticConfig: Config.IStaticConfig,
3232
private $helpService: IHelpService,
33-
private $extensibilityService: IExtensibilityService) {
33+
private $extensibilityService: IExtensibilityService,
34+
private $optionsTracker: IOptionsTracker) {
3435
}
3536

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

6566
await analyticsService.trackInGoogleAnalytics(googleAnalyticsPageData);
67+
await this.$optionsTracker.trackOptions(this.$options);
6668
}
6769

6870
const shouldExecuteHooks = !this.$staticConfig.disableCommandHooks && (command.enableHooks === undefined || command.enableHooks === true);

lib/constants.ts

+2-1
Original file line numberDiff line numberDiff line change
@@ -149,7 +149,8 @@ export const enum TrackActionNames {
149149
LiveSync = "LiveSync",
150150
RunSetupScript = "Run Setup Script",
151151
CheckLocalBuildSetup = "Check Local Build Setup",
152-
CheckEnvironmentRequirements = "Check Environment Requirements"
152+
CheckEnvironmentRequirements = "Check Environment Requirements",
153+
Options = "Options"
153154
}
154155

155156
export const AnalyticsEventLabelDelimiter = "__";

lib/declarations.d.ts

+4
Original file line numberDiff line numberDiff line change
@@ -915,6 +915,10 @@ interface IAndroidBundleValidatorHelper {
915915
validateRuntimeVersion(projectData: IProjectData): void
916916
}
917917

918+
interface IOptionsTracker {
919+
trackOptions(options: IOptions): Promise<void>
920+
}
921+
918922
interface INativeScriptCloudExtensionService {
919923
/**
920924
* Installs nativescript-cloud extension

lib/helpers/options-track-helper.ts

+80
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
1+
import * as path from "path";
2+
import { TrackActionNames } from "../constants";
3+
4+
export class OptionsTracker {
5+
public static PASSWORD_DETECTION_STRING = "password";
6+
public static PRIVATE_REPLACE_VALUE = "private";
7+
public static PATH_REPLACE_VALUE = "_localpath";
8+
public static SIZE_EXEEDED_REPLACE_VALUE = "sizeExceeded";
9+
10+
constructor(
11+
private $analyticsService: IAnalyticsService) {
12+
}
13+
14+
public async trackOptions(options: IOptions) {
15+
const trackObject = this.getTrackObject(options);
16+
17+
await this.$analyticsService.trackEventActionInGoogleAnalytics({
18+
action: TrackActionNames.Options,
19+
additionalData: JSON.stringify(trackObject)
20+
});
21+
}
22+
23+
private getTrackObject(options: IOptions): IDictionary<any> {
24+
const optionsArgvCopy = _.cloneDeep(options.argv);
25+
26+
return this.sanitizeTrackObject(optionsArgvCopy, options);
27+
}
28+
29+
private sanitizeTrackObject(data: IDictionary<any>, options?: IOptions): IDictionary<any> {
30+
const shorthands = options ? options.shorthands : [];
31+
const optionsDefinitions = options ? options.options : {};
32+
33+
_.forEach(data, (value, key) => {
34+
if (this.shouldSkipProperty(key, value, shorthands, optionsDefinitions)) {
35+
delete data[key];
36+
} else {
37+
if (options && optionsDefinitions[key] && optionsDefinitions[key].hasSensitiveValue !== false) {
38+
value = OptionsTracker.PRIVATE_REPLACE_VALUE;
39+
} else if (key.toLowerCase().indexOf(OptionsTracker.PASSWORD_DETECTION_STRING) >= 0) {
40+
value = OptionsTracker.PRIVATE_REPLACE_VALUE;
41+
} else if (_.isString(value) && value !== path.basename(value)) {
42+
value = OptionsTracker.PATH_REPLACE_VALUE;
43+
} else if (_.isObject(value) && !_.isArray(value)) {
44+
value = this.sanitizeTrackObject(value);
45+
}
46+
47+
data[key] = value;
48+
}
49+
});
50+
51+
return data;
52+
}
53+
54+
private shouldSkipProperty(key: string, value: any, shorthands: string[] = [], options: IDictionary<IDashedOption> = {}): Boolean {
55+
if (shorthands.indexOf(key) >= 0) {
56+
return true;
57+
}
58+
59+
if (key.indexOf("-") >= 0) {
60+
return true;
61+
}
62+
63+
if (key === "_") {
64+
return true;
65+
}
66+
67+
const optionDef = options[key];
68+
if (optionDef && optionDef.type === OptionType.Boolean) {
69+
if (optionDef.default !== true && value === false || optionDef.default === true && value === true) {
70+
return true;
71+
}
72+
}
73+
74+
if (_.isUndefined(value)) {
75+
return true;
76+
}
77+
}
78+
}
79+
80+
$injector.register("optionsTracker", OptionsTracker);

lib/options.ts

+87-85
Original file line numberDiff line numberDiff line change
@@ -8,15 +8,15 @@ export class Options {
88
private optionsWhiteList = ["ui", "recursive", "reporter", "require", "timeout", "_", "$0"]; // These options shouldn't be validated
99
public argv: IYargArgv;
1010
private globalOptions: IDictionary<IDashedOption> = {
11-
log: { type: OptionType.String },
12-
verbose: { type: OptionType.Boolean, alias: "v" },
13-
version: { type: OptionType.Boolean },
14-
help: { type: OptionType.Boolean, alias: "h" },
15-
profileDir: { type: OptionType.String },
16-
analyticsClient: { type: OptionType.String },
17-
path: { type: OptionType.String, alias: "p" },
11+
log: { type: OptionType.String, hasSensitiveValue: false },
12+
verbose: { type: OptionType.Boolean, alias: "v", hasSensitiveValue: false },
13+
version: { type: OptionType.Boolean, hasSensitiveValue: false },
14+
help: { type: OptionType.Boolean, alias: "h", hasSensitiveValue: false },
15+
profileDir: { type: OptionType.String, hasSensitiveValue: true },
16+
analyticsClient: { type: OptionType.String, hasSensitiveValue: false },
17+
path: { type: OptionType.String, alias: "p", hasSensitiveValue: true },
1818
// This will parse all non-hyphenated values as strings.
19-
_: { type: OptionType.String }
19+
_: { type: OptionType.String, hasSensitiveValue: false }
2020
};
2121

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

4242
private get commonOptions(): IDictionary<IDashedOption> {
4343
return {
44-
ipa: { type: OptionType.String },
45-
frameworkPath: { type: OptionType.String },
46-
frameworkName: { type: OptionType.String },
47-
framework: { type: OptionType.String },
48-
frameworkVersion: { type: OptionType.String },
49-
forDevice: { type: OptionType.Boolean },
50-
provision: { type: OptionType.Object },
51-
client: { type: OptionType.Boolean, default: true },
52-
env: { type: OptionType.Object },
53-
production: { type: OptionType.Boolean },
54-
debugTransport: { type: OptionType.Boolean },
55-
keyStorePath: { type: OptionType.String },
56-
keyStorePassword: { type: OptionType.String, },
57-
keyStoreAlias: { type: OptionType.String },
58-
keyStoreAliasPassword: { type: OptionType.String },
59-
ignoreScripts: { type: OptionType.Boolean },
60-
disableNpmInstall: { type: OptionType.Boolean },
61-
compileSdk: { type: OptionType.Number },
62-
port: { type: OptionType.Number },
63-
copyTo: { type: OptionType.String },
64-
platformTemplate: { type: OptionType.String },
65-
js: { type: OptionType.Boolean },
66-
javascript: { type: OptionType.Boolean },
67-
ng: { type: OptionType.Boolean },
68-
angular: { type: OptionType.Boolean },
69-
vue: { type: OptionType.Boolean },
70-
vuejs: { type: OptionType.Boolean },
71-
tsc: { type: OptionType.Boolean },
72-
ts: { type: OptionType.Boolean },
73-
typescript: { type: OptionType.Boolean },
74-
yarn: { type: OptionType.Boolean },
75-
androidTypings: { type: OptionType.Boolean },
76-
bundle: { type: OptionType.String },
77-
all: { type: OptionType.Boolean },
78-
teamId: { type: OptionType.Object },
79-
syncAllFiles: { type: OptionType.Boolean, default: false },
80-
chrome: { type: OptionType.Boolean },
81-
inspector: { type: OptionType.Boolean },
82-
clean: { type: OptionType.Boolean },
83-
watch: { type: OptionType.Boolean, default: true },
84-
background: { type: OptionType.String },
85-
username: { type: OptionType.String },
86-
pluginName: { type: OptionType.String },
87-
hmr: { type: OptionType.Boolean },
88-
collection: { type: OptionType.String, alias: "c" },
89-
json: { type: OptionType.Boolean },
90-
avd: { type: OptionType.String },
91-
config: { type: OptionType.Array },
92-
insecure: { type: OptionType.Boolean, alias: "k" },
93-
debug: { type: OptionType.Boolean, alias: "d" },
94-
timeout: { type: OptionType.String },
95-
device: { type: OptionType.String },
96-
availableDevices: { type: OptionType.Boolean },
97-
appid: { type: OptionType.String },
98-
geny: { type: OptionType.String },
99-
debugBrk: { type: OptionType.Boolean },
100-
debugPort: { type: OptionType.Number },
101-
start: { type: OptionType.Boolean },
102-
stop: { type: OptionType.Boolean },
103-
ddi: { type: OptionType.String }, // the path to developer disk image
104-
justlaunch: { type: OptionType.Boolean },
105-
file: { type: OptionType.String },
106-
force: { type: OptionType.Boolean, alias: "f" },
107-
companion: { type: OptionType.Boolean },
108-
emulator: { type: OptionType.Boolean },
109-
sdk: { type: OptionType.String },
110-
template: { type: OptionType.String },
111-
certificate: { type: OptionType.String },
112-
certificatePassword: { type: OptionType.String },
113-
release: { type: OptionType.Boolean, alias: "r" },
114-
var: { type: OptionType.Object },
115-
default: { type: OptionType.Boolean },
116-
count: { type: OptionType.Number },
117-
analyticsLogFile: { type: OptionType.String },
118-
hooks: { type: OptionType.Boolean, default: true },
119-
link: { type: OptionType.Boolean, default: false },
120-
aab: { type: OptionType.Boolean }
44+
ipa: { type: OptionType.String, hasSensitiveValue: true },
45+
frameworkPath: { type: OptionType.String, hasSensitiveValue: true },
46+
frameworkName: { type: OptionType.String, hasSensitiveValue: false },
47+
framework: { type: OptionType.String, hasSensitiveValue: false },
48+
frameworkVersion: { type: OptionType.String, hasSensitiveValue: false },
49+
forDevice: { type: OptionType.Boolean, hasSensitiveValue: false },
50+
provision: { type: OptionType.Object, hasSensitiveValue: true },
51+
client: { type: OptionType.Boolean, default: true, hasSensitiveValue: false },
52+
env: { type: OptionType.Object, hasSensitiveValue: false },
53+
production: { type: OptionType.Boolean, hasSensitiveValue: false },
54+
debugTransport: { type: OptionType.Boolean, hasSensitiveValue: false },
55+
keyStorePath: { type: OptionType.String, hasSensitiveValue: true },
56+
keyStorePassword: { type: OptionType.String, hasSensitiveValue: true },
57+
keyStoreAlias: { type: OptionType.String, hasSensitiveValue: true },
58+
keyStoreAliasPassword: { type: OptionType.String, hasSensitiveValue: true },
59+
ignoreScripts: { type: OptionType.Boolean, hasSensitiveValue: false },
60+
disableNpmInstall: { type: OptionType.Boolean, hasSensitiveValue: false },
61+
compileSdk: { type: OptionType.Number, hasSensitiveValue: false },
62+
port: { type: OptionType.Number, hasSensitiveValue: false },
63+
copyTo: { type: OptionType.String, hasSensitiveValue: true },
64+
platformTemplate: { type: OptionType.String, hasSensitiveValue: true },
65+
js: { type: OptionType.Boolean, hasSensitiveValue: false },
66+
javascript: { type: OptionType.Boolean, hasSensitiveValue: false },
67+
ng: { type: OptionType.Boolean, hasSensitiveValue: false },
68+
angular: { type: OptionType.Boolean, hasSensitiveValue: false },
69+
vue: { type: OptionType.Boolean, hasSensitiveValue: false },
70+
vuejs: { type: OptionType.Boolean, hasSensitiveValue: false },
71+
tsc: { type: OptionType.Boolean, hasSensitiveValue: false },
72+
ts: { type: OptionType.Boolean, hasSensitiveValue: false },
73+
typescript: { type: OptionType.Boolean, hasSensitiveValue: false },
74+
yarn: { type: OptionType.Boolean, hasSensitiveValue: false },
75+
androidTypings: { type: OptionType.Boolean, hasSensitiveValue: false },
76+
bundle: { type: OptionType.String, hasSensitiveValue: false },
77+
all: { type: OptionType.Boolean, hasSensitiveValue: false },
78+
teamId: { type: OptionType.Object, hasSensitiveValue: true },
79+
syncAllFiles: { type: OptionType.Boolean, default: false, hasSensitiveValue: false },
80+
chrome: { type: OptionType.Boolean, hasSensitiveValue: false },
81+
inspector: { type: OptionType.Boolean, hasSensitiveValue: false },
82+
clean: { type: OptionType.Boolean, hasSensitiveValue: false },
83+
watch: { type: OptionType.Boolean, default: true, hasSensitiveValue: false },
84+
background: { type: OptionType.String, hasSensitiveValue: false },
85+
username: { type: OptionType.String, hasSensitiveValue: true },
86+
pluginName: { type: OptionType.String, hasSensitiveValue: false },
87+
hmr: { type: OptionType.Boolean, hasSensitiveValue: false },
88+
collection: { type: OptionType.String, alias: "c", hasSensitiveValue: false },
89+
json: { type: OptionType.Boolean, hasSensitiveValue: false },
90+
avd: { type: OptionType.String, hasSensitiveValue: true },
91+
// check not used
92+
config: { type: OptionType.Array, hasSensitiveValue: false },
93+
insecure: { type: OptionType.Boolean, alias: "k", hasSensitiveValue: false },
94+
debug: { type: OptionType.Boolean, alias: "d", hasSensitiveValue: false },
95+
timeout: { type: OptionType.String, hasSensitiveValue: false },
96+
device: { type: OptionType.String, hasSensitiveValue: true },
97+
availableDevices: { type: OptionType.Boolean, hasSensitiveValue: false },
98+
appid: { type: OptionType.String, hasSensitiveValue: true },
99+
geny: { type: OptionType.String, hasSensitiveValue: true },
100+
debugBrk: { type: OptionType.Boolean, hasSensitiveValue: false },
101+
debugPort: { type: OptionType.Number, hasSensitiveValue: false },
102+
start: { type: OptionType.Boolean, hasSensitiveValue: false },
103+
stop: { type: OptionType.Boolean, hasSensitiveValue: false },
104+
ddi: { type: OptionType.String, hasSensitiveValue: true }, // the path to developer disk image
105+
justlaunch: { type: OptionType.Boolean, hasSensitiveValue: false },
106+
file: { type: OptionType.String, hasSensitiveValue: true },
107+
force: { type: OptionType.Boolean, alias: "f", hasSensitiveValue: false },
108+
// remove legacy
109+
companion: { type: OptionType.Boolean, hasSensitiveValue: false },
110+
emulator: { type: OptionType.Boolean, hasSensitiveValue: false },
111+
sdk: { type: OptionType.String, hasSensitiveValue: false },
112+
template: { type: OptionType.String, hasSensitiveValue: true },
113+
certificate: { type: OptionType.String, hasSensitiveValue: true },
114+
certificatePassword: { type: OptionType.String, hasSensitiveValue: true },
115+
release: { type: OptionType.Boolean, alias: "r", hasSensitiveValue: false },
116+
var: { type: OptionType.Object, hasSensitiveValue: true },
117+
default: { type: OptionType.Boolean, hasSensitiveValue: false },
118+
count: { type: OptionType.Number, hasSensitiveValue: false },
119+
analyticsLogFile: { type: OptionType.String, hasSensitiveValue: true },
120+
hooks: { type: OptionType.Boolean, default: true, hasSensitiveValue: false },
121+
link: { type: OptionType.Boolean, default: false, hasSensitiveValue: false },
122+
aab: { type: OptionType.Boolean, hasSensitiveValue: false }
121123
};
122124
}
123125

test/options.ts

+3-3
Original file line numberDiff line numberDiff line change
@@ -166,7 +166,7 @@ describe("options", () => {
166166
process.argv.push("--test1");
167167
process.argv.push("value");
168168
const options = createOptions(testInjector);
169-
options.validateOptions({ test1: { type: OptionType.String } });
169+
options.validateOptions({ test1: { type: OptionType.String, hasSensitiveValue: false } });
170170
process.argv.pop();
171171
process.argv.pop();
172172
assert.isFalse(isExecutionStopped);
@@ -175,7 +175,7 @@ describe("options", () => {
175175
it("does not break execution when valid commandSpecificOptions are passed and user specifies globally valid option", () => {
176176
const options = createOptions(testInjector);
177177
process.argv.push("--version");
178-
options.validateOptions({ test1: { type: OptionType.String } });
178+
options.validateOptions({ test1: { type: OptionType.String, hasSensitiveValue: false } });
179179
process.argv.pop();
180180
assert.isFalse(isExecutionStopped);
181181
});
@@ -253,7 +253,7 @@ describe("options", () => {
253253
it("does not break execution when dashed option with two dashes is passed", () => {
254254
process.argv.push("--special-dashed-v");
255255
const options = createOptions(testInjector);
256-
options.validateOptions({ specialDashedV: { type: OptionType.Boolean } });
256+
options.validateOptions({ specialDashedV: { type: OptionType.Boolean, hasSensitiveValue: false } });
257257
process.argv.pop();
258258
assert.isFalse(isExecutionStopped, "Dashed options should be validated in specific way. Make sure validation allows yargs specific behavior:" +
259259
"Dashed options (special-dashed-v) are added to yargs.argv in two ways: special-dashed-v and specialDashedV");

test/platform-commands.ts

+3
Original file line numberDiff line numberDiff line change
@@ -179,6 +179,9 @@ function createTestInjector() {
179179
testInjector.register("pacoteService", {
180180
extractPackage: async (packageName: string, destinationDirectory: string, options?: IPacoteExtractOptions): Promise<void> => undefined
181181
});
182+
testInjector.register("optionsTracker", {
183+
trackOptions: () => Promise.resolve(null)
184+
});
182185
testInjector.register("usbLiveSyncService", ({}));
183186

184187
return testInjector;

test/plugins-service.ts

+3
Original file line numberDiff line numberDiff line change
@@ -73,6 +73,9 @@ function createTestInjector() {
7373
testInjector.register("logger", stubs.LoggerStub);
7474
testInjector.register("staticConfig", StaticConfig);
7575
testInjector.register("hooksService", stubs.HooksServiceStub);
76+
testInjector.register("optionsTracker", {
77+
trackOptions: () => Promise.resolve(null)
78+
});
7679
testInjector.register("commandsService", CommandsService);
7780
testInjector.register("commandsServiceProvider", {
7881
registerDynamicSubCommands: () => { /* intentionally empty body */ }

0 commit comments

Comments
 (0)