From e2c839eadcfe1158de09477b3276d68a447347ec Mon Sep 17 00:00:00 2001 From: matthieu-crouzet Date: Mon, 22 Jan 2024 18:45:19 +0100 Subject: [PATCH] feat(metrics): question to activate telemetry + documentation --- docs/telemetry/README.md | 25 ++++++++ package.json | 3 + .../component-extractor/index.spec.ts | 4 +- .../builders/component-extractor/index.ts | 18 ++---- packages/@o3r/extractors/package.json | 2 + packages/@o3r/extractors/src/core/wrapper.ts | 54 +++++++++++++++++- .../@o3r/schematics/src/utility/wrapper.ts | 57 ++++++++++++++++++- .../@o3r/telemetry/schematics/ng-add/index.ts | 17 +++++- packages/@o3r/telemetry/src/builders/index.ts | 29 ++++++++-- .../@o3r/telemetry/src/environment/index.ts | 3 +- .../@o3r/telemetry/src/schematics/index.ts | 25 ++++++-- .../schematics/iframe-to-component/index.ts | 1 - yarn.lock | 25 +++++++- 13 files changed, 227 insertions(+), 36 deletions(-) create mode 100644 docs/telemetry/README.md diff --git a/docs/telemetry/README.md b/docs/telemetry/README.md new file mode 100644 index 0000000000..1930ffeb70 --- /dev/null +++ b/docs/telemetry/README.md @@ -0,0 +1,25 @@ +# 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. + +> [!WARNING] +> We don't use it but your IP address will also be stored during one month for security reason. + +## How to disable telemetry? + +To disable it for: +- your project, set `cli.o3rMetrics` to false in your `package.json`. +- your machine, set `O3R_METRICS` to false in your environment variables. +- a builder/schematic run, run it with `--no-o3r-metrics` diff --git a/package.json b/package.json index 027fa9a736..bb1ecdfe26 100644 --- a/package.json +++ b/package.json @@ -280,5 +280,8 @@ "tools/@*/*", "apps/*" ], + "cli": { + "o3rMetrics": true + }, "packageManager": "yarn@4.0.2" } diff --git a/packages/@o3r/components/builders/component-extractor/index.spec.ts b/packages/@o3r/components/builders/component-extractor/index.spec.ts index 6289736690..6d5c173785 100644 --- a/packages/@o3r/components/builders/component-extractor/index.spec.ts +++ b/packages/@o3r/components/builders/component-extractor/index.spec.ts @@ -47,14 +47,14 @@ describe('Component Extractor Builder', () => { expect(output.error).toBeUndefined(); await run.stop(); - const componentOutput = JSON.parse(virtualFileSystem.readFileSync(options.componentOutputFile, {encoding: 'utf8'})); + const componentOutput = JSON.parse(await virtualFileSystem.promises.readFile(options.componentOutputFile, {encoding: 'utf8'})); expect(typeof componentOutput).toBe('object'); expect(typeof componentOutput.length).toBe('number'); expect(componentOutput[0].library).toBe('showcase'); expect(componentOutput[0].name).toMatch(/.*Component$/); expect(componentOutput[0].path).toMatch(/.*component.ts$/); - const configOutput = JSON.parse(virtualFileSystem.readFileSync(options.configOutputFile, {encoding: 'utf8'})); + const configOutput = JSON.parse(await virtualFileSystem.promises.readFile(options.configOutputFile, {encoding: 'utf8'})); expect(typeof configOutput).toBe('object'); expect(typeof configOutput.length).toBe('number'); expect(configOutput[0].library).toBe('showcase'); diff --git a/packages/@o3r/components/builders/component-extractor/index.ts b/packages/@o3r/components/builders/component-extractor/index.ts index 27cad5bd48..cd6aa2a06f 100644 --- a/packages/@o3r/components/builders/component-extractor/index.ts +++ b/packages/@o3r/components/builders/component-extractor/index.ts @@ -104,21 +104,15 @@ export default createBuilder(createBuilderWithMetricsIfInstalled((resolve, reject) => - fs.writeFile( - path.resolve(context.workspaceRoot, options.componentOutputFile), - options.inline ? JSON.stringify(componentMetadata.components) : JSON.stringify(componentMetadata.components, null, 2), - (err) => err ? reject(err) : resolve() - ) + await fs.promises.writeFile( + path.resolve(context.workspaceRoot, options.componentOutputFile), + options.inline ? JSON.stringify(componentMetadata.components) : JSON.stringify(componentMetadata.components, null, 2) ); context.reportProgress(5, STEP_NUMBER, `Writing configurations in ${options.configOutputFile}`); - await new Promise((resolve, reject) => - fs.writeFile( - path.resolve(context.workspaceRoot, options.configOutputFile), - options.inline ? JSON.stringify(componentMetadata.configurations) : JSON.stringify(componentMetadata.configurations, null, 2), - (err) => err ? reject(err) : resolve() - ) + await fs.promises.writeFile( + path.resolve(context.workspaceRoot, options.configOutputFile), + options.inline ? JSON.stringify(componentMetadata.configurations) : JSON.stringify(componentMetadata.configurations, null, 2) ); } catch (e: any) { return { 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..2be789afaf 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,55 @@ const noopBuilderWrapper: BuilderWrapper = (fn) => fn; * @param builderFn */ export const createBuilderWithMetricsIfInstalled: BuilderWrapper = (builderFn) => async (opts, ctx) => { + const packageJsonPath = path.join(ctx.workspaceRoot, 'package.json'); + const packageJson = existsSync(packageJsonPath) + ? JSON.parse(readFileSync(packageJsonPath, {encoding: 'utf8'}).toString()) + : {}; let wrapper: BuilderWrapper = noopBuilderWrapper; try { const { createBuilderWithMetrics } = await import('@o3r/telemetry'); - wrapper = createBuilderWithMetrics; - } catch {} + if (packageJson.cli?.o3rMetrics) { + wrapper = createBuilderWithMetrics; + } + } catch (e: any) { + // Do not throw if `@o3r/telemetry is not installed + if (packageJson.cli?.o3rMetrics === true) { + ctx.logger.info('`cli.o3rMetrics` is set to true in your package.json, please install the telemetry package with `ng add @o3r/telemetry` to enable the collect of metrics.'); + } else if (typeof packageJson.cli?.o3rMetrics === 'undefined') { + 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(packageJson); + + 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`.'); + + packageJson.cli ||= {}; + packageJson.cli.o3rMetrics = false; + + if (existsSync(packageJsonPath)) { + writeFileSync(packageJsonPath, JSON.stringify(packageJson, 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..c3a2c278c6 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,58 @@ 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 packageJsonPath = '/angular.json'; + const packageJson = tree.exists(packageJsonPath) ? tree.readJson(packageJsonPath) as JsonObject : {}; try { const { createSchematicWithMetrics } = await import('@o3r/telemetry'); - wrapper = createSchematicWithMetrics; + if ((packageJson.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 ((packageJson.cli as JsonObject)?.o3rMetrics === true) { + context.logger.info('`cli.o3rMetrics` is set to true in your package.json, please install the telemetry package with `ng add @o3r/telemetry` to enable the collect of metrics.'); + } else if (context.interactive && (packageJson.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`.'); + + packageJson.cli ||= {}; + (packageJson.cli as JsonObject).o3rMetrics = false; + + tree.overwrite( + packageJsonPath, + JSON.stringify(packageJson, 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..4fcf3a0ae7 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('/package.json')) { + const packageJson = tree.readJson('/package.json') as JsonObject; + packageJson.cli ||= {}; + (packageJson.cli as JsonObject).o3rMetrics = true; + tree.overwrite('/package.json', JSON.stringify(packageJson, 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 'package.json' or by setting 'O3R_METRICS' to false as environment variable. + You can also temporarily deactivate it by running your builder or schematic with '--no-o3r-metrics'. + `); + }; } diff --git a/packages/@o3r/telemetry/src/builders/index.ts b/packages/@o3r/telemetry/src/builders/index.ts index 20eeb1f470..1c4fc12606 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, type SendDataFn } from '../sender'; type BuilderWrapperFn = @@ -59,11 +61,26 @@ export const createBuilderWithMetrics: BuilderWrapper = (builderFn, sendData = d error }; context.logger.debug(JSON.stringify(data, null, 2)); - void sendData(data, context.logger).catch((e) => { - // Do not throw error if we don't manage to collect data - const err = (e instanceof Error ? e : new Error(error)); - context.logger.error(err.stack || err.toString()); - }); + const packageJsonPath = path.join(context.currentDirectory, 'package.json'); + const packageJson = existsSync(packageJsonPath) ? JSON.parse(readFileSync(packageJsonPath, 'utf-8')) : {}; + const shouldSendData = !!( + (options as any).o3rMetrics + ?? ((process.env.O3R_METRICS || '').length > 0 ? process.env.O3R_METRICS !== 'false' : undefined) + ?? packageJson.cli?.o3rMetrics + ); + if (shouldSendData) { + if (typeof ((options as any).o3rMetrics ?? typeof process.env.O3R_METRICS) === 'undefined') { + context.logger.info( + 'Telemetry is globally activated for the project (`cli.o3rMetrics` in package.json).' + + 'If you personnaly don\'t want to send telemetry, you can deactivate it by setting `O3R_METRICS` to false in your environment variables, or calling builder with `--no-o3r-metrics`.' + ); + } + void sendData(data, context.logger).catch((e) => { + // Do not throw error if we don't manage to collect data + const err = (e instanceof Error ? e : new Error(error)); + context.logger.error(err.stack || err.toString()); + }); + } } }; 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 6ed053a75a..27c0002ddf 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'; @@ -48,11 +49,25 @@ export const createSchematicWithMetrics: SchematicWrapper = error }; context.logger.debug(JSON.stringify(data, null, 2)); - void sendData(data, context.logger).catch((e) => { - // Do not throw error if we don't manage to collect data - const err = (e instanceof Error ? e : new Error(error)); - context.logger.error(err.stack || err.toString()); - }); + const packageJson = (tree.exists('/package.json') ? tree.readJson('/package.json') : {}) as JsonObject; + const shouldSendData = !!( + (options as any).o3rMetrics + ?? ((process.env.O3R_METRICS || '').length > 0 ? process.env.O3R_METRICS !== 'false' : undefined) + ?? (packageJson.cli as JsonObject)?.o3rMetrics + ); + if (shouldSendData) { + if (typeof ((options as any).o3rMetrics ?? typeof process.env.O3R_METRICS) === 'undefined') { + context.logger.info( + 'Telemetry is globally activated for the project (`cli.o3rMetrics` in package.json).' + + 'If you personnaly don\'t want to send telemetry, you can deactivate it by setting `O3R_METRICS` to false in your environment variables, or calling schematic with `--no-o3r-metrics`.' + ); + } + void sendData(data, context.logger).catch((e) => { + // Do not throw error if we don't manage to collect data + const err = (e instanceof Error ? e : new Error(error)); + context.logger.error(err.stack || err.toString()); + }); + } } }; 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 57fa03be3a..01de83b1e0 100644 --- a/yarn.lock +++ b/yarn.lock @@ -7488,6 +7488,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" @@ -7499,6 +7500,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" @@ -11702,6 +11704,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" @@ -12176,6 +12188,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" @@ -20863,7 +20884,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: @@ -27945,7 +27966,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: