diff --git a/docs/telemetry/README.md b/docs/telemetry/README.md new file mode 100644 index 0000000000..2c10e102aa --- /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 `config.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 e634e95f7f..e3b6a88c3c 100644 --- a/package.json +++ b/package.json @@ -281,5 +281,8 @@ "tools/@*/*", "apps/*" ], + "config": { + "o3rMetrics": true + }, "packageManager": "yarn@4.1.0" } diff --git a/packages/@ama-sdk/create/src/index.it.spec.ts b/packages/@ama-sdk/create/src/index.it.spec.ts index fa9a9caa8d..88341688ce 100644 --- a/packages/@ama-sdk/create/src/index.it.spec.ts +++ b/packages/@ama-sdk/create/src/index.it.spec.ts @@ -51,7 +51,7 @@ describe('Create new sdk command', () => { test('should generate a full SDK when the specification is provided', () => { expect(() => - packageManagerCreate(`@ama-sdk typescript ${sdkPackageName} --package-manager ${packageManager} --spec-path ./swagger-spec.yml`, execAppOptions)).not.toThrow(); + packageManagerCreate(`@ama-sdk typescript ${sdkPackageName} --package-manager ${packageManager} --spec-path ./swagger-spec.yml --force`, execAppOptions)).not.toThrow(); expect(() => packageManagerRun('build', { ...execAppOptions, cwd: sdkPackagePath })).not.toThrow(); }); diff --git a/packages/@ama-sdk/schematics/schematics/code-generator/code-generator.ts b/packages/@ama-sdk/schematics/schematics/code-generator/code-generator.ts index 334520be3e..353c95d0ab 100644 --- a/packages/@ama-sdk/schematics/schematics/code-generator/code-generator.ts +++ b/packages/@ama-sdk/schematics/schematics/code-generator/code-generator.ts @@ -1,5 +1,6 @@ -import { chain, SchematicContext, TaskConfiguration, TaskConfigurationGenerator, TaskExecutor, Tree } from '@angular-devkit/schematics'; +import { chain, Rule, SchematicContext, TaskConfiguration, TaskConfigurationGenerator, TaskExecutor, Tree } from '@angular-devkit/schematics'; import { NodeModulesEngineHost } from '@angular-devkit/schematics/tools'; +import { lastValueFrom } from 'rxjs'; /** * Common configuration for all the code generators @@ -95,9 +96,9 @@ export class CodeGenerator { * @param factoryOptions.rootDirectory directory where all your commands will be run - fallback to process.cwd */ public getGeneratorRunSchematic(generatorOptions: Partial, factoryOptions: { rootDirectory?: string } = {}) { - const scheduleTask = (tree: Tree, context: SchematicContext) => { + const scheduleTask: Rule = async (_: Tree, context: SchematicContext) => { context.addTask(this.getTaskConfiguration(generatorOptions)); - return tree; + await lastValueFrom(context.engine.executePostTasks()); }; return chain([ this.registerGeneratorExecutor(factoryOptions), diff --git a/packages/@ama-sdk/schematics/schematics/ng-add/index.ts b/packages/@ama-sdk/schematics/schematics/ng-add/index.ts index 3b33baedff..6cc72c6a8d 100644 --- a/packages/@ama-sdk/schematics/schematics/ng-add/index.ts +++ b/packages/@ama-sdk/schematics/schematics/ng-add/index.ts @@ -144,9 +144,9 @@ const registerPackageSchematics = async (tree: Tree, context: SchematicContext) hideOutput: false, quiet: false } as any)); - const packageJsonContent = tree.readJson('package.json') as PackageJson; + const packageJsonContent = tree.readJson('/package.json') as PackageJson; packageJsonContent.devDependencies = {...packageJsonContent.devDependencies, [dependency]: amaSdkSchematicsVersion}; - tree.overwrite('package.json', JSON.stringify(packageJsonContent, null, 2)); + tree.overwrite('/package.json', JSON.stringify(packageJsonContent, null, 2)); await lastValueFrom(context.engine.executePostTasks()); } return () => chain([ diff --git a/packages/@o3r/extractors/package.json b/packages/@o3r/extractors/package.json index 1b6fe23c94..510683bd5b 100644 --- a/packages/@o3r/extractors/package.json +++ b/packages/@o3r/extractors/package.json @@ -31,15 +31,13 @@ "typescript": "~5.2.2" }, "peerDependenciesMeta": { - "@o3r/schematics": { - "optional": true - }, "@o3r/telemetry": { "optional": true } }, "dependencies": { "@microsoft/tsdoc": "~0.14.1", + "inquirer": "~8.2.6", "jsonschema": "~1.4.1", "tslib": "^2.5.3", "typedoc": "~0.25.0" @@ -71,6 +69,7 @@ "@o3r/telemetry": "workspace:^", "@o3r/test-helpers": "workspace:^", "@stylistic/eslint-plugin-ts": "^1.5.4", + "@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..8027341b20 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 { execFileSync } from 'node:child_process'; +import { existsSync, promises } from 'node:fs'; +import * as path from 'node:path'; const noopBuilderWrapper: BuilderWrapper = (fn) => fn; @@ -8,10 +13,63 @@ 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(await promises.readFile(packageJsonPath, {encoding: 'utf8'})) + : {}; let wrapper: BuilderWrapper = noopBuilderWrapper; try { const { createBuilderWithMetrics } = await import('@o3r/telemetry'); - wrapper = createBuilderWithMetrics; - } catch {} + if (packageJson.config?.o3rMetrics) { + wrapper = createBuilderWithMetrics; + } + } catch (e: any) { + // Do not throw if `@o3r/telemetry is not installed + if (packageJson.config?.o3rMetrics === true) { + ctx.logger.info('`config.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 ((!process.env.CI || process.env.CI === 'false') && typeof packageJson.config?.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(await promises.readFile(path.join(__dirname, '..', '..', 'package.json'), 'utf-8')).version; + execFileSync(`${pmr} ng add @o3r/telemetry@${version}`); + } catch { + ctx.logger.warn('Failed to install `@o3r/telemetry`.'); + } + + try { + const { createBuilderWithMetrics } = await import('@o3r/telemetry'); + wrapper = createBuilderWithMetrics; + } catch { + // If pnp context package installed in the same process will not be available + } + } else { + ctx.logger.info('You can activate it at any time by running `ng add @o3r/telemetry`.'); + + packageJson.config ||= {}; + packageJson.config.o3rMetrics = false; + + if (existsSync(packageJsonPath)) { + await promises.writeFile(packageJsonPath, JSON.stringify(packageJson, null, 2)); + } else { + ctx.logger.warn(`No package.json found in ${ctx.workspaceRoot}.`); + } + } + } + } 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..f10346548a 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,62 @@ 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 = 'package.json'; + const packageJson = tree.exists(packageJsonPath) ? tree.readJson(packageJsonPath) as JsonObject : {}; try { const { createSchematicWithMetrics } = await import('@o3r/telemetry'); - wrapper = createSchematicWithMetrics; + if ((packageJson.config 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.config as JsonObject)?.o3rMetrics) { + context.logger.warn('`config.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.config 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')).version; + context.addTask( + new NodePackageNgAddTask( + '@o3r/telemetry', + { + dependencyType: NodeDependencyType.Dev, + version + } + ) + ); + await lastValueFrom(context.engine.executePostTasks()); + + try { + const { createSchematicWithMetrics } = await import('@o3r/telemetry'); + wrapper = createSchematicWithMetrics; + } catch { + // If pnp context package installed in the same process will not be available + } + } else { + context.logger.info('You can activate it at any time by running `ng add @o3r/telemetry`.'); + + packageJson.config ||= {}; + (packageJson.config 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..2536694991 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.config ||= {}; + (packageJson.config 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 'config.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 6818ae2e8e..09958de481 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 = @@ -58,11 +60,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.config?.o3rMetrics + ); + if (shouldSendData) { + if (typeof ((options as any).o3rMetrics ?? process.env.O3R_METRICS) === 'undefined') { + context.logger.info( + 'Telemetry is globally activated for the project (`config.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 244d856ec0..56ca1b96e3 100644 --- a/packages/@o3r/telemetry/src/environment/index.ts +++ b/packages/@o3r/telemetry/src/environment/index.ts @@ -153,7 +153,8 @@ export const getEnvironmentInfo = async (): Promise => { 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.spec.ts b/packages/@o3r/telemetry/src/schematics/index.spec.ts index 100d2658ee..b7f29fb5ea 100644 --- a/packages/@o3r/telemetry/src/schematics/index.spec.ts +++ b/packages/@o3r/telemetry/src/schematics/index.spec.ts @@ -18,15 +18,17 @@ jest.mock('node:perf_hooks', () => { }); import { callRule, Rule, SchematicContext, Tree } from '@angular-devkit/schematics'; -import { lastValueFrom } from 'rxjs'; +import { lastValueFrom, of } from 'rxjs'; import { createSchematicWithMetrics, SchematicWrapper } from './index'; let context: SchematicContext; let debug: jest.Mock; +let executePostTasks: jest.Mock; describe('createSchematicWithMetricsIfInstalled', () => { beforeEach(() => { debug = jest.fn(); + executePostTasks = jest.fn().mockReturnValue(of('')); context = { schematic: { description: { @@ -36,6 +38,9 @@ describe('createSchematicWithMetricsIfInstalled', () => { name: 'MySchematic' } }, + engine: { + executePostTasks + }, interactive: false, logger: { debug @@ -55,6 +60,7 @@ describe('createSchematicWithMetricsIfInstalled', () => { expect(originalSchematic).toHaveBeenCalled(); expect(originalSchematic).toHaveBeenCalledWith(options); expect(rule).toHaveBeenCalled(); + expect(executePostTasks).toHaveBeenCalled(); expect(debug).toHaveBeenCalled(); expect(debug).toHaveBeenCalledWith(JSON.stringify({ environment: { env: 'env' }, @@ -76,6 +82,7 @@ describe('createSchematicWithMetricsIfInstalled', () => { expect(originalSchematic).toHaveBeenCalled(); expect(originalSchematic).toHaveBeenCalledWith(options); expect(rule).toHaveBeenCalled(); + expect(executePostTasks).toHaveBeenCalled(); expect(debug).toHaveBeenCalled(); expect(debug).toHaveBeenCalledWith(JSON.stringify({ environment: { env: 'env' }, @@ -97,6 +104,7 @@ describe('createSchematicWithMetricsIfInstalled', () => { expect(originalSchematic).toHaveBeenCalled(); expect(originalSchematic).toHaveBeenCalledWith(options); expect(rule).toHaveBeenCalled(); + expect(executePostTasks).not.toHaveBeenCalled(); expect(debug).toHaveBeenCalled(); expect(debug).toHaveBeenCalledWith(expect.stringContaining('error example')); }); diff --git a/packages/@o3r/telemetry/src/schematics/index.ts b/packages/@o3r/telemetry/src/schematics/index.ts index a7a9a5b7ee..0e6bb95be0 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'; @@ -26,6 +27,7 @@ export const createSchematicWithMetrics: SchematicWrapper = try { const rule = schematicFn(options); await lastValueFrom(callRule(rule, tree, context)); + await lastValueFrom(context.engine.executePostTasks()); } catch (e: any) { const err = e instanceof Error ? e : new Error(error); @@ -48,11 +50,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.config 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 (`config.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/yarn.lock b/yarn.lock index 4c56dfd540..c305f2d321 100644 --- a/yarn.lock +++ b/yarn.lock @@ -7653,6 +7653,7 @@ __metadata: "@o3r/telemetry": "workspace:^" "@o3r/test-helpers": "workspace:^" "@stylistic/eslint-plugin-ts": "npm:^1.5.4" + "@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" @@ -7664,6 +7665,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" @@ -7691,8 +7693,6 @@ __metadata: "@o3r/telemetry": "workspace:^" typescript: ~5.2.2 peerDependenciesMeta: - "@o3r/schematics": - optional: true "@o3r/telemetry": optional: true languageName: unknown @@ -11770,6 +11770,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: 10/d7c0c5ec95af583191942ac33f8af2eb1fe839da6b4560277a8c251fa289f2dd3a5d14850baf910343700646200258ecff89dc9e1d57df29c16a1082d91a5ae3 + languageName: node + linkType: hard + "@types/istanbul-lib-coverage@npm:*, @types/istanbul-lib-coverage@npm:^2.0.0, @types/istanbul-lib-coverage@npm:^2.0.1": version: 2.0.6 resolution: "@types/istanbul-lib-coverage@npm:2.0.6" @@ -12195,6 +12205,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: 10/fd0b73f873a64ed5366d1d757c42e5dbbb2201002667c8958eda7ca02fff09d73de91360572db465ee00240c32d50c6039ea736d8eca374300f9664f93e8da39 + languageName: node + linkType: hard + "@types/tough-cookie@npm:*": version: 4.0.5 resolution: "@types/tough-cookie@npm:4.0.5" @@ -20792,7 +20811,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: @@ -27859,7 +27878,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: