diff --git a/docs/telemetry/README.md b/docs/telemetry/README.md new file mode 100644 index 0000000000..fa3ee25a48 --- /dev/null +++ b/docs/telemetry/README.md @@ -0,0 +1,20 @@ +# Telemetry + +You can help the Otter Team to prioritize features and improvements by permitting the Otter team to send command-line command usage statistics to Amadeus. The Otter Team does not collect usage statistics unless you explicitly opt in. + +## What is collected? + +Usage analytics may include the following information: +- Your operating system (macOS, Linux distribution, Windows) and its version. +- Package manager name and version (local version only). +- Node.js version (local version only). +- Otter version (local version only). +- Command name that was run. +- The time it took to run. +- Project name. +- The schematic/builder options. + +## How to disable telemetry? + +To globally disable it, you can set `cli.o3rMetrics` to false in your `angular.json` (or `nx.json`). +To disable it only for a run, you can run your builder/schematic with `--o3r-metrics=false` diff --git a/nx.json b/nx.json index c9e2c42402..e36555460a 100644 --- a/nx.json +++ b/nx.json @@ -16,7 +16,8 @@ "enable": true, "environment": "all", "path": ".cache/angular" - } + }, + "o3rMetrics": true }, "namedInputs": { "global": [ diff --git a/packages/@o3r/extractors/package.json b/packages/@o3r/extractors/package.json index c94735c409..d55f41d6dd 100644 --- a/packages/@o3r/extractors/package.json +++ b/packages/@o3r/extractors/package.json @@ -37,6 +37,7 @@ }, "dependencies": { "@microsoft/tsdoc": "~0.14.1", + "inquirer": "~8.2.6", "jsonschema": "~1.4.1", "tslib": "^2.5.3", "typedoc": "~0.25.0" @@ -67,6 +68,7 @@ "@o3r/schematics": "workspace:^", "@o3r/telemetry": "workspace:^", "@o3r/test-helpers": "workspace:^", + "@types/inquirer": "~8.2.10", "@types/jest": "~29.5.2", "@types/node": "^20.0.0", "@typescript-eslint/eslint-plugin": "^6.19.0", diff --git a/packages/@o3r/extractors/src/core/wrapper.ts b/packages/@o3r/extractors/src/core/wrapper.ts index aa2835ca55..3eec3f1d27 100644 --- a/packages/@o3r/extractors/src/core/wrapper.ts +++ b/packages/@o3r/extractors/src/core/wrapper.ts @@ -1,4 +1,9 @@ +import { getPackageManagerRunner } from '@o3r/schematics'; import type { BuilderWrapper } from '@o3r/telemetry'; +import { prompt, Question } from 'inquirer'; +import { execSync } from 'node:child_process'; +import { existsSync, readFileSync, writeFileSync } from 'node:fs'; +import * as path from 'node:path'; const noopBuilderWrapper: BuilderWrapper = (fn) => fn; @@ -8,10 +13,61 @@ const noopBuilderWrapper: BuilderWrapper = (fn) => fn; * @param builderFn */ export const createBuilderWithMetricsIfInstalled: BuilderWrapper = (builderFn) => async (opts, ctx) => { + const angularJsonPath = path.join(ctx.workspaceRoot, 'angular.json'); + const nxJsonPath = path.join(ctx.workspaceRoot, 'nx.json'); + const configPath = existsSync(angularJsonPath) + ? angularJsonPath + : existsSync(nxJsonPath) + ? nxJsonPath + : ''; + const configJson = JSON.parse( + existsSync(configPath) + ? readFileSync(configPath, {encoding: 'utf8'}).toString() + : '{}' + ); let wrapper: BuilderWrapper = noopBuilderWrapper; try { const { createBuilderWithMetrics } = await import('@o3r/telemetry'); - wrapper = createBuilderWithMetrics; - } catch {} + if (configJson.cli?.o3rMetrics) { + wrapper = createBuilderWithMetrics; + } + } catch (e: any) { + // Do not throw if `@o3r/telemetry is not installed + if (configJson.cli?.o3rMetrics !== false) { + ctx.logger.debug('`@o3r/telemetry` is not available.\nAsking to add the dependency\n' + e.toString()); + + const question: Question = { + type: 'confirm', + name: 'isReplyPositive', + message: ` + Would you like to share anonymous usage data about this project with the Otter Team at Amadeus ? + It will help us to improve our tools. + For more details and instructions on how to change these settings, see https://github.com/AmadeusITGroup/otter/blob/main/docs/telemetry/README.md. + ` + }; + const { isReplyPositive } = await prompt([question]); + + if (isReplyPositive) { + const pmr = getPackageManagerRunner(configJson); + + try { + const version = JSON.parse(readFileSync(path.join(__dirname, '..', '..', 'package.json'), 'utf-8').toString()).version; + execSync(`${pmr} ng add @o3r/telemetry@${version}`); + } catch {} + + const { createBuilderWithMetrics } = await import('@o3r/telemetry'); + return createBuilderWithMetrics(builderFn)(opts, ctx); + } else { + ctx.logger.info('You can activate it at any time by running `ng add @o3r/telemetry`.'); + + configJson.cli ||= {}; + configJson.cli.o3rMetrics = false; + + if (existsSync(configPath)) { + writeFileSync(configPath, JSON.stringify(configJson, null, 2)); + } + } + } + } return wrapper(builderFn)(opts, ctx); }; diff --git a/packages/@o3r/schematics/src/utility/wrapper.ts b/packages/@o3r/schematics/src/utility/wrapper.ts index a90943bf93..2d098753f7 100644 --- a/packages/@o3r/schematics/src/utility/wrapper.ts +++ b/packages/@o3r/schematics/src/utility/wrapper.ts @@ -1,4 +1,11 @@ +import type { JsonObject } from '@angular-devkit/core'; +import { askConfirmation } from '@angular/cli/src/utilities/prompt'; import type { SchematicWrapper } from '@o3r/telemetry'; +import { NodeDependencyType } from '@schematics/angular/utility/dependencies'; +import { readFileSync } from 'node:fs'; +import * as path from 'node:path'; +import { lastValueFrom } from 'rxjs'; +import { NodePackageNgAddTask } from '../tasks/index'; const noopSchematicWrapper: SchematicWrapper = (fn) => fn; @@ -7,14 +14,56 @@ const noopSchematicWrapper: SchematicWrapper = (fn) => fn; * if @o3r/telemetry is installed * @param schematicFn */ -export const createSchematicWithMetricsIfInstalled: SchematicWrapper = (schematicFn) => (opts) => async (_, context) => { +export const createSchematicWithMetricsIfInstalled: SchematicWrapper = (schematicFn) => (opts) => async (tree, context) => { let wrapper: SchematicWrapper = noopSchematicWrapper; + const angularJsonPath = '/angular.json'; + const angularJson = tree.exists(angularJsonPath) ? tree.readJson(angularJsonPath) as JsonObject : {}; try { const { createSchematicWithMetrics } = await import('@o3r/telemetry'); - wrapper = createSchematicWithMetrics; + if ((angularJson.cli as JsonObject)?.o3rMetrics) { + wrapper = createSchematicWithMetrics; + } } catch (e: any) { // Do not throw if `@o3r/telemetry is not installed - context.logger.debug('`@o3r/telemetry` is not available\n' + e.toString()); + if (context.interactive && (angularJson.cli as JsonObject)?.o3rMetrics !== false) { + context.logger.debug('`@o3r/telemetry` is not available.\nAsking to add the dependency\n' + e.toString()); + + const isReplyPositive = await askConfirmation( + ` + Would you like to share anonymous usage data about this project with the Otter Team at Amadeus ? + It will help us to improve our tools. + For more details and instructions on how to change these settings, see https://github.com/AmadeusITGroup/otter/blob/main/docs/telemetry/README.md. + `, + false + ); + + if (isReplyPositive) { + const version = JSON.parse(readFileSync(path.join(__dirname, '..', '..', 'package.json'), 'utf-8').toString()).version; + context.addTask( + new NodePackageNgAddTask( + '@o3r/telemetry', + { + dependencyType: NodeDependencyType.Dev, + version + } + ) + ); + await lastValueFrom(context.engine.executePostTasks()); + + const { createSchematicWithMetrics } = await import('@o3r/telemetry'); + return createSchematicWithMetrics(schematicFn)(opts); + } else { + context.logger.info('You can activate it at any time by running `ng add @o3r/telemetry`.'); + + angularJson.cli ||= {}; + (angularJson.cli as JsonObject).o3rMetrics = false; + + tree.overwrite( + tree.exists('/nx.json') ? '/nx.json' : angularJsonPath, + JSON.stringify(angularJson, null, 2) + ); + } + } } return wrapper(schematicFn)(opts); }; diff --git a/packages/@o3r/telemetry/schematics/ng-add/index.ts b/packages/@o3r/telemetry/schematics/ng-add/index.ts index d26ce60c21..fd1526c932 100644 --- a/packages/@o3r/telemetry/schematics/ng-add/index.ts +++ b/packages/@o3r/telemetry/schematics/ng-add/index.ts @@ -1,4 +1,5 @@ -import { noop, Rule } from '@angular-devkit/schematics'; +import type { JsonObject } from '@angular-devkit/core'; +import type { Rule } from '@angular-devkit/schematics'; import type { NgAddSchematicsSchema } from './schema'; /** @@ -6,5 +7,17 @@ import type { NgAddSchematicsSchema } from './schema'; * @param options */ export function ngAdd(_options: NgAddSchematicsSchema): Rule { - return noop(); + return (tree, context) => { + if (tree.exists('/angular.json')) { + const angularJson = tree.readJson('/angular.json') as JsonObject; + angularJson.cli ||= {}; + (angularJson.cli as JsonObject).o3rMetrics = true; + tree.overwrite('/angular.json', JSON.stringify(angularJson, null, 2)); + } + context.logger.info(` + By installing '@o3r/telemetry', you have activated the collect of anonymous usage data. + You can deactivate it at any time by changing 'cli.o3rMetrics' in '${tree.exists('/nx.json') ? 'nx' : 'angular'}.json'. + You can also temporarily deactivate it by running your builder or schematic with '--o3r-metrics=false'. + `); + }; } diff --git a/packages/@o3r/telemetry/src/builders/index.ts b/packages/@o3r/telemetry/src/builders/index.ts index 38fe3d61c3..225f286eda 100644 --- a/packages/@o3r/telemetry/src/builders/index.ts +++ b/packages/@o3r/telemetry/src/builders/index.ts @@ -2,8 +2,10 @@ import type { BuilderContext, BuilderOutput } from '@angular-devkit/architect'; -import { getEnvironmentInfo } from '../environment/index'; +import { existsSync, readFileSync } from 'node:fs'; +import * as path from 'node:path'; import { performance } from 'node:perf_hooks'; +import { getEnvironmentInfo } from '../environment/index'; import { BuilderMetricData, sendData as defaultSendData } from '../sender'; type BuilderWrapperFn = @@ -61,7 +63,19 @@ export const createBuilderWithMetrics: BuilderWrapper = (builderFn, sendData = d error }; context.logger.debug(JSON.stringify(data, null, 2)); - await sendData(data, context.logger); + let shouldSendData = !!(options as any).o3rMetrics; + const nxJsonPath = path.join(context.currentDirectory, 'nx.json'); + const angularJsonPath = path.join(context.currentDirectory, 'angular.json'); + if (shouldSendData && existsSync(nxJsonPath)) { + const nxJson = JSON.parse(readFileSync(nxJsonPath, 'utf-8')); + shouldSendData &&= !!nxJson.cli.o3rMetrics; + } else if (shouldSendData && existsSync(angularJsonPath)) { + const angularJson = JSON.parse(readFileSync(angularJsonPath, 'utf-8')); + shouldSendData &&= !!angularJson.cli.o3rMetrics; + } + if (shouldSendData) { + await sendData(data, context.logger); + } } catch (e: any) { // Do not throw error if we don't manage to collect data const err = (e instanceof Error ? e : new Error(error)); diff --git a/packages/@o3r/telemetry/src/environment/index.ts b/packages/@o3r/telemetry/src/environment/index.ts index 0cf5fcc22c..8576402728 100644 --- a/packages/@o3r/telemetry/src/environment/index.ts +++ b/packages/@o3r/telemetry/src/environment/index.ts @@ -153,7 +153,8 @@ export const getEnvironmentInfo = (): EnvironmentMetricData => { os: osInfo, node: nodeInfo, packageManager: packageManagerInfo, - otter: otterInfo, ci, + otter: otterInfo, + ci, ...(projectName ? { project: { name: projectName } } : {}) }; }; diff --git a/packages/@o3r/telemetry/src/schematics/index.ts b/packages/@o3r/telemetry/src/schematics/index.ts index 90ba178fc6..860b53e6f6 100644 --- a/packages/@o3r/telemetry/src/schematics/index.ts +++ b/packages/@o3r/telemetry/src/schematics/index.ts @@ -1,3 +1,4 @@ +import type { JsonObject } from '@angular-devkit/core'; import { callRule, Rule } from '@angular-devkit/schematics'; import { performance } from 'node:perf_hooks'; import { lastValueFrom } from 'rxjs'; @@ -47,7 +48,17 @@ export const createSchematicWithMetrics: SchematicWrapper = error }; context.logger.debug(JSON.stringify(data, null, 2)); - await sendData(data, context.logger); + let shouldSendData = !!(options as any).o3rMetrics; + if (shouldSendData && tree.exists('/nx.json')) { + const nxJson = tree.readJson('/nx.json') as JsonObject; + shouldSendData &&= !!(nxJson.cli as JsonObject).o3rMetrics; + } else if (shouldSendData && tree.exists('/angular.json')) { + const angularJson = tree.readJson('/angular.json') as JsonObject; + shouldSendData &&= !!(angularJson.cli as JsonObject).o3rMetrics; + } + if (shouldSendData) { + await sendData(data, context.logger); + } } catch (e: any) { // Do not throw error if we don't manage to collect data const err = (e instanceof Error ? e : new Error(error)); diff --git a/packages/@o3r/third-party/schematics/iframe-to-component/index.ts b/packages/@o3r/third-party/schematics/iframe-to-component/index.ts index 4c907b8c87..8cea814ed3 100644 --- a/packages/@o3r/third-party/schematics/iframe-to-component/index.ts +++ b/packages/@o3r/third-party/schematics/iframe-to-component/index.ts @@ -225,7 +225,6 @@ export function ngAddIframeFn(options: NgAddIframeSchematicsSchema): Rule { options.skipLinter ? noop() : applyEsLintFix() ]); } catch (e) { - debugger; if (e instanceof NoOtterComponent && context.interactive) { const shouldConvertComponent = await askConfirmationToConvertComponent(); if (shouldConvertComponent) { diff --git a/yarn.lock b/yarn.lock index 41221c99be..e44b5c4a85 100644 --- a/yarn.lock +++ b/yarn.lock @@ -7305,6 +7305,7 @@ __metadata: "@o3r/schematics": "workspace:^" "@o3r/telemetry": "workspace:^" "@o3r/test-helpers": "workspace:^" + "@types/inquirer": "npm:~8.2.10" "@types/jest": "npm:~29.5.2" "@types/node": "npm:^20.0.0" "@typescript-eslint/eslint-plugin": "npm:^6.19.0" @@ -7316,6 +7317,7 @@ __metadata: eslint-plugin-jsdoc: "npm:~48.0.0" eslint-plugin-prefer-arrow: "npm:~1.2.3" eslint-plugin-unicorn: "npm:^50.0.0" + inquirer: "npm:~8.2.6" intl-messageformat: "npm:~10.5.1" jest: "npm:~29.7.0" jest-environment-jsdom: "npm:~29.7.0" @@ -11512,6 +11514,16 @@ __metadata: languageName: node linkType: hard +"@types/inquirer@npm:~8.2.10": + version: 8.2.10 + resolution: "@types/inquirer@npm:8.2.10" + dependencies: + "@types/through": "npm:*" + rxjs: "npm:^7.2.0" + checksum: d7c0c5ec95af583191942ac33f8af2eb1fe839da6b4560277a8c251fa289f2dd3a5d14850baf910343700646200258ecff89dc9e1d57df29c16a1082d91a5ae3 + languageName: node + linkType: hard + "@types/ioredis@npm:^4.27.1": version: 4.28.10 resolution: "@types/ioredis@npm:4.28.10" @@ -11986,6 +11998,15 @@ __metadata: languageName: node linkType: hard +"@types/through@npm:*": + version: 0.0.33 + resolution: "@types/through@npm:0.0.33" + dependencies: + "@types/node": "npm:*" + checksum: fd0b73f873a64ed5366d1d757c42e5dbbb2201002667c8958eda7ca02fff09d73de91360572db465ee00240c32d50c6039ea736d8eca374300f9664f93e8da39 + languageName: node + linkType: hard + "@types/tough-cookie@npm:*": version: 4.0.5 resolution: "@types/tough-cookie@npm:4.0.5" @@ -20609,7 +20630,7 @@ __metadata: languageName: node linkType: hard -"inquirer@npm:8.2.6": +"inquirer@npm:8.2.6, inquirer@npm:~8.2.6": version: 8.2.6 resolution: "inquirer@npm:8.2.6" dependencies: @@ -27686,7 +27707,7 @@ __metadata: languageName: node linkType: hard -"rxjs@npm:7.8.1, rxjs@npm:^7.5.5, rxjs@npm:^7.8.0, rxjs@npm:^7.8.1": +"rxjs@npm:7.8.1, rxjs@npm:^7.2.0, rxjs@npm:^7.5.5, rxjs@npm:^7.8.0, rxjs@npm:^7.8.1": version: 7.8.1 resolution: "rxjs@npm:7.8.1" dependencies: