Skip to content
This repository has been archived by the owner on Jan 5, 2022. It is now read-only.

Enable analytics #23

Merged
merged 6 commits into from
Dec 6, 2019
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
2 changes: 1 addition & 1 deletion src/commands/add-approuter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@ export default class AddApprouter extends Command {
},
{
title: 'Creating files',
task: ctx => copyFiles(ctx.files, options).catch(e => this.error(e, { exit: 2 }))
task: ctx => copyFiles(ctx.files, options)
}
]);

Expand Down
5 changes: 2 additions & 3 deletions src/commands/add-cx-server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,7 @@ import { Command, flags } from '@oclif/command';
import { OutputFlags } from '@oclif/parser';
import * as Listr from 'listr';
import * as path from 'path';
import { CopyDescriptor } from '../utils/copy-list';
import { copyFiles, findConflicts } from '../utils/templates';
import { CopyDescriptor, copyFiles, findConflicts } from '../utils/';

type Flags = OutputFlags<typeof AddCxServer.flags>;

Expand Down Expand Up @@ -49,7 +48,7 @@ export default class AddCxServer extends Command {
},
{
title: 'Creating files',
task: () => copyFiles(files, options).catch(e => this.error(e, { exit: 2 }))
task: () => copyFiles(files, options)
}
]);

Expand Down
29 changes: 23 additions & 6 deletions src/commands/init.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,11 +6,21 @@ import { Command, flags } from '@oclif/command';
import cli from 'cli-ux';
import * as Listr from 'listr';
import * as path from 'path';
import { modifyGitIgnore } from '../utils/git-ignore';
import { getJestConfig, modifyJestConfig } from '../utils/jest-config';
import { installDependencies, modifyPackageJson, parsePackageJson } from '../utils/package-json';
import { buildScaffold, shouldBuildScaffold } from '../utils/scaffold';
import { copyFiles, ensureDirectoryExistence, findConflicts, readTemplates } from '../utils/templates';
import {
buildScaffold,
copyFiles,
ensureDirectoryExistence,
findConflicts,
getJestConfig,
installDependencies,
modifyGitIgnore,
modifyJestConfig,
modifyPackageJson,
parsePackageJson,
readTemplates,
shouldBuildScaffold,
usageAnalytics
} from '../utils/';

export default class Init extends Command {
static description = 'Initializes your project for the SAP Cloud SDK, SAP Cloud Platform Cloud Foundry and CI/CD using the SAP Cloud SDK toolkit';
Expand All @@ -30,6 +40,11 @@ export default class Init extends Command {
hidden: true,
description: 'If the folder is empty, use nest-cli to create a project scaffold.'
}),
analytics: flags.boolean({
hidden: true,
allowNo: true,
description: 'Enable or disable collection of anonymous usage data.'
}),
force: flags.boolean({
description: 'Do not fail if a file or npm script already exist and overwrite it.'
}),
Expand Down Expand Up @@ -106,7 +121,7 @@ export default class Init extends Command {
},
{
title: 'Installing dependencies',
task: () => installDependencies(projectDir, verbose).catch(e => this.error(`Error during npm install: ${e.message}`, { exit: 2 }))
task: () => installDependencies(projectDir, verbose).catch(e => this.error(`Error during npm install: ${e.message}`, { exit: 13 }))
florian-richter marked this conversation as resolved.
Show resolved Hide resolved
},
{
title: 'Modifying `.gitignore`',
Expand All @@ -115,6 +130,8 @@ export default class Init extends Command {
]);

await tasks.run();

await usageAnalytics(projectDir, flags.analytics);
this.printSuccessMessage(isScaffold);
} catch (error) {
this.error(error, { exit: 1 });
Expand Down
11 changes: 11 additions & 0 deletions src/utils/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
/*!
* Copyright (c) 2019 SAP SE or an SAP affiliate company. All rights reserved.
*/

export * from './copy-list';
export * from './git-ignore';
export * from './jest-config';
export * from './package-json';
export * from './scaffold';
export * from './templates';
export * from './usage-analytics';
16 changes: 16 additions & 0 deletions src/utils/usage-analytics.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
/*!
* Copyright (c) 2019 SAP SE or an SAP affiliate company. All rights reserved.
*/

import cli from 'cli-ux';
import * as fs from 'fs';
import * as path from 'path';

export async function usageAnalytics(projectDir: string, agreeToAnalytics: boolean) {
if (agreeToAnalytics === false) {
return;
}
if (agreeToAnalytics || (await cli.confirm('Do you want to provide anonymous usage analytics to help us improve the SDK? (y|n)'))) {
florian-richter marked this conversation as resolved.
Show resolved Hide resolved
fs.writeFileSync(path.resolve(projectDir, 'sap-cloud-sdk-analytics.json'), JSON.stringify({ enabled: true }));
}
}
117 changes: 42 additions & 75 deletions test/init.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,8 +27,19 @@ import * as fs from 'fs-extra';
import * as path from 'path';
import Init from '../src/commands/init';

const pathPrefix = path.resolve(__dirname, __filename.replace(/\./g, '-')).replace('-ts', '');

function getCleanProjectDir(name: string) {
const projectDir = path.resolve(pathPrefix, name);
if (fs.existsSync(projectDir)) {
fs.removeSync(projectDir);
}
return projectDir;
}

describe('Init', () => {
const pathPrefix = path.resolve(__dirname, __filename.replace(/\./g, '-')).replace('-ts', '');
const expressAppDir = 'test/express/';
const nestAppDir = 'test/nest/';

beforeAll(() => {
if (!fs.existsSync(pathPrefix)) {
Expand All @@ -41,104 +52,60 @@ describe('Init', () => {
});

it('should create a new project with the necessary files', async () => {
const projectDir = path.resolve(pathPrefix, 'full-init');
if (fs.existsSync(projectDir)) {
fs.removeSync(projectDir);
}
const projectDir = getCleanProjectDir('full-init');

const argv = ['--projectName=testingApp', '--buildScaffold', `--projectDir=${projectDir}`];
await Init.run(argv);
await Init.run(['--projectName=testingApp', '--buildScaffold', '--no-analytics', `--projectDir=${projectDir}`]);

['.npmrc', 'credentials.json', 'systems.json', 'manifest.yml']
.map(file => path.resolve(projectDir, file))
.forEach(path => {
expect(fs.existsSync(path)).toBe(true);
});
}, 60000);

it('should create test cases when building a project with scaffold', async () => {
const projectDir = path.resolve(pathPrefix, 'full-init-with-test');
if (fs.existsSync(projectDir)) {
fs.removeSync(projectDir);
}

const argv = ['--projectName=testingApp', '--buildScaffold', `--projectDir=${projectDir}`];
await Init.run(argv);

const reportsPath = path.resolve(projectDir, 's4hana_pipeline', 'reports');

// execute the ci scripts and check if the reports are written
await execa('npm', ['run', 'ci-backend-unit-test'], { cwd: projectDir, stdio: 'inherit' });

const pathBackendUnit = path.resolve(reportsPath, 'backend-unit');
const pathCoverageUnit = path.resolve(reportsPath, 'coverage-reports', 'backend-unit');
expect(fs.readdirSync(pathBackendUnit).length).toBeGreaterThan(1);
expect(fs.readdirSync(pathCoverageUnit).length).toBeGreaterThan(1);
expect(fs.readdirSync(path.resolve(reportsPath, 'backend-unit')).length).toBeGreaterThan(1);
expect(fs.readdirSync(path.resolve(reportsPath, 'coverage-reports', 'backend-unit')).length).toBeGreaterThan(1);

await execa('npm', ['run', 'ci-integration-test'], { cwd: projectDir, stdio: 'inherit' });

const pathBackendIntegration = path.resolve(reportsPath, 'backend-integration');
const pathCoverageIntegration = path.resolve(reportsPath, 'coverage-reports', 'backend-integration');
console.log('Backend Integration', fs.readdirSync(pathBackendIntegration));
console.log('Coverage Integration', fs.readdirSync(pathCoverageIntegration));
expect(fs.readdirSync(pathBackendIntegration).length).toBeGreaterThan(1);
expect(fs.readdirSync(pathCoverageIntegration).length).toBeGreaterThan(1);
}, 60000);
expect(fs.readdirSync(path.resolve(reportsPath, 'backend-integration')).length).toBeGreaterThan(1);
expect(fs.readdirSync(path.resolve(reportsPath, 'coverage-reports', 'backend-integration')).length).toBeGreaterThan(1);
}, 120000);

it('should add necessary files to an existing project', async () => {
const expressAppDir = 'test/express/';
const projectDir = path.resolve(pathPrefix, 'add-to-existing');

if (fs.existsSync(projectDir)) {
fs.removeSync(projectDir);
}
const projectDir = getCleanProjectDir('add-to-existing');
fs.copySync(expressAppDir, projectDir, { recursive: true });

const argv = ['--projectName=testingApp', '--startCommand="npm start"', `--projectDir=${projectDir}`, '--force'];
await Init.run(argv);
await Init.run(['--projectName=testingApp', '--startCommand="npm start"', `--projectDir=${projectDir}`, '--no-analytics', '--force']);

['.npmrc', 'credentials.json', 'systems.json', 'manifest.yml']
.map(file => path.resolve(projectDir, file))
.forEach(path => {
expect(fs.existsSync(path)).toBe(true);
});

['jest.config.js', 'jest.integration-test.config.js', 'jets.unit-test.config.js']
.map(file => path.resolve(projectDir, file))
.forEach(path => {
expect(fs.existsSync(path)).toBe(false);
});

expect(fs.existsSync(path.resolve(projectDir, 'test'))).toBe(false);
}, 20000);

it('init should detect and fail if there are conflicts', async () => {
const appDir = 'test/nest/';
const projectDir = path.resolve(pathPrefix, 'detect-conflicts');
if (fs.existsSync(projectDir)) {
fs.removeSync(projectDir);
}
fs.copySync(appDir, projectDir, { recursive: true });
const projectDir = getCleanProjectDir('detect-conflicts');
fs.copySync(nestAppDir, projectDir, { recursive: true });
fs.createFileSync(`${projectDir}/.npmrc`);

const argv = ['--projectName=testingApp', '--startCommand="npm start"', `--projectDir=${projectDir}`];
await Init.run(argv);
await Init.run(['--projectName=testingApp', '--startCommand="npm start"', `--projectDir=${projectDir}`, '--no-analytics']);

expect(error).toHaveBeenCalledWith(
'A file with the name ".npmrc" already exists. If you want to overwrite it, rerun the command with `--force`.',
{ exit: 1 }
);
}, 60000);

it('should add to gitignore if there is one', async () => {
const exampleAppDir = 'test/nest/';
const projectDir = path.resolve(pathPrefix, 'add-to-gitignore');
if (fs.existsSync(projectDir)) {
fs.removeSync(projectDir);
}
fs.copySync(exampleAppDir, projectDir, { recursive: true });
it('should add to .gitignore if there is one', async () => {
const projectDir = getCleanProjectDir('add-to-gitignore');
fs.copySync(nestAppDir, projectDir, { recursive: true });

const argv = ['--projectName=testingApp', '--startCommand="npm start"', `--projectDir=${projectDir}`];
await Init.run(argv);
await Init.run(['--projectName=testingApp', '--startCommand="npm start"', `--projectDir=${projectDir}`, '--no-analytics']);

const gitignoreEntries = fs
.readFileSync(`${projectDir}/.gitignore`, 'utf8')
Expand All @@ -152,31 +119,22 @@ describe('Init', () => {
}, 50000);

it('should show a warning if the project is not using git', async () => {
const projectDir = path.resolve(pathPrefix, 'warn-on-no-git');
if (fs.existsSync(projectDir)) {
fs.removeSync(projectDir);
}
const projectDir = getCleanProjectDir('warn-on-no-git');

fs.createFileSync(path.resolve(projectDir, 'package.json'));
fs.writeFileSync(path.resolve(projectDir, 'package.json'), JSON.stringify({ name: 'project' }), 'utf8');

const argv = ['--projectName=testingApp', '--startCommand="npm start"', `--projectDir=${projectDir}`];
await Init.run(argv);
await Init.run(['--projectName=testingApp', '--startCommand="npm start"', `--projectDir=${projectDir}`, '--no-analytics']);

expect(warn).toHaveBeenCalledWith('No .gitignore file found!');
}, 30000);

it('should add our scripts and dependencies to the package.json', async () => {
const projectDir = path.resolve(pathPrefix, 'add-scripts-and-dependencies');
if (fs.existsSync(projectDir)) {
fs.removeSync(projectDir);
}

const projectDir = getCleanProjectDir('add-scripts-and-dependencies');
fs.createFileSync(path.resolve(projectDir, 'package.json'));
fs.writeFileSync(path.resolve(projectDir, 'package.json'), JSON.stringify({ name: 'project' }), 'utf8');

const argv = ['--projectName=testingApp', '--startCommand="npm start"', '--frontendScripts', `--projectDir=${projectDir}`];
await Init.run(argv);
await Init.run(['--projectName=testingApp', '--startCommand="npm start"', '--frontendScripts', `--projectDir=${projectDir}`, '--no-analytics']);

const packageJson = JSON.parse(fs.readFileSync(path.resolve(projectDir, 'package.json'), 'utf8'));

Expand All @@ -190,4 +148,13 @@ describe('Init', () => {
expect(scripts).toContain(script)
);
}, 20000);

it('should add the analytics file', async () => {
const projectDir = getCleanProjectDir('add-to-gitignore');
fs.copySync(nestAppDir, projectDir, { recursive: true });

await Init.run(['--projectName=testingApp', '--startCommand="npm start"', `--projectDir=${projectDir}`, '--analytics']);

expect(JSON.parse(fs.readFileSync(`${projectDir}/sap-cloud-sdk-analytics.json`, 'utf8'))).toEqual({ enabled: true });
}, 50000);
});
8 changes: 4 additions & 4 deletions usage-analytics.md
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ Any data that is sent during the analytics process is anonymized.
This means that neither your project's name nor other personally identifiable information is collected.
Instead, we create a pseudonym _p_ for every project, by concatenating the project's name and a randomly generated salt value, and hashing this value using a cryptographic hash function (currently SAH-256).
Neither the project name nor the salt itself will be transmitted, only the resulting pseudonym.
However, should you host your project publicly, e.g. on GitHub, and make both your project's name and your salt value publicly available, it is technically possible for us to recreate the corresponding pseudonym.
However, should you host your project publicly, e.g. on GitHub, and make both your project's name and your salt value publicly available, it would technically possible for us to recreate the corresponding pseudonym.

## What data is collected?

Expand All @@ -44,8 +44,8 @@ We're collecting the following data:

## Opt-in

Currently, you need to manually create a file called `sap-cloud-sdk-analytics.json` in the root directory of your project.
Copy the following code snippet and save it as `sap-cloud-sdk-analytics.json`:
When you initialize your project with the CLI, you will be asked if you want to provide analytics.
This will create `sap-cloud-sdk-analytics.json`:

```json
{
Expand All @@ -68,5 +68,5 @@ Please make sure not make up your own value, as this may lessen the guarantees a
## Opt-out

Should you ever decide to opt-out again, you can either set the value for `"enabled"` to `false`, or delete the configuration file altogether.
Usage data will be sent if and only if there's a file with the name `sap-cloud-sdk-analytics.json` in the root of the given project and `enabled` is so to true.
Usage data will be sent if and only if there's a file with the name `sap-cloud-sdk-analytics.json` in the root of the given project and `enabled` is set to true.
If any of these conditions are not met, no data will be sent.