Skip to content

Commit

Permalink
feat(metrics): question to activate telemetry + documentation
Browse files Browse the repository at this point in the history
  • Loading branch information
matthieu-crouzet committed Feb 20, 2024
1 parent da3eaf7 commit 369d250
Show file tree
Hide file tree
Showing 14 changed files with 249 additions and 31 deletions.
25 changes: 25 additions & 0 deletions docs/telemetry/README.md
Original file line number Diff line number Diff line change
@@ -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 for one month for security reasons.
## 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`
3 changes: 3 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -271,5 +271,8 @@
"tools/@*/*",
"apps/*"
],
"config": {
"o3rMetrics": true
},
"packageManager": "[email protected]"
}
4 changes: 2 additions & 2 deletions packages/@ama-sdk/create/src/index.it.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -62,8 +62,8 @@ describe('Create new sdk command', () => {
packageManagerExec({
script: 'schematics',
args: ['@ama-sdk/schematics:typescript-core', '--spec-path', path.join(path.relative(sdkPackagePath, sdkFolderPath), 'swagger-spec.yml')]
}, { ...execAppOptions, cwd: sdkPackagePath }
)).not.toThrow();
}, { ...execAppOptions, cwd: sdkPackagePath })
).not.toThrow();
expect(() => packageManagerRun({script: 'build'}, { ...execAppOptions, cwd: sdkPackagePath })).not.toThrow();
expect(() => packageManagerRun({ script: 'doc:generate'}, { ...execAppOptions, cwd: sdkPackagePath })).not.toThrow();
});
Expand Down
3 changes: 2 additions & 1 deletion packages/@ama-sdk/create/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -70,7 +70,8 @@ const schematicArgs = [
'--package', pck,
'--package-manager', packageManager,
'--directory', targetDirectory,
...(argv['spec-path'] ? ['--spec-path', argv['spec-path']] : [])
...(argv['spec-path'] ? ['--spec-path', argv['spec-path']] : []),
...(typeof argv['o3r-metrics'] !== 'undefined' ? [`--${!argv['o3r-metrics'] ? 'no-' : ''}o3r-metrics`] : [])
];

const getSchematicStepInfo = (schematic: string) => ({
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -71,7 +71,6 @@ function ngGenerateTypescriptSDKFn(options: NgGenerateTypescriptSDKCoreSchematic
* @param _context
*/
const clearGeneratedCode = (tree: Tree, _context: SchematicContext) => {
treeGlob(tree, path.posix.join(targetPath, 'src', 'api', '**', '*.ts')).forEach((file) => tree.delete(file));
treeGlob(tree, path.posix.join(targetPath, 'src', 'api', '**', '*.ts')).forEach((file) => tree.delete(file));
treeGlob(tree, path.posix.join(targetPath, 'src', 'models', 'base', '**', '!(index).ts')).forEach((file) => tree.delete(file));
treeGlob(tree, path.posix.join(targetPath, 'src', 'spec', '!(operation-adapter|index).ts')).forEach((file) => tree.delete(file));
Expand Down
5 changes: 2 additions & 3 deletions packages/@o3r/extractors/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -28,15 +28,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"
Expand Down Expand Up @@ -69,6 +67,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": "^7.0.1",
Expand Down
62 changes: 60 additions & 2 deletions packages/@o3r/extractors/src/core/wrapper.ts
Original file line number Diff line number Diff line change
@@ -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;

Expand All @@ -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 collection 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 data about the usage of Otter builders and schematics 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);
};
63 changes: 60 additions & 3 deletions packages/@o3r/schematics/src/utility/wrapper.ts
Original file line number Diff line number Diff line change
@@ -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;

Expand All @@ -7,14 +14,64 @@ 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 collection 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 data about the usage of Otter builders and schematics 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;

if (tree.exists(packageJsonPath)) {
tree.overwrite(
packageJsonPath,
JSON.stringify(packageJson, null, 2)
);
}
}
}
}
return wrapper(schematicFn)(opts);
};
17 changes: 15 additions & 2 deletions packages/@o3r/telemetry/schematics/ng-add/index.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,23 @@
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';

/**
* Add Otter telemetry to an Otter Project
* @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 collection of anonymous data for Otter builders and schematics usage.
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'.
`);
};
}
30 changes: 24 additions & 6 deletions packages/@o3r/telemetry/src/builders/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<S, O extends BuilderOutput = BuilderOutput> =
Expand Down Expand Up @@ -58,11 +60,27 @@ 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 personally don\'t want to send telemetry, you can deactivate it by setting `O3R_METRICS` to false in your environment variables, '
+ 'or by calling the 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());
});
}
}
};

3 changes: 2 additions & 1 deletion packages/@o3r/telemetry/src/environment/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -153,7 +153,8 @@ export const getEnvironmentInfo = async (): Promise<EnvironmentMetricData> => {
os: osInfo,
node: nodeInfo,
packageManager: packageManagerInfo,
otter: otterInfo, ci,
otter: otterInfo,
ci,
...(projectName ? { project: { name: projectName } } : {})
};
};
Expand Down
10 changes: 9 additions & 1 deletion packages/@o3r/telemetry/src/schematics/index.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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: {
Expand All @@ -36,6 +38,9 @@ describe('createSchematicWithMetricsIfInstalled', () => {
name: 'MySchematic'
}
},
engine: {
executePostTasks
},
interactive: false,
logger: {
debug
Expand All @@ -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' },
Expand All @@ -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' },
Expand All @@ -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'));
});
Expand Down
Loading

0 comments on commit 369d250

Please sign in to comment.