Skip to content

feat(cli): send telemetry events to local file #631

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

Merged
merged 129 commits into from
Jul 22, 2025
Merged
Show file tree
Hide file tree
Changes from 20 commits
Commits
Show all changes
129 commits
Select commit Hold shift + click to select a range
0c1c0b3
chore(cli): telemetry client
kaizencc Jun 9, 2025
2fe74df
docs
kaizencc Jun 9, 2025
c6bf5f4
wip
kaizencc Jun 13, 2025
e91e7e3
use interfaces and fix tests
kaizencc Jun 13, 2025
0d33ac5
add schema and pr feedback
kaizencc Jun 16, 2025
7d5a19a
feat(cli): telemetry for list
kaizencc Jun 18, 2025
41a461b
Merge branch 'main' into conroy/list
kaizencc Jun 18, 2025
c380240
Merge branch 'main' into conroy/basic-telemetry-client
kaizencc Jun 18, 2025
e51d30d
change to parsed url
kaizencc Jun 18, 2025
9d980d5
readonly
kaizencc Jun 18, 2025
d8dd12f
Merge branch 'conroy/basic-telemetry-client' into conroy/list
kaizencc Jun 18, 2025
9126147
Merge branch 'main' into conroy/basic-telemetry-client
kaizencc Jun 19, 2025
ab560fd
small change
kaizencc Jun 19, 2025
4e361ae
retries implemented, not yet tested
kaizencc Jun 19, 2025
69e63cd
add commented out test
kaizencc Jun 19, 2025
da20533
Merge branch 'conroy/basic-telemetry-client' into conroy/list
kaizencc Jun 19, 2025
ca3f172
refactor list telemetry, still wip
kaizencc Jun 20, 2025
3b0082e
1 line change
kaizencc Jun 20, 2025
8824e28
add message code
kaizencc Jun 20, 2025
f7cb38a
add failed synth
kaizencc Jun 20, 2025
8760b44
Merge branch 'main' into conroy/basic-telemetry-client
kaizencc Jun 21, 2025
ece2874
fixup sanitation"
kaizencc Jun 23, 2025
4dea47d
pr feedback
kaizencc Jun 23, 2025
3dbbe62
add proxy support, better retries, test succeeds
kaizencc Jun 25, 2025
feaf376
lint
kaizencc Jun 25, 2025
83ebd08
lints
kaizencc Jun 25, 2025
4b8397b
Merge branch 'conroy/basic-telemetry-client' into conroy/list
kaizencc Jun 25, 2025
cf285bb
wip
kaizencc Jun 25, 2025
2c8191b
reworked with iohost
kaizencc Jun 26, 2025
6c977b6
include spans
kaizencc Jun 26, 2025
56f23e5
force not ci in tests
kaizencc Jun 26, 2025
079bbc5
Merge branch 'main' into conroy/basic-telemetry-client
kaizencc Jun 26, 2025
8e43ba7
Merge branch 'conroy/basic-telemetry-client' into conroy/list
kaizencc Jun 26, 2025
9dd1580
refactors from friday are in a working state
kaizencc Jun 30, 2025
55a4a73
add cdk library version
kaizencc Jun 30, 2025
1041bf8
collect telemetry
kaizencc Jun 30, 2025
635d6b7
Merge branch 'main' into conroy/basic-telemetry-client
kaizencc Jun 30, 2025
18d9349
Merge branch 'main' into conroy/basic-telemetry-client
kaizencc Jul 1, 2025
1811c9a
Merge branch 'conroy/basic-telemetry-client' into conroy/list
kaizencc Jul 1, 2025
ea4720c
Merge branch 'main' into conroy/basic-telemetry-client
kaizencc Jul 2, 2025
b9c0d6d
Merge branch 'conroy/basic-telemetry-client' into conroy/list
kaizencc Jul 2, 2025
46cc2c8
remove circular dependencies
kaizencc Jul 2, 2025
76a19ff
diff tests need to not depend on message order
kaizencc Jul 2, 2025
2433e3e
update type registry name
kaizencc Jul 2, 2025
b30d188
can collect telemetry
kaizencc Jul 2, 2025
fe47576
callback function on crtl-c
kaizencc Jul 2, 2025
4bf3be9
Merge branch 'main' into conroy/basic-telemetry-client
kaizencc Jul 2, 2025
359a442
telemetry interface includes flush
kaizencc Jul 2, 2025
c20ad8b
Merge branch 'conroy/basic-telemetry-client' into conroy/list
kaizencc Jul 2, 2025
54109d6
rename
kaizencc Jul 2, 2025
334b9dd
lint
kaizencc Jul 2, 2025
39152d3
Merge branch 'conroy/basic-telemetry-client' into conroy/list
kaizencc Jul 2, 2025
fbd6dd6
small fixes
kaizencc Jul 2, 2025
70fa3e3
minor enhancements
kaizencc Jul 2, 2025
e02a283
wip
kaizencc Jul 2, 2025
25dec4a
pr feedback
kaizencc Jul 2, 2025
5628ed9
trace
kaizencc Jul 2, 2025
cc8c6dd
pr feedback
kaizencc Jul 2, 2025
9433b36
tests for collect-telemetry
kaizencc Jul 2, 2025
72c862e
lint
kaizencc Jul 2, 2025
d3c745c
test for installation ids
kaizencc Jul 2, 2025
da64d2b
library-version tests
kaizencc Jul 2, 2025
b007f0e
library version tests
kaizencc Jul 2, 2025
9caf42e
refactor telemetry session
kaizencc Jul 3, 2025
ad0e6cd
minors
kaizencc Jul 3, 2025
c7d365a
renames
kaizencc Jul 3, 2025
fb85dfa
Merge branch 'conroy/basic-telemetry-client' into conroy/list
kaizencc Jul 3, 2025
e139111
Merge branch 'main' into conroy/list
kaizencc Jul 3, 2025
27ea7e6
fix merge
kaizencc Jul 3, 2025
f74bb90
small updates
kaizencc Jul 3, 2025
d79756f
fix stupid await
kaizencc Jul 3, 2025
80fad80
sanitize tests
kaizencc Jul 3, 2025
e087b50
accountid test
kaizencc Jul 3, 2025
66bb8c1
region fetcher tests
kaizencc Jul 3, 2025
64b5fa2
fix tests from merge
kaizencc Jul 7, 2025
b5d0f32
fix build
kaizencc Jul 7, 2025
b2b56d3
Merge branch 'main' into conroy/list
kaizencc Jul 7, 2025
1adc88e
session unit tests
kaizencc Jul 7, 2025
541eeb4
Update packages/aws-cdk/test/commands/diff.test.ts
kaizencc Jul 7, 2025
59aecf1
iohost unit tests
kaizencc Jul 7, 2025
af1bbbb
Update packages/aws-cdk/lib/init-templates/.init-version.json
kaizencc Jul 7, 2025
6a86ee3
Update .recommended-feature-flags.json
kaizencc Jul 7, 2025
6427847
Update .recommended-feature-flags.json
kaizencc Jul 7, 2025
effed08
s
kaizencc Jul 7, 2025
27fa7de
fix
kaizencc Jul 8, 2025
75b6490
Merge branch 'main' into conroy/list
kaizencc Jul 8, 2025
9dc5a68
update test
kaizencc Jul 8, 2025
8341898
Apply suggestions from code review
kaizencc Jul 8, 2025
e783eb2
ci file
kaizencc Jul 9, 2025
4fad6c6
Merge branch 'main' into conroy/list
kaizencc Jul 9, 2025
31f5a92
refactors
kaizencc Jul 9, 2025
670a289
update-test
kaizencc Jul 10, 2025
8ea3e65
merge
kaizencc Jul 10, 2025
0a15701
delete previously deleted file
kaizencc Jul 10, 2025
d193044
fix merge
kaizencc Jul 10, 2025
c694d99
telemetry to file
kaizencc Jul 10, 2025
2b51b13
update
kaizencc Jul 10, 2025
d17faf7
add cli integ test for telemetry file
kaizencc Jul 10, 2025
1c94d1b
Merge branch 'main' into conroy/list
kaizencc Jul 11, 2025
1e138ac
update version and fix tests
kaizencc Jul 11, 2025
3b0917d
chore: self mutation
invalid-email-address Jul 11, 2025
956aa9f
Merge branch 'main' into conroy/list
kaizencc Jul 11, 2025
cfc070d
Merge branch 'main' into conroy/list
kaizencc Jul 11, 2025
3f850e0
integ test for failure
kaizencc Jul 11, 2025
2edd715
chore: self mutation
invalid-email-address Jul 11, 2025
e317acd
Merge branch 'main' into conroy/list
kaizencc Jul 16, 2025
17d4d19
Merge branch 'main' into conroy/list
kaizencc Jul 18, 2025
c53f310
Merge branch 'main' into conroy/list
iliapolo Jul 20, 2025
b37c6dd
build
iliapolo Jul 20, 2025
30e054b
pr feedback
kaizencc Jul 21, 2025
f36edc8
more pr feedback
kaizencc Jul 21, 2025
c933608
remove config for now
kaizencc Jul 21, 2025
136546b
final pr feedbacks
kaizencc Jul 21, 2025
7fb93b9
lint
kaizencc Jul 21, 2025
fb49fa4
unique files
kaizencc Jul 22, 2025
aa21c27
add feature flags
kaizencc Jul 22, 2025
4d8620f
Update packages/aws-cdk/README.md
kaizencc Jul 22, 2025
25afd6f
Apply suggestions from code review
kaizencc Jul 22, 2025
a90bf24
test changes
kaizencc Jul 22, 2025
b6911c9
rename featureflags
kaizencc Jul 22, 2025
cd2d2d0
rename file
kaizencc Jul 22, 2025
5b461b0
final pr feedback
kaizencc Jul 22, 2025
58254f8
update integs
kaizencc Jul 22, 2025
3b75fd9
Merge branch 'main' into conroy/list
kaizencc Jul 22, 2025
3a99e78
update tests
kaizencc Jul 22, 2025
e66d364
chore: self mutation
invalid-email-address Jul 22, 2025
84f6cc5
merge
kaizencc Jul 22, 2025
a737980
fix linter
kaizencc Jul 22, 2025
41389ff
chore: self mutation
invalid-email-address Jul 22, 2025
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
8 changes: 7 additions & 1 deletion packages/@aws-cdk/toolkit-lib/lib/api/io/private/messages.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ import type { StackRollbackProgress } from '../../../payloads/rollback';
import type { MfaTokenRequest, SdkTrace } from '../../../payloads/sdk';
import type { StackActivity, StackMonitoringControlEvent } from '../../../payloads/stack-activity';
import type { StackSelectionDetails } from '../../../payloads/synth';
import type { AssemblyData, ConfirmationRequest, ContextProviderMessageSource, Duration, ErrorPayload, SingleStack, StackAndAssemblyData } from '../../../payloads/types';
import type { AssemblyData, ConfirmationRequest, ContextProviderMessageSource, Duration, ErrorPayload, SingleStack, StackAndAssemblyData, Telemetry } from '../../../payloads/types';
import type { FileWatchEvent, WatchSettings } from '../../../payloads/watch';

/**
Expand All @@ -33,6 +33,12 @@ export const IO = {
description: 'Credential plugin warnings',
}),

CDK_TOOLKIT_I0050: make.debug<Telemetry>({
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

trace

Copy link
Contributor Author

@kaizencc kaizencc Jun 23, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

plus run by momo about the toolkit messages

code: 'CDK_TOOLKIT_I0050',
description: 'Generic Telemetric Event',
interface: 'Telemetry',
}),

// 1: Synth (1xxx)
CDK_TOOLKIT_I1000: make.info<Duration>({
code: 'CDK_TOOLKIT_I1000',
Expand Down
13 changes: 13 additions & 0 deletions packages/@aws-cdk/toolkit-lib/lib/payloads/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,19 @@ export interface SingleStack {
readonly stack: cxapi.CloudFormationStackArtifact;
}

export interface Telemetry {
readonly telemetry: {
readonly eventType: string,
readonly state: 'SUCCEEDED' | 'FAILED' | 'ABORTED';
readonly duration: number,
readonly error?: {
readonly name?: string;
readonly trace?: string;
readonly logs?: string;
},
}
}

/**
* Duration information returned in the payload of an IO Message.
*/
Expand Down
52 changes: 50 additions & 2 deletions packages/aws-cdk/lib/cli/cli.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ import { GLOBAL_PLUGIN_HOST } from './singleton-plugin-host';
import type { Command } from './user-configuration';
import { Configuration } from './user-configuration';
import * as version from './version';
import { asIoHelper } from '../../lib/api-private';
import { asIoHelper, IO } from '../../lib/api-private';
import type { IReadLock } from '../api';
import { ToolkitInfo, Notices } from '../api';
import { SdkProvider, IoHostSdkLogger, setSdkTracing, sdkRequestHandler } from '../api/aws-auth';
Expand Down Expand Up @@ -88,6 +88,13 @@ export async function exec(args: string[], synthesizer?: Synthesizer): Promise<n
});
await configuration.load();

if (process.env.TELEMETRY_TEST_ENV) {
ioHost.bindTelemetryClient(
argv,
configuration.context.all,
);
}

const ioHelper = asIoHelper(ioHost, ioHost.currentAction as any);

// Always create and use ProxyAgent to support configuration via env vars
Expand Down Expand Up @@ -613,17 +620,58 @@ function determineHotswapMode(hotswap?: boolean, hotswapFallback?: boolean, watc

/* c8 ignore start */ // we never call this in unit tests
export function cli(args: string[] = process.argv.slice(2)) {
const startTime = new Date().getTime();
exec(args)
.then(async (value) => {
if (typeof value === 'number') {
process.exitCode = value;
}
if (process.env.TELEMETRY_TEST_ENV) {
if (value === 1) {
await failedTelemetryExitEvent(args, startTime);
} else {
await successfulTelemetryExitEvent(args, startTime);
}
}
})
.catch((err) => {
.catch(async (err) => {
// Log the stack trace if we're on a developer workstation. Otherwise this will be into a minified
// file and the printed code line and stack trace are huge and useless.
prettyPrintError(err, version.isDeveloperBuild());
if (process.env.TELEMETRY_TEST_ENV) {
await failedTelemetryExitEvent(args, startTime, err);
}
process.exitCode = 1;
});
}
/* c8 ignore stop */

async function successfulTelemetryExitEvent(args: string[], startTime: number) {
await CliIoHost.instance().asIoHelper().notify(IO.CDK_TOOLKIT_I0050.msg(
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

method retrieves instance but not create a new one

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think I will do the .get() and change .instance() into .getOrCreate() in a separate PR. @iliapolo you good with that?

`Exiting ${args[1]}`,
{
telemetry: {
duration: new Date().getTime() - startTime,
eventType: 'Exit',
state: 'SUCCEEDED',
},
},
));
}

async function failedTelemetryExitEvent(args: string[], startTime: number, err?: any) {
await CliIoHost.instance().asIoHelper().notify(IO.CDK_TOOLKIT_I0050.msg(
`Exiting ${args[1]}`,
{
telemetry: {
duration: new Date().getTime() - startTime,
eventType: 'Exit',
state: 'FAILED',
error: {
name: err?.name ?? 'ExitCode1Error', // TODO: sanitize
// message: err.message, // TODO: sanitize
},
},
},
));
}
99 changes: 99 additions & 0 deletions packages/aws-cdk/lib/cli/io-host/cli-io-host.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,16 @@ 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 type { ITelemetryClient } from '../telemetry/client-interface';
import type { SessionSchema } from '../telemetry/schema';
import { randomUUID } from 'node:crypto';
import * as version from './../version';
import { IoHostTelemetryClient } from '../telemetry/io-host-client';
import { getInstallationId } from '../telemetry/installation-id';
import { makeConfig } from '../cli-config';
import { sanitizeCommandLineArguments, sanitizeContext } from '../telemetry/sanitation-utils';
import { AccountIdFetcher } from '../telemetry/account-id-fetcher';
import { RegionFetcher } from '../telemetry/region-fetcher';

export type { IIoHost, IoMessage, IoMessageCode, IoMessageLevel, IoRequest };

Expand Down Expand Up @@ -144,6 +154,10 @@ export class CliIoHost implements IIoHost {
private corkedCounter = 0;
private readonly corkedLoggingBuffer: IoMessage<unknown>[] = [];

private telemetryClient?: ITelemetryClient;
private telemetryInfo?: SessionSchema;
private telemetryCount = 0;

private constructor(props: CliIoHostProps = {}) {
this.currentAction = props.currentAction ?? 'none';
this.isTTY = props.isTTY ?? process.stdout.isTTY ?? false;
Expand All @@ -154,6 +168,45 @@ export class CliIoHost implements IIoHost {
this.stackProgress = props.stackProgress ?? StackActivityProgress.BAR;
}

/**
* Required for telemetry
*/
public async bindTelemetryClient(argv: any, context: {[key: string]: any}) {
// TODO: change this to EndpointTelemetryClient
this.telemetryClient = new IoHostTelemetryClient({
ioHost: this,
});

// sanitize the raw cli input
const command = sanitizeCommandLineArguments(argv, await makeConfig());
this.telemetryInfo = {
identifiers: {
installationId: getInstallationId(this.asIoHelper()),
sessionId: randomUUID(),
telemetryVersion: '1.0',
cdkCliVersion: version.versionNumber(),
accountId: await new AccountIdFetcher().fetch(),
region: await new RegionFetcher().fetch(),
},
event: {
command: {
path: command.path,
parameters: command.parameters,
config: sanitizeContext(context),
},
},
environment: {
ci: Boolean(process.env.CI),
os: {
platform: process.platform,
release: process.release.name,
},
nodeVersion: process.version,
},
project: {},
};
}

/**
* Returns the singleton instance
*/
Expand Down Expand Up @@ -248,6 +301,16 @@ export class CliIoHost implements IIoHost {
return this._internalIoHost.notify(msg);
}

if (process.env.TELEMETRY_TEST_ENV) {
try {
if (this.isTelemetryMessage(msg)) {
await this.emitTelemetry(msg);
}
} catch (e: any) {
this.defaults.debug(`Emit Telemetry Failed ${e.message}`);
}
}

if (this.isStackActivity(msg)) {
if (!this.activityPrinter) {
this.activityPrinter = this.makeActivityPrinter();
Expand All @@ -270,6 +333,42 @@ export class CliIoHost implements IIoHost {
stream?.write(output);
}

private isTelemetryMessage(msg: IoMessage<any>) {
return msg.data && msg.data.telemetry;
}

private async emitTelemetry(msg: IoMessage<any>) {
// Session has not been attached
if (!this.telemetryInfo) {
throw new ToolkitError('Session must be attached before telemetry is emitted');
}

const event = msg.data.telemetry;
await this.telemetryClient?.emit({
event: {
command: this.telemetryInfo.event.command,
state: event.state,
eventType: event.eventType,
},
identifiers: {
...this.telemetryInfo.identifiers,
eventId: `${this.telemetryInfo.identifiers.sessionId}:${this.telemetryCount}`,
timestamp: new Date().toISOString(),
},
environment: this.telemetryInfo.environment,
project: this.telemetryInfo.project,
duration: {
total: event.duration,
},
...(event.error ? {
error: {
name: event.error.name,
},
} : {}),
});
this.telemetryCount+=1;
}

/**
* Detect stack activity messages so they can be send to the printer.
*/
Expand Down
54 changes: 54 additions & 0 deletions packages/aws-cdk/lib/cli/telemetry/account-id-fetcher.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
import {
GetCallerIdentityCommand,
GetCallerIdentityCommandOutput,
STSClient,
} from '@aws-sdk/client-sts';
import { v5 as uuidV5 } from 'uuid';

// eslint-disable-next-line spellcheck/spell-checker
const CDK_CLI_UUID_NAMESPACE = 'f47ac10b-58cc-4372-a567-0e02b2c3d479'; // A random v4 UUID

/**
* Retrieves the account ID of the user
*/
export class AccountIdFetcher {
private accountIdPromise?: Promise<GetCallerIdentityCommandOutput>;
/**
* constructor for AccountIdFetcher
*/
constructor(private readonly stsClient = new STSClient()) {}
fetch = async () => {
if (this.accountIdPromise) {
try {
const stsResponse = await this.accountIdPromise;
return this.getAccountIdFromStsResponse(stsResponse);
} catch {
// We failed to get the account Id. Most likely the user doesn't have credentials
return;
}
}
try {
this.accountIdPromise = this.stsClient.send(
new GetCallerIdentityCommand({}),
);
const stsResponse = await this.accountIdPromise;
return this.getAccountIdFromStsResponse(stsResponse);
} catch {
// We failed to get the account Id. Most likely the user doesn't have credentials
return;
}
};

private getAccountIdFromStsResponse = (
stsResponse: GetCallerIdentityCommandOutput,
) => {
if (stsResponse && stsResponse.Account) {
return uuidV5(
stsResponse.Account,
CDK_CLI_UUID_NAMESPACE,
);
}
// We failed to get the account Id. Most likely the user doesn't have credentials
return;
};
}
14 changes: 14 additions & 0 deletions packages/aws-cdk/lib/cli/telemetry/client-interface.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import type { TelemetrySchema } from './schema';

/**
* Interface for all Telemetry Clients.
*
* A telemtry client receives event data and determines
* when and where to send it.
*/
export interface ITelemetryClient {
/**
* Send data to the client
*/
emit(event: TelemetrySchema): Promise<boolean>;
}
Loading
Loading