Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(metrics): question to activate telemetry + documentation #1257

Merged
merged 1 commit into from
Feb 20, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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`
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]);
fpaul-1A marked this conversation as resolved.
Show resolved Hide resolved

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)) {
matthieu-crouzet marked this conversation as resolved.
Show resolved Hide resolved
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
matthieu-crouzet marked this conversation as resolved.
Show resolved Hide resolved
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(
fpaul-1A marked this conversation as resolved.
Show resolved Hide resolved
'@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
Loading