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 Jan 30, 2024
1 parent 7dad11e commit e2c839e
Show file tree
Hide file tree
Showing 13 changed files with 227 additions and 36 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 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`
3 changes: 3 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -280,5 +280,8 @@
"tools/@*/*",
"apps/*"
],
"cli": {
"o3rMetrics": true
},
"packageManager": "[email protected]"
}
Original file line number Diff line number Diff line change
Expand Up @@ -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');
Expand Down
18 changes: 6 additions & 12 deletions packages/@o3r/components/builders/component-extractor/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -104,21 +104,15 @@ export default createBuilder(createBuilderWithMetricsIfInstalled<ComponentExtrac
try {
await fs.promises.mkdir(path.dirname(path.resolve(context.workspaceRoot, options.componentOutputFile)), {recursive: true});
} catch {}
await new Promise<void>((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<void>((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 {
Expand Down
2 changes: 2 additions & 0 deletions packages/@o3r/extractors/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -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",
Expand Down
54 changes: 52 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 { execSync } from 'node:child_process';
import { existsSync, readFileSync, writeFileSync } from 'node:fs';
import * as path from 'node:path';

const noopBuilderWrapper: BuilderWrapper = (fn) => fn;

Expand All @@ -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);
};
57 changes: 54 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,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);
};
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.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'.
`);
};
}
29 changes: 23 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 @@ -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());
});
}
}
};

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 = (): EnvironmentMetricData => {
os: osInfo,
node: nodeInfo,
packageManager: packageManagerInfo,
otter: otterInfo, ci,
otter: otterInfo,
ci,
...(projectName ? { project: { name: projectName } } : {})
};
};
Expand Down
25 changes: 20 additions & 5 deletions packages/@o3r/telemetry/src/schematics/index.ts
Original file line number Diff line number Diff line change
@@ -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';
Expand Down Expand Up @@ -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());
});
}
}
};

Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down
Loading

0 comments on commit e2c839e

Please sign in to comment.