Skip to content

Commit

Permalink
feat: usage data consent (#685)
Browse files Browse the repository at this point in the history
* feat: user consent for telemetry

* fix: removing unused vars

* fix: lint errors

* feat: integrate with usage_data_emitter

* fix: update API.md

* update comment

* fix: refactor and move LocalConfigController to platform-core

* fix: add encoding when reading config file

* fix: adding additional file verification check

* fix: update unit test

* fix: export all types to update API.md

* fix: update API.md

* fix: open file using file descriptor

* fix: update fs mocks in unit test

* fix: update fs.open mock

* fix: mock fs open for unit test
  • Loading branch information
bombguy authored Nov 20, 2023
1 parent d010539 commit 85e6191
Show file tree
Hide file tree
Showing 22 changed files with 594 additions and 17 deletions.
5 changes: 5 additions & 0 deletions .changeset/forty-geese-decide.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'create-amplify': patch
---

Adding message about usage data tracking when creating new project
5 changes: 5 additions & 0 deletions .changeset/nine-socks-attack.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@aws-amplify/backend-cli': patch
---

Added subcommands under configure data tracking preferences
6 changes: 6 additions & 0 deletions .changeset/slimy-cups-draw.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
'@aws-amplify/platform-core': patch
'@aws-amplify/backend-cli': patch
---

integrate usage data tracking consent with usage-data-emitter
1 change: 1 addition & 0 deletions .eslint_dictionary.json
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,7 @@
"localhost",
"lsof",
"lstat",
"macos",
"matchers",
"mfas",
"mkdtemp",
Expand Down
10 changes: 5 additions & 5 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
import { configControllerFactory } from '@aws-amplify/platform-core';
import { CommandModule } from 'yargs';
import { ConfigureProfileCommand } from './configure_profile_command.js';
import { ProfileController } from './profile_controller.js';
import { ConfigureCommand } from './configure_command.js';
import { ConfigureTelemetryCommand } from './telemetry/configure_telemetry_command.js';
/**
* Creates a configure command.
*/
Expand All @@ -10,8 +12,12 @@ export const createConfigureCommand = (): CommandModule<object> => {
const configureProfileCommand = new ConfigureProfileCommand(
profileController
);
const configureTelemetryCommand = new ConfigureTelemetryCommand(
configControllerFactory.getInstance('usage_data_preferences.json')
);

return new ConfigureCommand([
configureProfileCommand as unknown as CommandModule,
configureTelemetryCommand as unknown as CommandModule,
]);
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
import { Printer } from '@aws-amplify/cli-core';
import { beforeEach, describe, it, mock } from 'node:test';
import yargs, { CommandModule } from 'yargs';
import assert from 'node:assert';
import { TestCommandRunner } from '../../../test-utils/command_runner.js';
import { ConfigureTelemetryCommand } from './configure_telemetry_command.js';
import {
USAGE_DATA_TRACKING_ENABLED,
configControllerFactory,
} from '@aws-amplify/platform-core';

void describe('configure command', () => {
const mockedConfigControllerSet = mock.fn();
mock.method(configControllerFactory, 'getInstance', () => ({
set: mockedConfigControllerSet,
}));
const telemetryCommand = new ConfigureTelemetryCommand(
configControllerFactory.getInstance('usage_data_preferences.json')
);
const parser = yargs().command(telemetryCommand as unknown as CommandModule);
const commandRunner = new TestCommandRunner(parser);

const mockedPrint = mock.method(Printer, 'print');

beforeEach(() => {
mockedPrint.mock.resetCalls();
mockedConfigControllerSet.mock.resetCalls();
});

void it('enable telemetry & updates local config', async () => {
await commandRunner.runCommand(`telemetry enable`);
assert.match(
mockedPrint.mock.calls[0].arguments[0],
/You have enabled telemetry data collection/
);
assert.strictEqual(
mockedConfigControllerSet.mock.calls[0].arguments[0],
USAGE_DATA_TRACKING_ENABLED
);
assert.strictEqual(
mockedConfigControllerSet.mock.calls[0].arguments[1],
true
);
});

void it('disables telemetry & updates local config', async () => {
await commandRunner.runCommand(`telemetry disable`);
assert.match(
mockedPrint.mock.calls[0].arguments[0],
/You have disabled telemetry data collection/
);
assert.strictEqual(
mockedConfigControllerSet.mock.calls[0].arguments[0],
USAGE_DATA_TRACKING_ENABLED
);
assert.strictEqual(
mockedConfigControllerSet.mock.calls[0].arguments[1],
false
);
});

void it('if subcommand is not defined, it should list of subcommands and demandCommand', async () => {
await commandRunner.runCommand(`telemetry`);
assert.match(
mockedPrint.mock.calls[0].arguments[0],
/Not enough non-option arguments: got 0, need at least 1/
);
assert.strictEqual(mockedConfigControllerSet.mock.callCount(), 0);
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
import { Printer } from '@aws-amplify/cli-core';
import {
ConfigurationController,
USAGE_DATA_TRACKING_ENABLED,
} from '@aws-amplify/platform-core';
import { Argv, CommandModule } from 'yargs';
import { handleCommandFailure } from '../../../command_failure_handler.js';
/**
* Command to configure AWS Amplify profile.
*/
export class ConfigureTelemetryCommand implements CommandModule<object> {
/**
* @inheritDoc
*/
readonly command: string;

/**
* @inheritDoc
*/
readonly describe: string;

/**
* Configure profile command.
*/
constructor(private readonly configController: ConfigurationController) {
this.command = 'telemetry';
this.describe = 'Configure anonymous usage data collection';
}

/**
* @inheritDoc
*/
handler = () => {
// CommandModule requires handler implementation. But this is never called if top level command
// is configured to require subcommand.
// Help is printed by default in that case before ever attempting to call handler.
throw new Error('Top level generate handler should never be called');
};

/**
* @inheritDoc
*/
builder = (yargs: Argv) => {
return yargs
.command('enable', 'Enable anonymous data collection', {}, async () => {
await this.configController.set(USAGE_DATA_TRACKING_ENABLED, true);

Printer.print('You have enabled telemetry data collection');
})
.command('disable', 'Disable anonymous data collection', {}, async () => {
await this.configController.set(USAGE_DATA_TRACKING_ENABLED, false);

Printer.print('You have disabled telemetry data collection');
})
.demandCommand()
.strictCommands()
.recommendCommands()
.fail((msg, err) => {
handleCommandFailure(msg, err, yargs);
yargs.exit(1, err);
});
};
}
11 changes: 6 additions & 5 deletions packages/cli/src/commands/sandbox/sandbox_command_factory.ts
Original file line number Diff line number Diff line change
Expand Up @@ -50,13 +50,14 @@ export const createSandboxCommand = (): CommandModule<
credentialProvider
);

const libraryVersion =
new PackageJsonReader().read(
fileURLToPath(new URL('../../../package.json', import.meta.url))
).version ?? '';

const eventHandlerFactory = new SandboxEventHandlerFactory(
sandboxBackendIdentifierResolver,
new UsageDataEmitterFactory().getInstance(
new PackageJsonReader().read(
fileURLToPath(new URL('../../../package.json', import.meta.url))
).version ?? ''
)
async () => await new UsageDataEmitterFactory().getInstance(libraryVersion)
);

const commandMiddleWare = new CommandMiddleware();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ void describe('sandbox_event_handler_factory', () => {
name: 'name',
type: 'sandbox',
}),
usageDataEmitterMock
async () => usageDataEmitterMock
);

afterEach(() => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ export class SandboxEventHandlerFactory {
private readonly getBackendIdentifier: (
sandboxName?: string
) => Promise<BackendIdentifier>,
private readonly usageDataEmitter: UsageDataEmitter
private readonly getUsageDataEmitter: () => Promise<UsageDataEmitter>
) {}

getSandboxEventHandlers: SandboxEventHandlerCreator = ({
Expand All @@ -28,14 +28,15 @@ export class SandboxEventHandlerFactory {
const backendIdentifier = await this.getBackendIdentifier(
sandboxName
);
const usageDataEmitter = await this.getUsageDataEmitter();
try {
await clientConfigLifecycleHandler.generateClientConfigFile(
backendIdentifier
);
if (args && args[0]) {
const deployResult = args[0] as DeployResult;
if (deployResult && deployResult.deploymentTimes) {
await this.usageDataEmitter.emitSuccess(
await usageDataEmitter.emitSuccess(
deployResult.deploymentTimes,
{ command: 'Sandbox' }
);
Expand Down Expand Up @@ -68,12 +69,13 @@ export class SandboxEventHandlerFactory {
],
failedDeployment: [
async (...args: unknown[]) => {
const usageDataEmitter = await this.getUsageDataEmitter();
if (args.length == 0 || !args[0]) {
return;
}
const deployError = args[0] as Error;
if (deployError && deployError.message) {
await this.usageDataEmitter.emitFailure(deployError, {
await usageDataEmitter.emitFailure(deployError, {
command: 'Sandbox',
});
}
Expand Down
8 changes: 8 additions & 0 deletions packages/create-amplify/src/amplify_project_creator.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,10 @@ void describe('AmplifyProjectCreator', () => {
logMock.log.mock.calls[4].arguments[0],
'Welcome to AWS Amplify! \nRun `npx amplify help` for a list of available commands. \nGet started by running `npx amplify sandbox`.'
);
assert.equal(
logMock.log.mock.calls[5].arguments[0],
`Amplify (Gen 2) collects anonymous telemetry data about general usage of the CLI.\n\nParticipation is optional, and you may opt-out by using \`amplify configure telemetry disable\`.\n\nTo learn more about telemetry, visit https://docs.amplify.aws/gen2/reference/telemetry`
);
});

void it('should instruct users to use the custom project root', async () => {
Expand Down Expand Up @@ -76,5 +80,9 @@ void describe('AmplifyProjectCreator', () => {
logMock.log.mock.calls[4].arguments[0],
'Welcome to AWS Amplify! \nRun `npx amplify help` for a list of available commands. \nGet started by running `cd ./project/root; npx amplify sandbox`.'
);
assert.equal(
logMock.log.mock.calls[5].arguments[0],
`Amplify (Gen 2) collects anonymous telemetry data about general usage of the CLI.\n\nParticipation is optional, and you may opt-out by using \`amplify configure telemetry disable\`.\n\nTo learn more about telemetry, visit https://docs.amplify.aws/gen2/reference/telemetry`
);
});
});
10 changes: 10 additions & 0 deletions packages/create-amplify/src/amplify_project_creator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@ import { NpmProjectInitializer } from './npm_project_initializer.js';
import { GitIgnoreInitializer } from './gitignore_initializer.js';
import { logger } from './logger.js';

const LEARN_MORE_USAGE_DATA_TRACKING_LINK = `https://docs.amplify.aws/gen2/reference/telemetry`;

/**
*
*/
Expand Down Expand Up @@ -72,5 +74,13 @@ export class AmplifyProjectCreator {
Run \`npx amplify help\` for a list of available commands.
Get started by running \`${cdCommand}npx amplify sandbox\`.`
);

logger.log(
`Amplify (Gen 2) collects anonymous telemetry data about general usage of the CLI.
Participation is optional, and you may opt-out by using \`amplify configure telemetry disable\`.
To learn more about telemetry, visit ${LEARN_MORE_USAGE_DATA_TRACKING_LINK}`
);
};
}
25 changes: 24 additions & 1 deletion packages/platform-core/API.md
Original file line number Diff line number Diff line change
Expand Up @@ -30,13 +30,33 @@ export enum CDKContextKey {
DEPLOYMENT_TYPE = "amplify-backend-type"
}

// @public (undocumented)
export const configControllerFactory: ConfigurationControllerFactory;

// @public (undocumented)
export type ConfigurationController = {
get: <T>(path: string) => Promise<T>;
set: (path: string, value: string | boolean | number) => Promise<void>;
clear: () => Promise<void>;
write: () => Promise<void>;
};

// @public
export class ConfigurationControllerFactory {
constructor();
getInstance: (configFileName: LocalConfigurationFileName) => ConfigurationController;
}

// @public
export class FilePathExtractor {
constructor(stackTraceLine: string);
// (undocumented)
extract: () => string | undefined;
}

// @public (undocumented)
export type LocalConfigurationFileName = 'usage_data_preferences.json';

// @public (undocumented)
export type PackageJson = z.infer<typeof packageJsonSchema>;

Expand All @@ -62,6 +82,9 @@ export const packageJsonSchema: z.ZodObject<{
type?: "module" | "commonjs" | undefined;
}>;

// @public
export const USAGE_DATA_TRACKING_ENABLED = "telemetry.enabled";

// @public (undocumented)
export type UsageDataEmitter = {
emitSuccess: (metrics?: Record<string, number>, dimensions?: Record<string, string>) => Promise<void>;
Expand All @@ -70,7 +93,7 @@ export type UsageDataEmitter = {

// @public
export class UsageDataEmitterFactory {
getInstance: (libraryVersion: string) => UsageDataEmitter;
getInstance: (libraryVersion: string) => Promise<UsageDataEmitter>;
}

// (No @packageDocumentation comment for this package)
Expand Down
Loading

0 comments on commit 85e6191

Please sign in to comment.