Skip to content

Commit

Permalink
feat(@angular/cli): add support for analytics command proper
Browse files Browse the repository at this point in the history
To add/remove/prompt about the analytics configuration.
  • Loading branch information
hansl authored and alexeagle committed Mar 20, 2019
1 parent e96c7ce commit 1cbd915
Show file tree
Hide file tree
Showing 5 changed files with 223 additions and 25 deletions.
1 change: 1 addition & 0 deletions packages/angular/cli/commands.json
@@ -1,5 +1,6 @@
{
"add": "./commands/add.json",
"analytics": "./commands/analytics.json",
"build": "./commands/build.json",
"config": "./commands/config.json",
"doc": "./commands/doc.json",
Expand Down
96 changes: 96 additions & 0 deletions packages/angular/cli/commands/analytics-impl.ts
@@ -0,0 +1,96 @@
/**
* @license
* Copyright Google Inc. All Rights Reserved.
*
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.io/license
*/
import {
promptGlobalAnalytics,
promptProjectAnalytics,
setAnalyticsConfig,
} from '../models/analytics';
import { Command } from '../models/command';
import { Arguments } from '../models/interface';
import { ProjectSetting, Schema as AnalyticsCommandSchema, SettingOrProject } from './analytics';


export class AnalyticsCommand extends Command<AnalyticsCommandSchema> {
public async run(options: AnalyticsCommandSchema & Arguments) {
// Our parser does not support positional enums (won't report invalid parameters). Do the
// validation manually.
// TODO(hansl): fix parser to better support positionals. This would be a breaking change.
if (options.settingOrProject === undefined) {
if (options['--']) {
// The user passed positional arguments but they didn't validate.
this.logger.error(`Argument ${JSON.stringify(options['--'][0])} is invalid.`);
this.logger.error(`Please provide one of the following value: on, off, ci or project.`);

return 1;
} else {
// No argument were passed.
await this.printHelp(options);

return 2;
}
} else if (options.settingOrProject == SettingOrProject.Project
&& options.projectSetting === undefined) {
this.logger.error(`Argument ${JSON.stringify(options.settingOrProject)} requires a second `
+ `argument of one of the following value: on, off.`);

return 2;
}

try {
switch (options.settingOrProject) {
case SettingOrProject.Off:
setAnalyticsConfig('global', false);
break;

case SettingOrProject.On:
setAnalyticsConfig('global', true);
break;

case SettingOrProject.Ci:
setAnalyticsConfig('global', 'ci');
break;

case SettingOrProject.Project:
switch (options.projectSetting) {
case ProjectSetting.Off:
setAnalyticsConfig('local', false);
break;

case ProjectSetting.On:
setAnalyticsConfig('local', true);
break;

case ProjectSetting.Prompt:
await promptProjectAnalytics(true);
break;

default:
await this.printHelp(options);

return 3;
}
break;

case SettingOrProject.Prompt:
await promptGlobalAnalytics(true);
break;

default:
await this.printHelp(options);

return 4;
}
} catch (err) {
this.logger.fatal(err.message);

return 1;
}

return 0;
}
}
49 changes: 49 additions & 0 deletions packages/angular/cli/commands/analytics.json
@@ -0,0 +1,49 @@
{
"$schema": "http://json-schema.org/schema",
"$id": "ng-cli://commands/analytics.json",
"description": "Configures usage metric gathering for the Angular CLI. See http://angular.io/MORE_INFO_HERE",
"$longDescription": "",

"$aliases": [],
"$scope": "all",
"$type": "native",
"$impl": "./analytics-impl#AnalyticsCommand",

"type": "object",
"allOf": [
{
"properties": {
"settingOrProject": {
"enum": [
"on",
"off",
"ci",
"project",
"prompt"
],
"description": ".",
"$default": {
"$source": "argv",
"index": 0
}
},
"projectSetting": {
"enum": [
"on",
"off",
"prompt"
],
"description": ".",
"$default": {
"$source": "argv",
"index": 1
}
}
},
"required": [
"settingOrProject"
]
},
{ "$ref": "./definitions.json#/definitions/base" }
]
}
9 changes: 6 additions & 3 deletions packages/angular/cli/commands/definitions.json
Expand Up @@ -42,13 +42,15 @@
"type": "boolean",
"default": false,
"aliases": [ "d" ],
"description": "When true, runs through and reports activity without writing out results."
"description": "When true, runs through and reports activity without writing out results.",
"x-user-analytics": 1
},
"force": {
"type": "boolean",
"default": false,
"aliases": [ "f" ],
"description": "When true, forces overwriting of existing files."
"description": "When true, forces overwriting of existing files.",
"x-user-analytics": 2
}
}
},
Expand All @@ -57,7 +59,8 @@
"interactive": {
"type": "boolean",
"default": "true",
"description": "When false, disables interactive input prompts."
"description": "When false, disables interactive input prompts.",
"x-user-analytics": 3
},
"defaults": {
"type": "boolean",
Expand Down
93 changes: 71 additions & 22 deletions packages/angular/cli/models/schematic-command.ts
Expand Up @@ -6,6 +6,7 @@
* found in the LICENSE file at https://angular.io/license
*/
import {
analytics,
experimental,
json,
logging,
Expand All @@ -17,20 +18,11 @@ import {
virtualFs,
} from '@angular-devkit/core';
import { NodeJsSyncHost } from '@angular-devkit/core/node';
import {
DryRunEvent,
Engine,
SchematicEngine,
UnsuccessfulWorkflowExecution,
workflow,
} from '@angular-devkit/schematics';
import { DryRunEvent, UnsuccessfulWorkflowExecution, workflow } from '@angular-devkit/schematics';
import {
FileSystemCollection,
FileSystemCollectionDesc,
FileSystemEngineHostBase,
FileSystemEngine,
FileSystemSchematic,
FileSystemSchematicDesc,
NodeModulesEngineHost,
NodeWorkflow,
validateOptionsWithSchema,
} from '@angular-devkit/schematics/tools';
Expand All @@ -50,6 +42,12 @@ import { Arguments, CommandContext, CommandDescription, Option } from './interfa
import { parseArguments, parseFreeFormArguments } from './parser';


export const schematicsAnalyticsWhitelist = [
'@schematics/angular',
'@schematics/update',
];


export interface BaseSchematicSchema {
debug?: boolean;
dryRun?: boolean;
Expand Down Expand Up @@ -80,8 +78,7 @@ export abstract class SchematicCommand<
readonly allowAdditionalArgs: boolean = false;
private _host = new NodeJsSyncHost();
private _workspace: experimental.workspace.Workspace;
private readonly _engine: Engine<FileSystemCollectionDesc, FileSystemSchematicDesc>;
protected _workflow: workflow.BaseWorkflow;
protected _workflow: NodeWorkflow;

protected collectionName = '@schematics/angular';
protected schematicName?: string;
Expand All @@ -90,10 +87,8 @@ export abstract class SchematicCommand<
context: CommandContext,
description: CommandDescription,
logger: logging.Logger,
private readonly _engineHost: FileSystemEngineHostBase = new NodeModulesEngineHost(),
) {
super(context, description, logger);
this._engine = new SchematicEngine(this._engineHost);
}

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

this.description.options.push(...options.filter(x => !x.hidden));

// Remove any user analytics from schematics that are NOT part of our whitelist.
for (const o of this.description.options) {
if (o.userAnalytics) {
if (!schematicsAnalyticsWhitelist.includes(this.collectionName)) {
o.userAnalytics = undefined;
}
}
}
}
}

Expand Down Expand Up @@ -198,12 +202,8 @@ export abstract class SchematicCommand<
}
}

protected getEngineHost() {
return this._engineHost;
}
protected getEngine():
Engine<FileSystemCollectionDesc, FileSystemSchematicDesc> {
return this._engine;
protected getEngine(): FileSystemEngine {
return this._workflow.engine;
}

protected getCollection(collectionName: string): FileSystemCollection {
Expand Down Expand Up @@ -260,8 +260,57 @@ export abstract class SchematicCommand<
root: normalize(this.workspace.root),
},
);
workflow.engineHost.registerContextTransform(context => {
// This is run by ALL schematics, so if someone uses `externalSchematics(...)` which
// is whitelisted, it would move to the right analytics (even if their own isn't).
const collectionName: string = context.schematic.collection.description.name;
if (schematicsAnalyticsWhitelist.includes(collectionName)) {
return {
...context,
analytics: this.analytics,
};
} else {
return {
...context,
analytics: new analytics.NoopAnalytics(),
};
}
});

this._engineHost.registerOptionsTransform(validateOptionsWithSchema(workflow.registry));
workflow.engineHost.registerOptionsTransform(validateOptionsWithSchema(workflow.registry));

// This needs to be the last transform as it reports the flags to analytics (if enabled).
workflow.engineHost.registerOptionsTransform(async (
schematic,
options: { [prop: string]: number | string },
context,
): Promise<{ [prop: string]: number | string }> => {
const analytics = context && context.analytics;
if (!schematic.schemaJson || !context || !analytics) {
return options;
}

const collectionName = context.schematic.collection.description.name;
const schematicName = context.schematic.description.name;

if (!schematicsAnalyticsWhitelist.includes(collectionName)) {
return options;
}

const args = await parseJsonSchemaToOptions(this._workflow.registry, schematic.schemaJson);
const dimensions: (boolean | number | string)[] = [];
for (const option of args) {
const ua = option.userAnalytics;

if (option.name in options && ua) {
dimensions[ua] = options[option.name];
}
}

analytics.event('schematics', collectionName + ':' + schematicName, { dimensions });

return options;
});

if (options.defaults) {
workflow.registry.addPreTransform(schema.transforms.addUndefinedDefaults);
Expand Down

0 comments on commit 1cbd915

Please sign in to comment.