diff --git a/packages/@aws-cdk-testing/cli-integ/tests/cli-integ-tests/cli-telemetry/cdk-cli-telemetry-adds-context-value.integtest.ts b/packages/@aws-cdk-testing/cli-integ/tests/cli-integ-tests/cli-telemetry/cdk-cli-telemetry-adds-context-value.integtest.ts index 74e039bc2..deed4212e 100644 --- a/packages/@aws-cdk-testing/cli-integ/tests/cli-integ-tests/cli-telemetry/cdk-cli-telemetry-adds-context-value.integtest.ts +++ b/packages/@aws-cdk-testing/cli-integ/tests/cli-integ-tests/cli-telemetry/cdk-cli-telemetry-adds-context-value.integtest.ts @@ -45,7 +45,7 @@ integTest( ['cli-telemetry']: false, }); } finally { - await fs.unlink(path.join(fixture.integTestDir, 'cdk.context.json')); + await fs.unlink(contextFile); } }), ); diff --git a/packages/@aws-cdk-testing/cli-integ/tests/cli-integ-tests/cli-telemetry/cdk-cli-telemetry-reports-status.integtest.ts b/packages/@aws-cdk-testing/cli-integ/tests/cli-integ-tests/cli-telemetry/cdk-cli-telemetry-reports-status.integtest.ts new file mode 100644 index 000000000..f22b6b8a9 --- /dev/null +++ b/packages/@aws-cdk-testing/cli-integ/tests/cli-integ-tests/cli-telemetry/cdk-cli-telemetry-reports-status.integtest.ts @@ -0,0 +1,24 @@ +import { promises as fs } from 'fs'; +import * as path from 'path'; +import { integTest, withDefaultFixture } from '../../../lib'; + +jest.setTimeout(2 * 60 * 60_000); // Includes the time to acquire locks, worst-case single-threaded runtime + +integTest( + 'CLI Telemetry reports status', + withDefaultFixture(async (fixture) => { + const userContextFile = path.join(fixture.integTestDir, 'cdk.json'); + try { + // default status is enabled + const output1 = await fixture.cdk(['cli-telemetry', '--status']); + expect(output1).toContain('CLI Telemetry is enabled. See https://github.com/aws/aws-cdk-cli/tree/main/packages/aws-cdk#cdk-cli-telemetry for ways to disable.'); + + // disable status + await fs.writeFile(userContextFile, JSON.stringify({ context: { 'cli-telemetry': false } })); + const output2 = await fixture.cdk(['cli-telemetry', '--status']); + expect(output2).toContain('CLI Telemetry is disabled. See https://github.com/aws/aws-cdk-cli/tree/main/packages/aws-cdk#cdk-cli-telemetry for ways to enable.'); + } finally { + await fs.unlink(userContextFile); + } + }), +); diff --git a/packages/aws-cdk/README.md b/packages/aws-cdk/README.md index ba02d61ef..daca89cb6 100644 --- a/packages/aws-cdk/README.md +++ b/packages/aws-cdk/README.md @@ -11,26 +11,27 @@ The AWS CDK Toolkit provides the `cdk` command-line interface that can be used to work with AWS CDK applications. This module is part of the [AWS Cloud Development Kit](https://github.com/aws/aws-cdk) project. -| Command | Description | -| ------------------------------------- | --------------------------------------------------------------------------------- | -| [`cdk docs`](#cdk-docs) | Access the online documentation | -| [`cdk init`](#cdk-init) | Start a new CDK project (app or library) | -| [`cdk list`](#cdk-list) | List stacks and their dependencies in an application | -| [`cdk synth`](#cdk-synth) | Synthesize a CDK app to CloudFormation template(s) | -| [`cdk diff`](#cdk-diff) | Diff stacks against current state | -| [`cdk deploy`](#cdk-deploy) | Deploy a stack into an AWS account | -| [`cdk rollback`](#cdk-rollback) | Roll back a failed deployment | -| [`cdk import`](#cdk-import) | Import existing AWS resources into a CDK stack | -| [`cdk migrate`](#cdk-migrate) | Migrate AWS resources, CloudFormation stacks, and CloudFormation templates to CDK | -| [`cdk watch`](#cdk-watch) | Watches a CDK app for deployable and hotswappable changes | -| [`cdk destroy`](#cdk-destroy) | Deletes a stack from an AWS account | -| [`cdk bootstrap`](#cdk-bootstrap) | Deploy a toolkit stack to support deploying large stacks & artifacts | -| [`cdk gc`](#cdk-gc) | Garbage collect assets associated with the bootstrapped stack | -| [`cdk doctor`](#cdk-doctor) | Inspect the environment and produce information useful for troubleshooting | -| [`cdk acknowledge`](#cdk-acknowledge) | Acknowledge (and hide) a notice by issue number | -| [`cdk notices`](#cdk-notices) | List all relevant notices for the application | -| [`cdk refactor`](#cdk-refactor) | Moves resources between stacks or within the same stack | -| [`cdk drift`](#cdk-drift) | Detect drifts in the given CloudFormation stack(s) | +| Command | Description | +| ---------------------------------------- | --------------------------------------------------------------------------------- | +| [`cdk docs`](#cdk-docs) | Access the online documentation | +| [`cdk init`](#cdk-init) | Start a new CDK project (app or library) | +| [`cdk list`](#cdk-list) | List stacks and their dependencies in an application | +| [`cdk synth`](#cdk-synth) | Synthesize a CDK app to CloudFormation template(s) | +| [`cdk diff`](#cdk-diff) | Diff stacks against current state | +| [`cdk deploy`](#cdk-deploy) | Deploy a stack into an AWS account | +| [`cdk rollback`](#cdk-rollback) | Roll back a failed deployment | +| [`cdk import`](#cdk-import) | Import existing AWS resources into a CDK stack | +| [`cdk migrate`](#cdk-migrate) | Migrate AWS resources, CloudFormation stacks, and CloudFormation templates to CDK | +| [`cdk watch`](#cdk-watch) | Watches a CDK app for deployable and hotswappable changes | +| [`cdk destroy`](#cdk-destroy) | Deletes a stack from an AWS account | +| [`cdk bootstrap`](#cdk-bootstrap) | Deploy a toolkit stack to support deploying large stacks & artifacts | +| [`cdk gc`](#cdk-gc) | Garbage collect assets associated with the bootstrapped stack | +| [`cdk doctor`](#cdk-doctor) | Inspect the environment and produce information useful for troubleshooting | +| [`cdk acknowledge`](#cdk-acknowledge) | Acknowledge (and hide) a notice by issue number | +| [`cdk notices`](#cdk-notices) | List all relevant notices for the application | +| [`cdk refactor`](#cdk-refactor) | Moves resources between stacks or within the same stack | +| [`cdk drift`](#cdk-drift) | Detect drifts in the given CloudFormation stack(s) | +| [`cdk cli-telemetry](#cdk-cli-telemetry) | Enable or disable cli telemetry collection | ## Common topics @@ -1213,6 +1214,31 @@ $ # Detect drift against the currently-deployed stack with the verbose flag enab $ cdk drift --verbose ``` +### `cdk cli-telemetry` + +Enables or disables cli telemetry collection for your local CDK App. Records your +choice in `cdk.context.json`. You can also set your preference manually under the `context` key in your +`~/.cdk.json` file or `/cdk.json` file. + +```bash +$ # Disable telemetry +$ cdk cli-telemetry --disable + +$ # Enable telemetry +$ cdk cli-telemetry --enable +``` + +You can also check the current status on whether your CDK App is opted in or out of +cli telemetry collection. Note that this takes into account all methods of disabling +cli telemetry, including environment variables and +[context values](https://docs.aws.amazon.com/cdk/v2/guide/context.html) +that can be set in many different ways (such as `~/.cdk.json`). + +```bash +$ # Check the current status of telemetry +$ cdk cli-telemetry --status +``` + ## Global Options ### `unstable` diff --git a/packages/aws-cdk/lib/cli/cdk-toolkit.ts b/packages/aws-cdk/lib/cli/cdk-toolkit.ts index 4e023d7b7..68897cba8 100644 --- a/packages/aws-cdk/lib/cli/cdk-toolkit.ts +++ b/packages/aws-cdk/lib/cli/cdk-toolkit.ts @@ -64,6 +64,7 @@ import { serializeStructure, validateSnsTopicArn, } from '../util'; +import { canCollectTelemetry } from './telemetry/collect-telemetry'; // Must use a require() otherwise esbuild complains about calling a namespace // eslint-disable-next-line @typescript-eslint/no-require-imports,@typescript-eslint/consistent-type-imports @@ -207,6 +208,15 @@ export class CdkToolkit { await this.props.configuration.saveContext(); } + public async cliTelemetryStatus() { + const canCollect = canCollectTelemetry(this.props.configuration.context); + if (canCollect) { + await this.ioHost.asIoHelper().defaults.info('CLI Telemetry is enabled. See https://github.com/aws/aws-cdk-cli/tree/main/packages/aws-cdk#cdk-cli-telemetry for ways to disable.'); + } else { + await this.ioHost.asIoHelper().defaults.info('CLI Telemetry is disabled. See https://github.com/aws/aws-cdk-cli/tree/main/packages/aws-cdk#cdk-cli-telemetry for ways to enable.'); + } + } + public async cliTelemetry(enable: boolean) { this.props.configuration.context.set('cli-telemetry', enable); await this.props.configuration.saveContext(); diff --git a/packages/aws-cdk/lib/cli/cli-config.ts b/packages/aws-cdk/lib/cli/cli-config.ts index ef49b1588..8c5942889 100644 --- a/packages/aws-cdk/lib/cli/cli-config.ts +++ b/packages/aws-cdk/lib/cli/cli-config.ts @@ -476,6 +476,11 @@ export async function makeConfig(): Promise { desc: 'Disable anonymous telemetry', conflicts: 'enable', }, + status: { + type: 'boolean', + desc: 'Report telemetry opt-in/out status', + conflicts: ['enable', 'disable'], + }, }, }, }, diff --git a/packages/aws-cdk/lib/cli/cli-type-registry.json b/packages/aws-cdk/lib/cli/cli-type-registry.json index 0065a9f41..78e2476c7 100644 --- a/packages/aws-cdk/lib/cli/cli-type-registry.json +++ b/packages/aws-cdk/lib/cli/cli-type-registry.json @@ -939,6 +939,14 @@ "type": "boolean", "desc": "Disable anonymous telemetry", "conflicts": "enable" + }, + "status": { + "type": "boolean", + "desc": "Report telemetry opt-in/out status", + "conflicts": [ + "enable", + "disable" + ] } } } diff --git a/packages/aws-cdk/lib/cli/cli.ts b/packages/aws-cdk/lib/cli/cli.ts index 9612fc59b..c8121b460 100644 --- a/packages/aws-cdk/lib/cli/cli.ts +++ b/packages/aws-cdk/lib/cli/cli.ts @@ -478,13 +478,16 @@ export async function exec(args: string[], synthesizer?: Synthesizer): Promise): any { type: 'boolean', desc: 'Disable anonymous telemetry', conflicts: 'enable', + }) + .option('status', { + default: undefined, + type: 'boolean', + desc: 'Report telemetry opt-in/out status', + conflicts: ['enable', 'disable'], }), ) .version(helpers.cliVersion()) diff --git a/packages/aws-cdk/lib/cli/telemetry/collect-telemetry.ts b/packages/aws-cdk/lib/cli/telemetry/collect-telemetry.ts new file mode 100644 index 000000000..82461c2e8 --- /dev/null +++ b/packages/aws-cdk/lib/cli/telemetry/collect-telemetry.ts @@ -0,0 +1,12 @@ +import type { Context } from '../../api/context'; + +/** + * Whether or not we collect telemetry + */ +export function canCollectTelemetry(context: Context): boolean { + if ((['true', '1'].includes(process.env.CDK_CLI_DISABLE_TELEMETRY ?? '')) || ['false', false].includes(context.get('cli-telemetry'))) { + return false; + } + + return true; +} diff --git a/packages/aws-cdk/lib/cli/user-input.ts b/packages/aws-cdk/lib/cli/user-input.ts index 646f7082f..128cf9d63 100644 --- a/packages/aws-cdk/lib/cli/user-input.ts +++ b/packages/aws-cdk/lib/cli/user-input.ts @@ -1484,4 +1484,11 @@ export interface CliTelemetryOptions { * @default - undefined */ readonly disable?: boolean; + + /** + * Report telemetry opt-in/out status + * + * @default - undefined + */ + readonly status?: boolean; } diff --git a/packages/aws-cdk/test/_helpers/with-env.ts b/packages/aws-cdk/test/_helpers/with-env.ts new file mode 100644 index 000000000..bd552a93f --- /dev/null +++ b/packages/aws-cdk/test/_helpers/with-env.ts @@ -0,0 +1,16 @@ +/** + * Run code with additional environment variables + */ +export async function withEnv(block: () => Promise, env: Record = {}) { + const originalEnv = process.env; + try { + process.env = { + ...originalEnv, + ...env, + }; + + return await block(); + } finally { + process.env = originalEnv; + } +} diff --git a/packages/aws-cdk/test/cli/cli-commands.test.ts b/packages/aws-cdk/test/cli/cli-commands.test.ts index 3545f3a2f..0ce32c618 100644 --- a/packages/aws-cdk/test/cli/cli-commands.test.ts +++ b/packages/aws-cdk/test/cli/cli-commands.test.ts @@ -39,9 +39,9 @@ describe('context', () => { }); describe('cli-telemetry', () => { - test('requires either --enable or --disable flag', async () => { + test('requires a flag to be set', async () => { await expect(exec(['cli-telemetry'])) .rejects - .toThrow("Must specify either '--enable' or '--disable'"); + .toThrow("Must specify '--enable', '--disable', or '--status'"); }); }); diff --git a/packages/aws-cdk/test/cli/telemetry/collect-telemetry.test.ts b/packages/aws-cdk/test/cli/telemetry/collect-telemetry.test.ts new file mode 100644 index 000000000..1dd2666f3 --- /dev/null +++ b/packages/aws-cdk/test/cli/telemetry/collect-telemetry.test.ts @@ -0,0 +1,47 @@ +import { Context } from '../../../lib/api/context'; +import { canCollectTelemetry } from '../../../lib/cli/telemetry/collect-telemetry'; +import { withEnv } from '../../_helpers/with-env'; + +describe(canCollectTelemetry, () => { + let context: Context; + + beforeEach(() => { + context = new Context(); + }); + + test('returns true by default', async () => { + expect(canCollectTelemetry(context)).toBeTruthy(); + }); + + test('returns false if env variable is set to true', async () => { + await withEnv(async () => { + expect(canCollectTelemetry(context)).toBeFalsy(); + }, { + CDK_CLI_DISABLE_TELEMETRY: 'true', + }); + }); + + test('returns false if env variable is set to 1', async () => { + await withEnv(async () => { + expect(canCollectTelemetry(context)).toBeFalsy(); + }, { + CDK_CLI_DISABLE_TELEMETRY: '1', + }); + }); + + test('returns false if context is set to false', async () => { + context.set('cli-telemetry', false); + expect(canCollectTelemetry(context)).toBeFalsy(); + + context.set('cli-telemetry', 'false'); + expect(canCollectTelemetry(context)).toBeFalsy(); + }); + + test('returns true if context is set to true', async () => { + context.set('cli-telemetry', true); + expect(canCollectTelemetry(context)).toBeTruthy(); + + context.set('cli-telemetry', 'true'); + expect(canCollectTelemetry(context)).toBeTruthy(); + }); +}); diff --git a/packages/aws-cdk/test/commands/telemetry.test.ts b/packages/aws-cdk/test/commands/telemetry.test.ts index 6281c830d..f032f36a5 100644 --- a/packages/aws-cdk/test/commands/telemetry.test.ts +++ b/packages/aws-cdk/test/commands/telemetry.test.ts @@ -1,6 +1,7 @@ import { CdkToolkit } from '../../lib/cli/cdk-toolkit'; import { CliIoHost } from '../../lib/cli/io-host'; import { Configuration } from '../../lib/cli/user-configuration'; +import { withEnv } from '../_helpers/with-env'; const ioHost = CliIoHost.instance({}, true); const ioHelper = ioHost.asIoHelper(); @@ -39,4 +40,43 @@ describe('telemetry command', () => { expect(configuration.context.get('cli-telemetry')).toBe(false); expect(notifySpy).toHaveBeenCalledWith(expect.objectContaining({ level: 'info', message: 'Telemetry disabled' })); }); + + test('status reports current telemetry status -- enabled by default', async () => { + // WHEN + await toolkit.cliTelemetryStatus(); + + // THEN + expect(notifySpy).toHaveBeenCalledWith(expect.objectContaining({ level: 'info', message: 'CLI Telemetry is enabled. See https://github.com/aws/aws-cdk-cli/tree/main/packages/aws-cdk#cdk-cli-telemetry for ways to disable.' })); + }); + + test('status reports current telemetry status -- enabled intentionally', async () => { + // WHEN + configuration.context.set('cli-telemetry', true); + await toolkit.cliTelemetryStatus(); + + // THEN + expect(notifySpy).toHaveBeenCalledWith(expect.objectContaining({ level: 'info', message: 'CLI Telemetry is enabled. See https://github.com/aws/aws-cdk-cli/tree/main/packages/aws-cdk#cdk-cli-telemetry for ways to disable.' })); + }); + + test('status reports current telemetry status -- disabled via context', async () => { + // WHEN + configuration.context.set('cli-telemetry', false); + await toolkit.cliTelemetryStatus(); + + // THEN + expect(notifySpy).toHaveBeenCalledWith(expect.objectContaining({ level: 'info', message: 'CLI Telemetry is disabled. See https://github.com/aws/aws-cdk-cli/tree/main/packages/aws-cdk#cdk-cli-telemetry for ways to enable.' })); + }); + + test('status reports current telemetry status -- disabled via env var', async () => { + await withEnv(async () => { + // WHEN + process.env.CDK_CLI_DISABLE_TELEMETRY = 'true'; + await toolkit.cliTelemetryStatus(); + + // THEN + expect(notifySpy).toHaveBeenCalledWith(expect.objectContaining({ level: 'info', message: 'CLI Telemetry is disabled. See https://github.com/aws/aws-cdk-cli/tree/main/packages/aws-cdk#cdk-cli-telemetry for ways to enable.' })); + }, { + CDK_CLI_DISABLE_TELEMETRY: 'true', + }); + }); });