Skip to content

Commit 1cbd915

Browse files
hanslalexeagle
authored andcommitted
feat(@angular/cli): add support for analytics command proper
To add/remove/prompt about the analytics configuration.
1 parent e96c7ce commit 1cbd915

File tree

5 files changed

+223
-25
lines changed

5 files changed

+223
-25
lines changed

packages/angular/cli/commands.json

+1
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
{
22
"add": "./commands/add.json",
3+
"analytics": "./commands/analytics.json",
34
"build": "./commands/build.json",
45
"config": "./commands/config.json",
56
"doc": "./commands/doc.json",
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,96 @@
1+
/**
2+
* @license
3+
* Copyright Google Inc. All Rights Reserved.
4+
*
5+
* Use of this source code is governed by an MIT-style license that can be
6+
* found in the LICENSE file at https://angular.io/license
7+
*/
8+
import {
9+
promptGlobalAnalytics,
10+
promptProjectAnalytics,
11+
setAnalyticsConfig,
12+
} from '../models/analytics';
13+
import { Command } from '../models/command';
14+
import { Arguments } from '../models/interface';
15+
import { ProjectSetting, Schema as AnalyticsCommandSchema, SettingOrProject } from './analytics';
16+
17+
18+
export class AnalyticsCommand extends Command<AnalyticsCommandSchema> {
19+
public async run(options: AnalyticsCommandSchema & Arguments) {
20+
// Our parser does not support positional enums (won't report invalid parameters). Do the
21+
// validation manually.
22+
// TODO(hansl): fix parser to better support positionals. This would be a breaking change.
23+
if (options.settingOrProject === undefined) {
24+
if (options['--']) {
25+
// The user passed positional arguments but they didn't validate.
26+
this.logger.error(`Argument ${JSON.stringify(options['--'][0])} is invalid.`);
27+
this.logger.error(`Please provide one of the following value: on, off, ci or project.`);
28+
29+
return 1;
30+
} else {
31+
// No argument were passed.
32+
await this.printHelp(options);
33+
34+
return 2;
35+
}
36+
} else if (options.settingOrProject == SettingOrProject.Project
37+
&& options.projectSetting === undefined) {
38+
this.logger.error(`Argument ${JSON.stringify(options.settingOrProject)} requires a second `
39+
+ `argument of one of the following value: on, off.`);
40+
41+
return 2;
42+
}
43+
44+
try {
45+
switch (options.settingOrProject) {
46+
case SettingOrProject.Off:
47+
setAnalyticsConfig('global', false);
48+
break;
49+
50+
case SettingOrProject.On:
51+
setAnalyticsConfig('global', true);
52+
break;
53+
54+
case SettingOrProject.Ci:
55+
setAnalyticsConfig('global', 'ci');
56+
break;
57+
58+
case SettingOrProject.Project:
59+
switch (options.projectSetting) {
60+
case ProjectSetting.Off:
61+
setAnalyticsConfig('local', false);
62+
break;
63+
64+
case ProjectSetting.On:
65+
setAnalyticsConfig('local', true);
66+
break;
67+
68+
case ProjectSetting.Prompt:
69+
await promptProjectAnalytics(true);
70+
break;
71+
72+
default:
73+
await this.printHelp(options);
74+
75+
return 3;
76+
}
77+
break;
78+
79+
case SettingOrProject.Prompt:
80+
await promptGlobalAnalytics(true);
81+
break;
82+
83+
default:
84+
await this.printHelp(options);
85+
86+
return 4;
87+
}
88+
} catch (err) {
89+
this.logger.fatal(err.message);
90+
91+
return 1;
92+
}
93+
94+
return 0;
95+
}
96+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
{
2+
"$schema": "http://json-schema.org/schema",
3+
"$id": "ng-cli://commands/analytics.json",
4+
"description": "Configures usage metric gathering for the Angular CLI. See http://angular.io/MORE_INFO_HERE",
5+
"$longDescription": "",
6+
7+
"$aliases": [],
8+
"$scope": "all",
9+
"$type": "native",
10+
"$impl": "./analytics-impl#AnalyticsCommand",
11+
12+
"type": "object",
13+
"allOf": [
14+
{
15+
"properties": {
16+
"settingOrProject": {
17+
"enum": [
18+
"on",
19+
"off",
20+
"ci",
21+
"project",
22+
"prompt"
23+
],
24+
"description": ".",
25+
"$default": {
26+
"$source": "argv",
27+
"index": 0
28+
}
29+
},
30+
"projectSetting": {
31+
"enum": [
32+
"on",
33+
"off",
34+
"prompt"
35+
],
36+
"description": ".",
37+
"$default": {
38+
"$source": "argv",
39+
"index": 1
40+
}
41+
}
42+
},
43+
"required": [
44+
"settingOrProject"
45+
]
46+
},
47+
{ "$ref": "./definitions.json#/definitions/base" }
48+
]
49+
}

packages/angular/cli/commands/definitions.json

+6-3
Original file line numberDiff line numberDiff line change
@@ -42,13 +42,15 @@
4242
"type": "boolean",
4343
"default": false,
4444
"aliases": [ "d" ],
45-
"description": "When true, runs through and reports activity without writing out results."
45+
"description": "When true, runs through and reports activity without writing out results.",
46+
"x-user-analytics": 1
4647
},
4748
"force": {
4849
"type": "boolean",
4950
"default": false,
5051
"aliases": [ "f" ],
51-
"description": "When true, forces overwriting of existing files."
52+
"description": "When true, forces overwriting of existing files.",
53+
"x-user-analytics": 2
5254
}
5355
}
5456
},
@@ -57,7 +59,8 @@
5759
"interactive": {
5860
"type": "boolean",
5961
"default": "true",
60-
"description": "When false, disables interactive input prompts."
62+
"description": "When false, disables interactive input prompts.",
63+
"x-user-analytics": 3
6164
},
6265
"defaults": {
6366
"type": "boolean",

packages/angular/cli/models/schematic-command.ts

+71-22
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
* found in the LICENSE file at https://angular.io/license
77
*/
88
import {
9+
analytics,
910
experimental,
1011
json,
1112
logging,
@@ -17,20 +18,11 @@ import {
1718
virtualFs,
1819
} from '@angular-devkit/core';
1920
import { NodeJsSyncHost } from '@angular-devkit/core/node';
20-
import {
21-
DryRunEvent,
22-
Engine,
23-
SchematicEngine,
24-
UnsuccessfulWorkflowExecution,
25-
workflow,
26-
} from '@angular-devkit/schematics';
21+
import { DryRunEvent, UnsuccessfulWorkflowExecution, workflow } from '@angular-devkit/schematics';
2722
import {
2823
FileSystemCollection,
29-
FileSystemCollectionDesc,
30-
FileSystemEngineHostBase,
24+
FileSystemEngine,
3125
FileSystemSchematic,
32-
FileSystemSchematicDesc,
33-
NodeModulesEngineHost,
3426
NodeWorkflow,
3527
validateOptionsWithSchema,
3628
} from '@angular-devkit/schematics/tools';
@@ -50,6 +42,12 @@ import { Arguments, CommandContext, CommandDescription, Option } from './interfa
5042
import { parseArguments, parseFreeFormArguments } from './parser';
5143

5244

45+
export const schematicsAnalyticsWhitelist = [
46+
'@schematics/angular',
47+
'@schematics/update',
48+
];
49+
50+
5351
export interface BaseSchematicSchema {
5452
debug?: boolean;
5553
dryRun?: boolean;
@@ -80,8 +78,7 @@ export abstract class SchematicCommand<
8078
readonly allowAdditionalArgs: boolean = false;
8179
private _host = new NodeJsSyncHost();
8280
private _workspace: experimental.workspace.Workspace;
83-
private readonly _engine: Engine<FileSystemCollectionDesc, FileSystemSchematicDesc>;
84-
protected _workflow: workflow.BaseWorkflow;
81+
protected _workflow: NodeWorkflow;
8582

8683
protected collectionName = '@schematics/angular';
8784
protected schematicName?: string;
@@ -90,10 +87,8 @@ export abstract class SchematicCommand<
9087
context: CommandContext,
9188
description: CommandDescription,
9289
logger: logging.Logger,
93-
private readonly _engineHost: FileSystemEngineHostBase = new NodeModulesEngineHost(),
9490
) {
9591
super(context, description, logger);
96-
this._engine = new SchematicEngine(this._engineHost);
9792
}
9893

9994
public async initialize(options: T & Arguments) {
@@ -110,6 +105,15 @@ export abstract class SchematicCommand<
110105
);
111106

112107
this.description.options.push(...options.filter(x => !x.hidden));
108+
109+
// Remove any user analytics from schematics that are NOT part of our whitelist.
110+
for (const o of this.description.options) {
111+
if (o.userAnalytics) {
112+
if (!schematicsAnalyticsWhitelist.includes(this.collectionName)) {
113+
o.userAnalytics = undefined;
114+
}
115+
}
116+
}
113117
}
114118
}
115119

@@ -198,12 +202,8 @@ export abstract class SchematicCommand<
198202
}
199203
}
200204

201-
protected getEngineHost() {
202-
return this._engineHost;
203-
}
204-
protected getEngine():
205-
Engine<FileSystemCollectionDesc, FileSystemSchematicDesc> {
206-
return this._engine;
205+
protected getEngine(): FileSystemEngine {
206+
return this._workflow.engine;
207207
}
208208

209209
protected getCollection(collectionName: string): FileSystemCollection {
@@ -260,8 +260,57 @@ export abstract class SchematicCommand<
260260
root: normalize(this.workspace.root),
261261
},
262262
);
263+
workflow.engineHost.registerContextTransform(context => {
264+
// This is run by ALL schematics, so if someone uses `externalSchematics(...)` which
265+
// is whitelisted, it would move to the right analytics (even if their own isn't).
266+
const collectionName: string = context.schematic.collection.description.name;
267+
if (schematicsAnalyticsWhitelist.includes(collectionName)) {
268+
return {
269+
...context,
270+
analytics: this.analytics,
271+
};
272+
} else {
273+
return {
274+
...context,
275+
analytics: new analytics.NoopAnalytics(),
276+
};
277+
}
278+
});
263279

264-
this._engineHost.registerOptionsTransform(validateOptionsWithSchema(workflow.registry));
280+
workflow.engineHost.registerOptionsTransform(validateOptionsWithSchema(workflow.registry));
281+
282+
// This needs to be the last transform as it reports the flags to analytics (if enabled).
283+
workflow.engineHost.registerOptionsTransform(async (
284+
schematic,
285+
options: { [prop: string]: number | string },
286+
context,
287+
): Promise<{ [prop: string]: number | string }> => {
288+
const analytics = context && context.analytics;
289+
if (!schematic.schemaJson || !context || !analytics) {
290+
return options;
291+
}
292+
293+
const collectionName = context.schematic.collection.description.name;
294+
const schematicName = context.schematic.description.name;
295+
296+
if (!schematicsAnalyticsWhitelist.includes(collectionName)) {
297+
return options;
298+
}
299+
300+
const args = await parseJsonSchemaToOptions(this._workflow.registry, schematic.schemaJson);
301+
const dimensions: (boolean | number | string)[] = [];
302+
for (const option of args) {
303+
const ua = option.userAnalytics;
304+
305+
if (option.name in options && ua) {
306+
dimensions[ua] = options[option.name];
307+
}
308+
}
309+
310+
analytics.event('schematics', collectionName + ':' + schematicName, { dimensions });
311+
312+
return options;
313+
});
265314

266315
if (options.defaults) {
267316
workflow.registry.addPreTransform(schema.transforms.addUndefinedDefaults);

0 commit comments

Comments
 (0)