Skip to content
7 changes: 6 additions & 1 deletion packages/@aws-cdk-testing/cli-integ/lib/with-cdk-app.ts
Original file line number Diff line number Diff line change
Expand Up @@ -581,7 +581,12 @@ export class TestFixture extends ShellHelper {

await this.cli.makeCliAvailable();

return this.shell(['cdk', ...(verbose ? ['-v'] : []), ...args], {
return this.shell([
'cdk',
...(verbose ? ['-v'] : []),
...args,
...(options?.options ?? []),
], {
...options,
modEnv: {
...this.cdkShellEnv(),
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
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 --disable does not send to endpoint',
withDefaultFixture(async (fixture) => {
const output = await fixture.cdk(['cli-telemetry', '--disable'], { options: ['-vvv'] });

// Check the trace that telemetry was not executed successfully
expect(output).not.toContain('Telemetry Sent Successfully');

// Check the trace that endpoint telemetry was never connected
expect(output).toContain('Endpoint Telemetry NOT connected');
}),
);
Original file line number Diff line number Diff line change
Expand Up @@ -10,9 +10,14 @@ integTest(
const telemetryFile = path.join(fixture.integTestDir, 'telemetry.json');

// Deploy stack while collecting telemetry
await fixture.cdkDeploy('test-1', {
const deployOutput = await fixture.cdkDeploy('test-1', {
telemetryFile,
options: ['-vvv'], // force trace mode
});

// Check the trace that telemetry was executed successfully
expect(deployOutput).toContain('Telemetry Sent Successfully');

const json = fs.readJSONSync(telemetryFile);
expect(json).toEqual([
expect.objectContaining({
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,18 +13,21 @@ integTest(
modEnv: {
INTEG_STACK_SET: 'stage-with-errors',
},
options: ['-vvv'], // force trace mode
});

expect(output).toContain('This is an error');

// Check the trace that telemetry was executed successfully despite error in synth
expect(output).toContain('Telemetry Sent Successfully');

const json = fs.readJSONSync(telemetryFile);
expect(json).toEqual([
expect.objectContaining({
event: expect.objectContaining({
command: expect.objectContaining({
path: ['synth'],
parameters: {
verbose: 1,
parameters: expect.objectContaining({
unstable: '<redacted>',
['telemetry-file']: '<redacted>',
lookups: true,
Expand All @@ -36,7 +39,7 @@ integTest(
ci: expect.anything(), // changes based on where this is called
validation: true,
quiet: false,
},
}),
config: {
context: {},
},
Expand Down Expand Up @@ -71,8 +74,7 @@ integTest(
event: expect.objectContaining({
command: expect.objectContaining({
path: ['synth'],
parameters: {
verbose: 1,
parameters: expect.objectContaining({
unstable: '<redacted>',
['telemetry-file']: '<redacted>',
lookups: true,
Expand All @@ -84,7 +86,7 @@ integTest(
ci: expect.anything(), // changes based on where this is called
validation: true,
quiet: false,
},
}),
config: {
context: {},
},
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,15 +8,22 @@ integTest(
'cdk synth with telemetry data',
withDefaultFixture(async (fixture) => {
const telemetryFile = path.join(fixture.integTestDir, `telemetry-${Date.now()}.json`);
await fixture.cdk(['synth', fixture.fullStackName('test-1'), '--unstable=telemetry', `--telemetry-file=${telemetryFile}`]);

const synthOutput = await fixture.cdk(
['synth', fixture.fullStackName('test-1'), '--unstable=telemetry', `--telemetry-file=${telemetryFile}`],
{ options: ['-vvv'] }, // force trace mode
);

// Check the trace that telemetry was executed successfully
expect(synthOutput).toContain('Telemetry Sent Successfully');

const json = fs.readJSONSync(telemetryFile);
expect(json).toEqual([
expect.objectContaining({
event: expect.objectContaining({
command: expect.objectContaining({
path: ['synth', '$STACKS_1'],
parameters: {
verbose: 1,
parameters: expect.objectContaining({
unstable: '<redacted>',
['telemetry-file']: '<redacted>',
lookups: true,
Expand All @@ -28,7 +35,7 @@ integTest(
ci: expect.anything(), // changes based on where this is called
validation: true,
quiet: false,
},
}),
config: {
context: {},
},
Expand Down Expand Up @@ -64,8 +71,7 @@ integTest(
event: expect.objectContaining({
command: expect.objectContaining({
path: ['synth', '$STACKS_1'],
parameters: {
verbose: 1,
parameters: expect.objectContaining({
unstable: '<redacted>',
['telemetry-file']: '<redacted>',
lookups: true,
Expand All @@ -77,7 +83,7 @@ integTest(
ci: expect.anything(), // changes based on where this is called
validation: true,
quiet: false,
},
}),
config: {
context: {},
},
Expand Down
52 changes: 34 additions & 18 deletions packages/aws-cdk/lib/cli/io-host/cli-io-host.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,11 +9,15 @@ import * as promptly from 'promptly';
import type { IoHelper, ActivityPrinterProps, IActivityPrinter } from '../../../lib/api-private';
import { asIoHelper, IO, isMessageRelevantForLevel, CurrentActivityPrinter, HistoryActivityPrinter } from '../../../lib/api-private';
import { StackActivityProgress } from '../../commands/deploy';
import { canCollectTelemetry } from '../telemetry/collect-telemetry';
import type { EventResult } from '../telemetry/messages';
import { CLI_PRIVATE_IO, CLI_TELEMETRY_CODES } from '../telemetry/messages';
import type { EventType } from '../telemetry/schema';
import { TelemetrySession } from '../telemetry/session';
import { EndpointTelemetrySink } from '../telemetry/sink/endpoint-sink';
import { FileTelemetrySink } from '../telemetry/sink/file-sink';
import { Funnel } from '../telemetry/sink/funnel';
import type { ITelemetrySink } from '../telemetry/sink/sink-interface';
import { isCI } from '../util/ci';

export type { IIoHost, IoMessage, IoMessageCode, IoMessageLevel, IoRequest };
Expand Down Expand Up @@ -168,32 +172,44 @@ export class CliIoHost implements IIoHost {
this.logLevel = props.logLevel ?? 'info';
this.isCI = props.isCI ?? isCI();
this.requireDeployApproval = props.requireDeployApproval ?? RequireApproval.BROADENING;

this.stackProgress = props.stackProgress ?? StackActivityProgress.BAR;
}

public async startTelemetry(args: any, context: Context, _proxyAgent?: Agent) {
let sink;
public async startTelemetry(args: any, context: Context, proxyAgent?: Agent) {
let sinks: ITelemetrySink[] = [];
const telemetryFilePath = args['telemetry-file'];
if (telemetryFilePath) {
sink = new FileTelemetrySink({
ioHost: this,
logFilePath: telemetryFilePath,
});
try {
sinks.push(new FileTelemetrySink({
ioHost: this,
logFilePath: telemetryFilePath,
}));
await this.asIoHelper().defaults.trace('File Telemetry connected');
} catch (e: any) {
await this.asIoHelper().defaults.trace(`File Telemetry instantiation failed: ${e.message}`);
}
}
// TODO: uncomment this at launch
// if (canCollectTelemetry(args, context)) {
// sink = new EndpointTelemetrySink({
// ioHost: this,
// agent: proxyAgent,
// endpoint: '', // TODO: add endpoint
// });
// }

if (sink) {

const telemetryEndpoint = process.env.TELEMETRY_ENDPOINT; // TODO: replace with endpoint at launch
if (canCollectTelemetry(args, context) && telemetryEndpoint) {
try {
sinks.push(new EndpointTelemetrySink({
ioHost: this,
agent: proxyAgent,
endpoint: telemetryEndpoint,
}));
await this.asIoHelper().defaults.trace('Endpoint Telemetry connected');
} catch (e: any) {
await this.asIoHelper().defaults.trace(`Endpoint Telemetry instantiation failed: ${e.message}`);
}
} else {
await this.asIoHelper().defaults.trace('Endpoint Telemetry NOT connected');
}

if (sinks.length > 0) {
this.telemetry = new TelemetrySession({
ioHost: this,
client: sink,
client: new Funnel({ sinks }),
arguments: args,
context: context,
});
Expand Down
3 changes: 2 additions & 1 deletion packages/aws-cdk/lib/cli/telemetry/collect-telemetry.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,8 @@ import type { Context } from '../../api/context';
export function canCollectTelemetry(args: any, context: Context): boolean {
if ((['true', '1'].includes(process.env.CDK_DISABLE_CLI_TELEMETRY ?? '')) ||
['false', false].includes(context.get('cli-telemetry')) ||
(args['version-reporting'] !== undefined && !args['version-reporting'])) /* aliased with telemetry option */ {
(args['version-reporting'] !== undefined && !args['version-reporting']) || /* aliased with telemetry option */
(Array.isArray(args._) && args._.includes('cli-telemetry') && args.disable)) /* special case for `cdk cli-telemetry --disable` */ {
return false;
}

Expand Down
8 changes: 7 additions & 1 deletion packages/aws-cdk/lib/cli/telemetry/sink/endpoint-sink.ts
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,11 @@ export class EndpointTelemetrySink implements ITelemetrySink {

public constructor(props: EndpointTelemetrySinkProps) {
this.endpoint = parse(props.endpoint);

if (!this.endpoint.hostname || !this.endpoint.pathname) {
throw new ToolkitError(`Telemetry Endpoint malformed. Received hostname: ${this.endpoint.hostname}, pathname: ${this.endpoint.pathname}`);
}

this.ioHelper = IoHelper.fromActionAwareIoHost(props.ioHost);
this.agent = props.agent;

Expand Down Expand Up @@ -78,7 +83,7 @@ export class EndpointTelemetrySink implements ITelemetrySink {
}
} catch (e: any) {
// Never throw errors, just log them via ioHost
await this.ioHelper.defaults.trace(`Failed to add telemetry event: ${e.message}`);
await this.ioHelper.defaults.trace(`Failed to send telemetry event: ${e.message}`);
}
}

Expand All @@ -94,6 +99,7 @@ export class EndpointTelemetrySink implements ITelemetrySink {

// Successfully posted
if (res.statusCode && res.statusCode >= 200 && res.statusCode < 300) {
await this.ioHelper.defaults.trace('Telemetry Sent Successfully');
return true;
}

Expand Down
4 changes: 4 additions & 0 deletions packages/aws-cdk/test/cli/telemetry/collect-telemetry.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -48,4 +48,8 @@ describe(canCollectTelemetry, () => {
test('returns false if no-version-reporting is set', async () => {
expect(canCollectTelemetry({ 'version-reporting': false }, context)).toBeFalsy();
});

test('special case for cli-telemetry --disable', async () => {
expect(canCollectTelemetry({ _: ['cli-telemetry'], disable: true }, context)).toBeFalsy();
});
});