From 2e96368e8f533976a5647b162b1f24592a4c8581 Mon Sep 17 00:00:00 2001 From: Rico Huijbers Date: Thu, 5 Feb 2026 14:58:20 +0100 Subject: [PATCH 01/13] fix: not enough information to root cause performance problems --- .../toolkit-lib/lib/api/io/private/span.ts | 95 +++++++++++++++---- .../toolkit-lib/lib/toolkit/toolkit.ts | 58 +++++++---- .../aws-cdk/lib/cli/io-host/cli-io-host.ts | 1 + .../aws-cdk/lib/cli/telemetry/messages.ts | 5 + packages/aws-cdk/lib/cli/telemetry/session.ts | 18 ++++ 5 files changed, 139 insertions(+), 38 deletions(-) diff --git a/packages/@aws-cdk/toolkit-lib/lib/api/io/private/span.ts b/packages/@aws-cdk/toolkit-lib/lib/api/io/private/span.ts index 8c21f99cb..03e7895cd 100644 --- a/packages/@aws-cdk/toolkit-lib/lib/api/io/private/span.ts +++ b/packages/@aws-cdk/toolkit-lib/lib/api/io/private/span.ts @@ -7,8 +7,12 @@ import { formatTime } from '../../../util'; import type { IActionAwareIoHost } from '../io-host'; import type { IoDefaultMessages } from './io-default-messages'; +/** + * These data fields are automatically added by ending a span + */ export interface SpanEnd { readonly duration: number; + readonly counters?: Record; } /** @@ -23,34 +27,41 @@ export interface SpanDefinition { readonly end: make.IoMessageMaker; } + +/** + * Arguments to the span.end() function + * + * `SpanEnd` represents fields that are added by the underlying `end` function. + * + * - In principle, the intersection of the underlying type and the SpanEnd keys + * are made optional, the rest is the same as the underlying type. + * - If the expected type is subsumed by the SpanEnd type, then either void or + * their intersection with all fields optional. + */ +type SpanEndArguments = keyof T extends keyof SpanEnd + ? (Pick, keyof T & keyof SpanEnd> | void) + : Optional; + /** * Used in conditional types to check if a type (e.g. after omitting fields) is an empty object * This is needed because counter-intuitive neither `object` nor `{}` represent that. */ -type EmptyObject = { - [index: string | number | symbol]: never; -}; +type EmptyObject = Record; /** * Helper type to force a parameter to be not present of the computed type is an empty object */ type VoidWhenEmpty = T extends EmptyObject ? void : T; -/** - * Helper type to force a parameter to be an empty object if the computed type is an empty object - * This is weird, but some computed types (e.g. using `Omit`) don't end up enforcing this. - */ -type ForceEmpty = T extends EmptyObject ? EmptyObject : T; - /** * Make some properties optional */ -type Optional = Pick, K> & Omit; +type Optional = Omit & Pick, K>; /** * Ending the span returns the observed duration */ -interface ElapsedTime { +export interface ElapsedTime { readonly asMs: number; readonly asSec: number; } @@ -67,6 +78,7 @@ export interface IMessageSpan extends IActionAwareIoHost { * An IoDefaultMessages wrapped around the span. */ readonly defaults: IoDefaultMessages; + /** * Get the time elapsed since the start */ @@ -79,15 +91,34 @@ export interface IMessageSpan extends IActionAwareIoHost { /** * End the span with a payload */ - end(payload: VoidWhenEmpty>): Promise; + end(payload: SpanEndArguments): Promise; /** - * End the span with a payload, overwriting + * End the span with a message and payload */ - end(payload: VoidWhenEmpty>): Promise; + end(message: string, payload: SpanEndArguments): Promise; + /** - * End the span with a message and payload + * Increment a counter */ - end(message: string, payload: ForceEmpty>): Promise; + incCounter(name: string, delta?: number): void; + + /** + * Return a new timer object + * + * It will be added into the span data when it's stopped. All open timers are + * automatically stopped when the span is ended. + * + * Timers are ultimately added to the `counters` array with `_ms` and + * `_cnt` keys. + */ + startTimer(name: string): ITimer; +} + +/** + * A timer to time an operation in a span. + */ +export interface ITimer { + stop(): void; } /** @@ -133,6 +164,8 @@ class MessageSpan implements IMessageSpan = {}; + private readonly openTimers = new Set(); public constructor(ioHelper: IoHelper, definition: SpanDefinition, makeHelper: (ioHost: IActionAwareIoHost) => IoHelper) { this.definition = definition; @@ -161,26 +194,50 @@ class MessageSpan implements IMessageSpan): Promise { return this.ioHelper.notify(withSpanId(this.spanId, msg)); } - public async end(x: any, y?: ForceEmpty>): Promise { + public async end(x: any, y?: SpanEndArguments): Promise { const duration = this.time(); - const endInput = parseArgs>>(x, y); + for (const t of this.openTimers) { + t.stop(); + } + this.openTimers.clear(); + + const endInput = parseArgs>(x, y); const endMsg = endInput.message ?? util.format(this.timingMsgTemplate, this.definition.name, duration.asSec); const endPayload = endInput.payload; await this.notify(this.definition.end.msg( endMsg, { duration: duration.asMs, + ...(Object.keys(this.counters).length > 0 ? { counters: this.counters } : {}), ...endPayload, } as E)); return duration; } + public incCounter(name: string, delta: number = 1): void { + this.counters[name] = (this.counters[name] ?? 0) + delta; + } + public async requestResponse(msg: ActionLessRequest): Promise { return this.ioHelper.requestResponse(withSpanId(this.spanId, msg)); } + public startTimer(name: string): ITimer { + const start = Date.now(); + + const t: ITimer = { + stop: () => { + this.openTimers.delete(t); + this.incCounter(`${name}_ms`, Math.floor(Date.now() - start) / 1000); + this.incCounter(`${name}_cnt`, 1); + }, + }; + this.openTimers.add(t); + return t; + } + private time() { const elapsedTime = new Date().getTime() - this.startTime; return { @@ -190,7 +247,7 @@ class MessageSpan implements IMessageSpan(first: any, second?: S): { message: string | undefined; payload: S } { +function parseArgs(first: any, second?: S): { message: string | undefined; payload: S } { const firstIsMessage = typeof first === 'string'; // When the first argument is a string or we have a second argument, then the first arg is the message diff --git a/packages/@aws-cdk/toolkit-lib/lib/toolkit/toolkit.ts b/packages/@aws-cdk/toolkit-lib/lib/toolkit/toolkit.ts index cb0d11dc1..a25a8c067 100644 --- a/packages/@aws-cdk/toolkit-lib/lib/toolkit/toolkit.ts +++ b/packages/@aws-cdk/toolkit-lib/lib/toolkit/toolkit.ts @@ -71,7 +71,7 @@ import { DiffFormatter } from '../api/diff'; import { detectStackDrift } from '../api/drift'; import { DriftFormatter } from '../api/drift/drift-formatter'; import type { IIoHost, IoMessageLevel, ToolkitAction } from '../api/io'; -import type { IoHelper } from '../api/io/private'; +import type { ElapsedTime, IMessageSpan, IoHelper } from '../api/io/private'; import { asIoHelper, IO, SPAN, withoutColor, withoutEmojis, withTrimmedWhitespace } from '../api/io/private'; import { CloudWatchLogEventMonitor, findCloudWatchLogGroups } from '../api/logs-monitor'; import { Mode, PluginHost } from '../api/plugin'; @@ -321,12 +321,12 @@ export class Toolkit extends CloudAssemblySourceBuilder { const synthSpan = await ioHelper.span(SPAN.SYNTH_ASSEMBLY).begin({ stacks: selectStacks }); // NOTE: NOT 'await using' because we return ownership to the caller - const assembly = await assemblyFromSource(synthSpan.asHelper, cx); + const assembly = await loggedAssemblyFromSource(synthSpan, cx); + await synthSpan.end(); const stacks = await assembly.selectStacksV2(selectStacks); const autoValidateStacks = options.validateStacks ? [assembly.selectStacksForValidation()] : []; await this.validateStacksMetadata(stacks.concat(...autoValidateStacks), synthSpan.asHelper); - await synthSpan.end(); // if we have a single stack, print it to STDOUT const message = `Successfully synthesized to ${chalk.blue(path.resolve(stacks.assembly.directory))}`; @@ -366,10 +366,10 @@ export class Toolkit extends CloudAssemblySourceBuilder { const ioHelper = asIoHelper(this.ioHost, 'diff'); const selectStacks = options.stacks ?? ALL_STACKS; const synthSpan = await ioHelper.span(SPAN.SYNTH_ASSEMBLY).begin({ stacks: selectStacks }); - await using assembly = await assemblyFromSource(synthSpan.asHelper, cx); - const stacks = await assembly.selectStacksV2(selectStacks); + await using assembly = await loggedAssemblyFromSource(synthSpan, cx); await synthSpan.end(); + const stacks = await assembly.selectStacksV2(selectStacks); const diffSpan = await ioHelper.span(SPAN.DIFF_STACK).begin({ stacks: selectStacks }); const deployments = await this.deploymentsForAction('diff'); @@ -424,10 +424,11 @@ export class Toolkit extends CloudAssemblySourceBuilder { const ioHelper = asIoHelper(this.ioHost, 'drift'); const selectStacks = options.stacks ?? ALL_STACKS; const synthSpan = await ioHelper.span(SPAN.SYNTH_ASSEMBLY).begin({ stacks: selectStacks }); - await using assembly = await assemblyFromSource(synthSpan.asHelper, cx); - const stacks = await assembly.selectStacksV2(selectStacks); + await using assembly = await loggedAssemblyFromSource(synthSpan, cx); await synthSpan.end(); + const stacks = await assembly.selectStacksV2(selectStacks); + const driftSpan = await ioHelper.span(SPAN.DRIFT_APP).begin({ stacks: selectStacks }); const allDriftResults: { [name: string]: DriftResult } = {}; const unavailableDrifts = []; @@ -502,10 +503,10 @@ export class Toolkit extends CloudAssemblySourceBuilder { const ioHelper = asIoHelper(this.ioHost, 'list'); const selectStacks = options.stacks ?? ALL_STACKS; const synthSpan = await ioHelper.span(SPAN.SYNTH_ASSEMBLY).begin({ stacks: selectStacks }); - await using assembly = await assemblyFromSource(ioHelper, cx); - const stackCollection = await assembly.selectStacksV2(selectStacks); + await using assembly = await loggedAssemblyFromSource(synthSpan, cx); await synthSpan.end(); + const stackCollection = await assembly.selectStacksV2(selectStacks); const stacks = stackCollection.withDependencies(); const message = stacks.map(s => s.id).join('\n'); @@ -520,20 +521,22 @@ export class Toolkit extends CloudAssemblySourceBuilder { */ public async deploy(cx: ICloudAssemblySource, options: DeployOptions = {}): Promise { const ioHelper = asIoHelper(this.ioHost, 'deploy'); - await using assembly = await assemblyFromSource(ioHelper, cx); - return await this._deploy(assembly, 'deploy', options); + const selectStacks = options.stacks ?? ALL_STACKS; + const synthSpan = await ioHelper.span(SPAN.SYNTH_ASSEMBLY).begin({ stacks: selectStacks }); + await using assembly = await loggedAssemblyFromSource(synthSpan, cx); + const synthDuration = await synthSpan.end(); + + return await this._deploy(assembly, 'deploy', synthDuration, options); } /** * Helper to allow deploy being called as part of the watch action. */ - private async _deploy(assembly: StackAssembly, action: 'deploy' | 'watch', options: PrivateDeployOptions = {}): Promise { + private async _deploy(assembly: StackAssembly, action: 'deploy' | 'watch', synthDuration: ElapsedTime, options: PrivateDeployOptions = {}): Promise { const ioHelper = asIoHelper(this.ioHost, action); const selectStacks = options.stacks ?? ALL_STACKS; - const synthSpan = await ioHelper.span(SPAN.SYNTH_ASSEMBLY).begin({ stacks: selectStacks }); const stackCollection = await assembly.selectStacksV2(selectStacks); await this.validateStacksMetadata(stackCollection, ioHelper); - const synthDuration = await synthSpan.end(); const ret: DeployResult = { stacks: [], @@ -997,7 +1000,11 @@ export class Toolkit extends CloudAssemblySourceBuilder { */ public async rollback(cx: ICloudAssemblySource, options: RollbackOptions = {}): Promise { const ioHelper = asIoHelper(this.ioHost, 'rollback'); + const selectStacks = options.stacks ?? ALL_STACKS; + const synthSpan = await ioHelper.span(SPAN.SYNTH_ASSEMBLY).begin({ stacks: selectStacks }); await using assembly = await assemblyFromSource(ioHelper, cx); + await synthSpan.end(); + return await this._rollback(assembly, 'rollback', options); } @@ -1007,10 +1014,9 @@ export class Toolkit extends CloudAssemblySourceBuilder { private async _rollback(assembly: StackAssembly, action: 'rollback' | 'deploy' | 'watch', options: RollbackOptions): Promise { const selectStacks = options.stacks ?? ALL_STACKS; const ioHelper = asIoHelper(this.ioHost, action); - const synthSpan = await ioHelper.span(SPAN.SYNTH_ASSEMBLY).begin({ stacks: selectStacks }); + const stacks = await assembly.selectStacksV2(selectStacks); await this.validateStacksMetadata(stacks, ioHelper); - await synthSpan.end(); const ret: RollbackResult = { stacks: [], @@ -1236,7 +1242,12 @@ export class Toolkit extends CloudAssemblySourceBuilder { */ public async destroy(cx: ICloudAssemblySource, options: DestroyOptions = {}): Promise { const ioHelper = asIoHelper(this.ioHost, 'destroy'); + + const selectStacks = options.stacks ?? ALL_STACKS; + const synthSpan = await ioHelper.span(SPAN.SYNTH_ASSEMBLY).begin({ stacks: selectStacks }); await using assembly = await assemblyFromSource(ioHelper, cx); + await synthSpan.end(); + return await this._destroy(assembly, 'destroy', options); } @@ -1246,10 +1257,8 @@ export class Toolkit extends CloudAssemblySourceBuilder { private async _destroy(assembly: StackAssembly, action: 'deploy' | 'destroy', options: DestroyOptions): Promise { const selectStacks = options.stacks ?? ALL_STACKS; const ioHelper = asIoHelper(this.ioHost, action); - const synthSpan = await ioHelper.span(SPAN.SYNTH_ASSEMBLY).begin({ stacks: selectStacks }); // The stacks will have been ordered for deployment, so reverse them for deletion. const stacks = (await assembly.selectStacksV2(selectStacks)).reversed(); - await synthSpan.end(); const ret: DestroyResult = { stacks: [], @@ -1352,7 +1361,7 @@ export class Toolkit extends CloudAssemblySourceBuilder { }; try { - await this._deploy(assembly, 'watch', deployOptions); + await this._deploy(assembly, 'watch', zeroTime(), deployOptions); } catch { // just continue - deploy will show the error } @@ -1410,3 +1419,14 @@ export class Toolkit extends CloudAssemblySourceBuilder { } } +/** + * Produce an assembly from a source, and emit the number of stacks to the span + */ +async function loggedAssemblyFromSource(span: IMessageSpan, assemblySource: ICloudAssemblySource, cache: boolean = true) { + const ret = await assemblyFromSource(span.asHelper, assemblySource, cache); + return ret; +} + +function zeroTime(): ElapsedTime { + return { asMs: 0, asSec: 0 }; +} \ No newline at end of file diff --git a/packages/aws-cdk/lib/cli/io-host/cli-io-host.ts b/packages/aws-cdk/lib/cli/io-host/cli-io-host.ts index 0909e6730..47dc743bd 100644 --- a/packages/aws-cdk/lib/cli/io-host/cli-io-host.ts +++ b/packages/aws-cdk/lib/cli/io-host/cli-io-host.ts @@ -632,6 +632,7 @@ function eventFromMessage(msg: IoMessage): TelemetryEvent | undefined { eventType, duration: m.data.duration, error: m.data.error, + counters: m.data.counters, }; } } diff --git a/packages/aws-cdk/lib/cli/telemetry/messages.ts b/packages/aws-cdk/lib/cli/telemetry/messages.ts index 3682be875..2f683bb26 100644 --- a/packages/aws-cdk/lib/cli/telemetry/messages.ts +++ b/packages/aws-cdk/lib/cli/telemetry/messages.ts @@ -5,6 +5,11 @@ import type { SpanDefinition } from '../../api-private'; export interface EventResult extends Duration { error?: ErrorDetails; + + /** + * Counts of noteworthy things in this event + */ + counters?: Record; } export interface EventStart { diff --git a/packages/aws-cdk/lib/cli/telemetry/session.ts b/packages/aws-cdk/lib/cli/telemetry/session.ts index 1f6087051..9c6465e93 100644 --- a/packages/aws-cdk/lib/cli/telemetry/session.ts +++ b/packages/aws-cdk/lib/cli/telemetry/session.ts @@ -27,6 +27,22 @@ export interface TelemetryEvent { readonly eventType: EventType; readonly duration: number; readonly error?: ErrorDetails; + counters?: Record; +} + +/** + * Timer of a single event + */ +export interface Timing { + /** + * Total time spent in this operation + */ + totalMs: number; + + /** + * Count of operations that together took `totalMs`. + */ + count: number; } export class TelemetrySession { @@ -110,6 +126,7 @@ export class TelemetrySession { public async emit(event: TelemetryEvent): Promise { this.count += 1; + return this.client.emit({ event: { command: this.sessionInfo.event.command, @@ -131,6 +148,7 @@ export class TelemetrySession { name: event.error.name, }, } : {}), + ...(event.counters && Object.keys(event.counters).length > 0 ? { counters: event.counters } : {}), }); } From 6684eb7261ecc115b9e90a0bf1c89238f3f63f76 Mon Sep 17 00:00:00 2001 From: Rico Huijbers Date: Thu, 5 Feb 2026 15:01:07 +0100 Subject: [PATCH 02/13] fix(toolkit-lib): synth time is not measured accurately In multiple places the `SYNTH` span doesn't measure the time to synthesize the assembly, but rather the time to select stacks from an already-synthesized assembly. Presumably this was done in order to avoid duplicating this line: ```ts const selectStacks = options.stacks ?? ALL_STACKS; ``` Instead, introduce a helper function that gets called from all places, and centralize the `selectStacks` logic so we don't fear duplication anymore. --- .../lib/api/io/private/messages.ts | 5 +- .../toolkit-lib/lib/api/io/private/span.ts | 2 +- .../toolkit-lib/lib/payloads/types.ts | 10 ++ .../toolkit-lib/lib/toolkit/toolkit.ts | 93 +++++++++++-------- 4 files changed, 68 insertions(+), 42 deletions(-) diff --git a/packages/@aws-cdk/toolkit-lib/lib/api/io/private/messages.ts b/packages/@aws-cdk/toolkit-lib/lib/api/io/private/messages.ts index 00e8158da..685c799b2 100644 --- a/packages/@aws-cdk/toolkit-lib/lib/api/io/private/messages.ts +++ b/packages/@aws-cdk/toolkit-lib/lib/api/io/private/messages.ts @@ -24,6 +24,7 @@ import type { ContextProviderMessageSource, Duration, ErrorPayload, + Operation, SingleStack, StackAndAssemblyData, } from '../../../payloads/types'; @@ -43,10 +44,10 @@ export const IO = { }), // 1: Synth (1xxx) - CDK_TOOLKIT_I1000: make.info({ + CDK_TOOLKIT_I1000: make.info({ code: 'CDK_TOOLKIT_I1000', description: 'Provides synthesis times.', - interface: 'Duration', + interface: 'Operation', }), CDK_TOOLKIT_I1001: make.trace({ code: 'CDK_TOOLKIT_I1001', diff --git a/packages/@aws-cdk/toolkit-lib/lib/api/io/private/span.ts b/packages/@aws-cdk/toolkit-lib/lib/api/io/private/span.ts index 8c21f99cb..c06043645 100644 --- a/packages/@aws-cdk/toolkit-lib/lib/api/io/private/span.ts +++ b/packages/@aws-cdk/toolkit-lib/lib/api/io/private/span.ts @@ -50,7 +50,7 @@ type Optional = Pick, K> & Omit; /** * Ending the span returns the observed duration */ -interface ElapsedTime { +export interface ElapsedTime { readonly asMs: number; readonly asSec: number; } diff --git a/packages/@aws-cdk/toolkit-lib/lib/payloads/types.ts b/packages/@aws-cdk/toolkit-lib/lib/payloads/types.ts index 34bb4adc5..33fcd6835 100644 --- a/packages/@aws-cdk/toolkit-lib/lib/payloads/types.ts +++ b/packages/@aws-cdk/toolkit-lib/lib/payloads/types.ts @@ -90,6 +90,16 @@ export interface ErrorPayload { readonly error: Error; } +/** + * Operation information that *definitely* took time, and *maybe* produced an error + */ +export interface Operation extends Duration { + /** + * Optionally, an error that occurred + */ + readonly error?: Error; +} + /** * Generic payload of a simple yes/no question. * diff --git a/packages/@aws-cdk/toolkit-lib/lib/toolkit/toolkit.ts b/packages/@aws-cdk/toolkit-lib/lib/toolkit/toolkit.ts index cb0d11dc1..46e03106a 100644 --- a/packages/@aws-cdk/toolkit-lib/lib/toolkit/toolkit.ts +++ b/packages/@aws-cdk/toolkit-lib/lib/toolkit/toolkit.ts @@ -60,7 +60,7 @@ import { import { sdkRequestHandler } from '../api/aws-auth/awscli-compatible'; import { IoHostSdkLogger, SdkProvider } from '../api/aws-auth/private'; import { Bootstrapper } from '../api/bootstrap'; -import type { ICloudAssemblySource } from '../api/cloud-assembly'; +import type { ICloudAssemblySource, StackSelector } from '../api/cloud-assembly'; import { CachedCloudAssembly, StackSelectionStrategy } from '../api/cloud-assembly'; import type { StackAssembly } from '../api/cloud-assembly/private'; import { ALL_STACKS } from '../api/cloud-assembly/private'; @@ -71,7 +71,7 @@ import { DiffFormatter } from '../api/diff'; import { detectStackDrift } from '../api/drift'; import { DriftFormatter } from '../api/drift/drift-formatter'; import type { IIoHost, IoMessageLevel, ToolkitAction } from '../api/io'; -import type { IoHelper } from '../api/io/private'; +import type { ElapsedTime, IoHelper } from '../api/io/private'; import { asIoHelper, IO, SPAN, withoutColor, withoutEmojis, withTrimmedWhitespace } from '../api/io/private'; import { CloudWatchLogEventMonitor, findCloudWatchLogGroups } from '../api/logs-monitor'; import { Mode, PluginHost } from '../api/plugin'; @@ -317,16 +317,13 @@ export class Toolkit extends CloudAssemblySourceBuilder { */ public async synth(cx: ICloudAssemblySource, options: SynthOptions = {}): Promise { const ioHelper = asIoHelper(this.ioHost, 'synth'); - const selectStacks = options.stacks ?? ALL_STACKS; - const synthSpan = await ioHelper.span(SPAN.SYNTH_ASSEMBLY).begin({ stacks: selectStacks }); // NOTE: NOT 'await using' because we return ownership to the caller - const assembly = await assemblyFromSource(synthSpan.asHelper, cx); + const assembly = await synthAndMeasure(ioHelper, cx, stacksOpt(options)); - const stacks = await assembly.selectStacksV2(selectStacks); + const stacks = await assembly.selectStacksV2(stacksOpt(options)); const autoValidateStacks = options.validateStacks ? [assembly.selectStacksForValidation()] : []; - await this.validateStacksMetadata(stacks.concat(...autoValidateStacks), synthSpan.asHelper); - await synthSpan.end(); + await this.validateStacksMetadata(stacks.concat(...autoValidateStacks), ioHelper); // if we have a single stack, print it to STDOUT const message = `Successfully synthesized to ${chalk.blue(path.resolve(stacks.assembly.directory))}`; @@ -364,12 +361,10 @@ export class Toolkit extends CloudAssemblySourceBuilder { */ public async diff(cx: ICloudAssemblySource, options: DiffOptions = {}): Promise<{ [name: string]: TemplateDiff }> { const ioHelper = asIoHelper(this.ioHost, 'diff'); - const selectStacks = options.stacks ?? ALL_STACKS; - const synthSpan = await ioHelper.span(SPAN.SYNTH_ASSEMBLY).begin({ stacks: selectStacks }); - await using assembly = await assemblyFromSource(synthSpan.asHelper, cx); - const stacks = await assembly.selectStacksV2(selectStacks); - await synthSpan.end(); + const selectStacks = stacksOpt(options); + await using assembly = await synthAndMeasure(ioHelper, cx, selectStacks); + const stacks = await assembly.selectStacksV2(selectStacks); const diffSpan = await ioHelper.span(SPAN.DIFF_STACK).begin({ stacks: selectStacks }); const deployments = await this.deploymentsForAction('diff'); @@ -422,11 +417,10 @@ export class Toolkit extends CloudAssemblySourceBuilder { */ public async drift(cx: ICloudAssemblySource, options: DriftOptions = {}): Promise<{ [name: string]: DriftResult }> { const ioHelper = asIoHelper(this.ioHost, 'drift'); - const selectStacks = options.stacks ?? ALL_STACKS; - const synthSpan = await ioHelper.span(SPAN.SYNTH_ASSEMBLY).begin({ stacks: selectStacks }); - await using assembly = await assemblyFromSource(synthSpan.asHelper, cx); + const selectStacks = stacksOpt(options); + await using assembly = await synthAndMeasure(ioHelper, cx, selectStacks); + const stacks = await assembly.selectStacksV2(selectStacks); - await synthSpan.end(); const driftSpan = await ioHelper.span(SPAN.DRIFT_APP).begin({ stacks: selectStacks }); const allDriftResults: { [name: string]: DriftResult } = {}; @@ -500,12 +494,10 @@ export class Toolkit extends CloudAssemblySourceBuilder { */ public async list(cx: ICloudAssemblySource, options: ListOptions = {}): Promise { const ioHelper = asIoHelper(this.ioHost, 'list'); - const selectStacks = options.stacks ?? ALL_STACKS; - const synthSpan = await ioHelper.span(SPAN.SYNTH_ASSEMBLY).begin({ stacks: selectStacks }); - await using assembly = await assemblyFromSource(ioHelper, cx); - const stackCollection = await assembly.selectStacksV2(selectStacks); - await synthSpan.end(); + const selectStacks = stacksOpt(options); + await using assembly = await synthAndMeasure(ioHelper, cx, selectStacks); + const stackCollection = await assembly.selectStacksV2(selectStacks); const stacks = stackCollection.withDependencies(); const message = stacks.map(s => s.id).join('\n'); @@ -520,20 +512,19 @@ export class Toolkit extends CloudAssemblySourceBuilder { */ public async deploy(cx: ICloudAssemblySource, options: DeployOptions = {}): Promise { const ioHelper = asIoHelper(this.ioHost, 'deploy'); - await using assembly = await assemblyFromSource(ioHelper, cx); - return await this._deploy(assembly, 'deploy', options); + await using assembly = await synthAndMeasure(ioHelper, cx, stacksOpt(options)); + + return await this._deploy(assembly, 'deploy', assembly.synthDuration, options); } /** * Helper to allow deploy being called as part of the watch action. */ - private async _deploy(assembly: StackAssembly, action: 'deploy' | 'watch', options: PrivateDeployOptions = {}): Promise { + private async _deploy(assembly: StackAssembly, action: 'deploy' | 'watch', synthDuration: ElapsedTime, options: PrivateDeployOptions = {}): Promise { const ioHelper = asIoHelper(this.ioHost, action); - const selectStacks = options.stacks ?? ALL_STACKS; - const synthSpan = await ioHelper.span(SPAN.SYNTH_ASSEMBLY).begin({ stacks: selectStacks }); + const selectStacks = stacksOpt(options); const stackCollection = await assembly.selectStacksV2(selectStacks); await this.validateStacksMetadata(stackCollection, ioHelper); - const synthDuration = await synthSpan.end(); const ret: DeployResult = { stacks: [], @@ -997,7 +988,8 @@ export class Toolkit extends CloudAssemblySourceBuilder { */ public async rollback(cx: ICloudAssemblySource, options: RollbackOptions = {}): Promise { const ioHelper = asIoHelper(this.ioHost, 'rollback'); - await using assembly = await assemblyFromSource(ioHelper, cx); + await using assembly = await synthAndMeasure(ioHelper, cx, stacksOpt(options)); + return await this._rollback(assembly, 'rollback', options); } @@ -1005,12 +997,11 @@ export class Toolkit extends CloudAssemblySourceBuilder { * Helper to allow rollback being called as part of the deploy or watch action. */ private async _rollback(assembly: StackAssembly, action: 'rollback' | 'deploy' | 'watch', options: RollbackOptions): Promise { - const selectStacks = options.stacks ?? ALL_STACKS; + const selectStacks = stacksOpt(options); const ioHelper = asIoHelper(this.ioHost, action); - const synthSpan = await ioHelper.span(SPAN.SYNTH_ASSEMBLY).begin({ stacks: selectStacks }); + const stacks = await assembly.selectStacksV2(selectStacks); await this.validateStacksMetadata(stacks, ioHelper); - await synthSpan.end(); const ret: RollbackResult = { stacks: [], @@ -1072,13 +1063,13 @@ export class Toolkit extends CloudAssemblySourceBuilder { this.requireUnstableFeature('refactor'); const ioHelper = asIoHelper(this.ioHost, 'refactor'); - await using assembly = await assemblyFromSource(ioHelper, cx); + await using assembly = await synthAndMeasure(ioHelper, cx, stacksOpt(options)); return await this._refactor(assembly, ioHelper, cx, options); } private async _refactor(assembly: StackAssembly, ioHelper: IoHelper, cx: ICloudAssemblySource, options: RefactorOptions = {}): Promise { const sdkProvider = await this.sdkProvider('refactor'); - const selectedStacks = await assembly.selectStacksV2(options.stacks ?? ALL_STACKS); + const selectedStacks = await assembly.selectStacksV2(stacksOpt(options)); const groups = await groupStacks(sdkProvider, selectedStacks.stackArtifacts, options.additionalStackNames ?? []); for (let { environment, localStacks, deployedStacks } of groups) { @@ -1236,7 +1227,7 @@ export class Toolkit extends CloudAssemblySourceBuilder { */ public async destroy(cx: ICloudAssemblySource, options: DestroyOptions = {}): Promise { const ioHelper = asIoHelper(this.ioHost, 'destroy'); - await using assembly = await assemblyFromSource(ioHelper, cx); + await using assembly = await synthAndMeasure(ioHelper, cx, stacksOpt(options)); return await this._destroy(assembly, 'destroy', options); } @@ -1244,12 +1235,10 @@ export class Toolkit extends CloudAssemblySourceBuilder { * Helper to allow destroy being called as part of the deploy action. */ private async _destroy(assembly: StackAssembly, action: 'deploy' | 'destroy', options: DestroyOptions): Promise { - const selectStacks = options.stacks ?? ALL_STACKS; + const selectStacks = stacksOpt(options); const ioHelper = asIoHelper(this.ioHost, action); - const synthSpan = await ioHelper.span(SPAN.SYNTH_ASSEMBLY).begin({ stacks: selectStacks }); // The stacks will have been ordered for deployment, so reverse them for deletion. const stacks = (await assembly.selectStacksV2(selectStacks)).reversed(); - await synthSpan.end(); const ret: DestroyResult = { stacks: [], @@ -1352,7 +1341,7 @@ export class Toolkit extends CloudAssemblySourceBuilder { }; try { - await this._deploy(assembly, 'watch', deployOptions); + await this._deploy(assembly, 'watch', zeroTime(), deployOptions); } catch { // just continue - deploy will show the error } @@ -1410,3 +1399,29 @@ export class Toolkit extends CloudAssemblySourceBuilder { } } +/** + * Centralize the default stack selection logic in a single place + */ +function stacksOpt(o: { stacks?: StackSelector }): StackSelector { + return o.stacks ?? ALL_STACKS; +} + +/** + * Perform synthesis and emit the time taken to a new span + */ +async function synthAndMeasure(ioHelper: IoHelper, cx: ICloudAssemblySource, selectStacks: StackSelector): Promise { + const synthSpan = await ioHelper.span(SPAN.SYNTH_ASSEMBLY).begin({ stacks: selectStacks }); + try { + const ret = await assemblyFromSource(synthSpan.asHelper, cx); + const synthDuration = await synthSpan.end({}); + return Object.assign(ret, { synthDuration }); + } catch (error: any) { + // End the span even if we had a failure + await synthSpan.end({ error }); + throw error; + } +} + +function zeroTime(): ElapsedTime { + return { asMs: 0, asSec: 0 }; +} \ No newline at end of file From b1fc30f01898891f66e6dcf5ba5164e2795595f3 Mon Sep 17 00:00:00 2001 From: Rico Huijbers Date: Fri, 6 Feb 2026 16:35:08 +0100 Subject: [PATCH 03/13] WIP --- .../toolkit-lib/lib/toolkit/toolkit.ts | 2 - packages/aws-cdk/lib/cli/cdk-toolkit.ts | 1 + packages/aws-cdk/lib/cli/cli.ts | 5 +- packages/aws-cdk/lib/cli/telemetry/schema.ts | 1 + packages/aws-cdk/lib/cli/telemetry/session.ts | 28 +++++++++ packages/aws-cdk/lib/cli/util/detect-agent.ts | 34 +++++++++++ .../aws-cdk/lib/cli/util/guess-language.ts | 60 +++++++++++++++++++ .../aws-cdk/lib/cxapp/cloud-executable.ts | 46 +++++++++----- 8 files changed, 159 insertions(+), 18 deletions(-) create mode 100644 packages/aws-cdk/lib/cli/util/detect-agent.ts create mode 100644 packages/aws-cdk/lib/cli/util/guess-language.ts diff --git a/packages/@aws-cdk/toolkit-lib/lib/toolkit/toolkit.ts b/packages/@aws-cdk/toolkit-lib/lib/toolkit/toolkit.ts index 66f27674b..46e03106a 100644 --- a/packages/@aws-cdk/toolkit-lib/lib/toolkit/toolkit.ts +++ b/packages/@aws-cdk/toolkit-lib/lib/toolkit/toolkit.ts @@ -422,8 +422,6 @@ export class Toolkit extends CloudAssemblySourceBuilder { const stacks = await assembly.selectStacksV2(selectStacks); - const stacks = await assembly.selectStacksV2(selectStacks); - const driftSpan = await ioHelper.span(SPAN.DRIFT_APP).begin({ stacks: selectStacks }); const allDriftResults: { [name: string]: DriftResult } = {}; const unavailableDrifts = []; diff --git a/packages/aws-cdk/lib/cli/cdk-toolkit.ts b/packages/aws-cdk/lib/cli/cdk-toolkit.ts index afde16837..ae155b088 100644 --- a/packages/aws-cdk/lib/cli/cdk-toolkit.ts +++ b/packages/aws-cdk/lib/cli/cdk-toolkit.ts @@ -542,6 +542,7 @@ export class CdkToolkit { // There is already a startDeployTime constant, but that does not work with telemetry. // We should integrate the two in the future const deploySpan = await this.ioHost.asIoHelper().span(CLI_PRIVATE_SPAN.DEPLOY).begin({}); + let error: ErrorDetails | undefined; let elapsedDeployTime = 0; try { diff --git a/packages/aws-cdk/lib/cli/cli.ts b/packages/aws-cdk/lib/cli/cli.ts index a0775adb0..b3b4e31ff 100644 --- a/packages/aws-cdk/lib/cli/cli.ts +++ b/packages/aws-cdk/lib/cli/cli.ts @@ -37,6 +37,7 @@ import type { ErrorDetails } from './telemetry/schema'; import { isCI } from './util/ci'; import { isDeveloperBuildVersion, versionWithBuild, versionNumber } from './version'; import { getLanguageFromAlias } from '../commands/language'; +import { guessLanguage } from './util/guess-language'; export async function exec(args: string[], synthesizer?: Synthesizer): Promise { const argv = await parseCommandLineArguments(args); @@ -107,11 +108,13 @@ export async function exec(args: string[], synthesizer?: Synthesizer): Promise(x: A): { -readonly [k in keyof A]: A[k] } { + return x; +} \ No newline at end of file diff --git a/packages/aws-cdk/lib/cli/util/detect-agent.ts b/packages/aws-cdk/lib/cli/util/detect-agent.ts new file mode 100644 index 000000000..dd610fd80 --- /dev/null +++ b/packages/aws-cdk/lib/cli/util/detect-agent.ts @@ -0,0 +1,34 @@ + +/** + * Guess whether we're being executed by an AI agent + * + * It's hard for us to say `false` for sure, so we only respond + * with `yes` or `don't know`. + */ +export function guessAgent(): true | undefined { + if ((process.env.AWS_EXECUTION_ENV ?? '').toLocaleLowerCase().includes('amazonq')) { + return true; + } + + if (process.env.CLAUDECODE) { + return true; + } + + // Expecting CODEX_SANDBOX, CODEX_THREAD_ID + if (Object.keys(process.env).some(x => x.startsWith('CODEX_'))) { + return true; + } + + if (process.env.CURSOR_AGENT) { + return true; + } + + // Cline -- not sure if it sets these, but users might to configure Cline. + if (Object.keys(process.env).some(x => x.startsWith('CLINE_'))) { + return true; + } + + // Copilot doesn't set an envvar (at least not in VS Code) + + return undefined; +} \ No newline at end of file diff --git a/packages/aws-cdk/lib/cli/util/guess-language.ts b/packages/aws-cdk/lib/cli/util/guess-language.ts new file mode 100644 index 000000000..ad49fc5be --- /dev/null +++ b/packages/aws-cdk/lib/cli/util/guess-language.ts @@ -0,0 +1,60 @@ +import * as fs from 'fs-extra'; +import * as path from 'path'; + +/** + * Guess the CDK app language based on the files in the given directory + * + * Returns `undefined` if our guess fails. + */ +export async function guessLanguage(dir: string): Promise { + try { + const files = new Set(await listFiles(dir, 2)); + + if (files.has('package.json')) { + const pjContents = await JSON.parse(await fs.readFile(path.join(dir, 'package.json'), 'utf-8')); + const deps = new Set([ + ...Object.keys(pjContents.dependencies ?? {}), + ...Object.keys(pjContents.devDependencies ?? {}), + ]); + if (deps.has('typescript') || deps.has('ts-node') || deps.has('tsx') || deps.has('swc')) { + return 'typescript'; + } else { + return 'javascript'; + } + } + + if (files.has('requirements.txt') || files.has('setup.py') || files.has('pyproject.toml')) { + return 'python'; + } + + if (files.has('pom.xml') || files.has('build.xml') || files.has('settings.gradle')) { + return 'java'; + } + + if (Array.from(files).some(n => n.endsWith('.sln') || n.endsWith('.csproj') || n.endsWith('.fsproj') || n.endsWith('.vbproj'))) { + return 'dotnet'; + } + + if (files.has('go.mod')) { + return 'go'; + } + } catch { + // Swallow failure + } + return undefined; + + async function listFiles(dir: string, depth: number): Promise { + const ret = await fs.readdir(dir, { encoding: 'utf-8', withFileTypes: true }); + + return (await Promise.all(ret.map(async (f) => { + if (f.isDirectory()) { + if (depth <= 1) { + return Promise.resolve([]); + } + return await listFiles(path.join(dir, f.name), depth - 1); + } else { + return Promise.resolve([f.name]); + } + }))).flatMap(xs => xs); + } +} \ No newline at end of file diff --git a/packages/aws-cdk/lib/cxapp/cloud-executable.ts b/packages/aws-cdk/lib/cxapp/cloud-executable.ts index acb1c6390..c54a1c3c2 100644 --- a/packages/aws-cdk/lib/cxapp/cloud-executable.ts +++ b/packages/aws-cdk/lib/cxapp/cloud-executable.ts @@ -2,7 +2,7 @@ import type * as cxapi from '@aws-cdk/cloud-assembly-api'; import { ToolkitError } from '@aws-cdk/toolkit-lib'; import { CloudAssembly } from './cloud-assembly'; import type { ICloudAssemblySource, IReadableCloudAssembly } from '../../lib/api'; -import type { IoHelper } from '../../lib/api-private'; +import type { IMessageSpan, IoHelper } from '../../lib/api-private'; import { BorrowedAssembly } from '../../lib/api-private'; import type { SdkProvider } from '../api/aws-auth'; import { GLOBAL_PLUGIN_HOST } from '../cli/singleton-plugin-host'; @@ -110,26 +110,33 @@ export class CloudExecutable implements ICloudAssemblySource { previouslyMissingKeys = missingKeys; if (tryLookup) { - await this.props.ioHelper.defaults.debug('Some context information is missing. Fetching...'); - - const updates = await contextproviders.provideContextValues( - assembly.manifest.missing, - this.props.sdkProvider, - GLOBAL_PLUGIN_HOST, - this.props.ioHelper, - ); - - for (const [key, value] of Object.entries(updates)) { - this.props.configuration.context.set(key, value); + const lookupsTimer = synthSpan.startTimer('lookups'); + try { + await this.props.ioHelper.defaults.debug('Some context information is missing. Fetching...'); + + const updates = await contextproviders.provideContextValues( + assembly.manifest.missing, + this.props.sdkProvider, + GLOBAL_PLUGIN_HOST, + this.props.ioHelper, + ); + + for (const [key, value] of Object.entries(updates)) { + this.props.configuration.context.set(key, value); + } + + // Cache the new context to disk + await this.props.configuration.saveContext(); + } finally { + lookupsTimer.stop(); } - // Cache the new context to disk - await this.props.configuration.saveContext(); - // Execute again continue; } } + + countAssemblyResults(synthSpan, assembly); return new CloudAssembly(assembly, this.props.ioHelper); } } catch (e: any) { @@ -165,3 +172,12 @@ function setsEqual(a: Set, b: Set) { } return true; } + +function countAssemblyResults(span: IMessageSpan, asm: cxapi.CloudAssembly) { + span.incCounter('stacks', asm.stacksRecursively.length); + span.incCounter('assemblies', asmCount(asm)); + + function asmCount(x: cxapi.CloudAssembly): number { + return 1 + x.nestedAssemblies.reduce((acc, asm) => acc + asmCount(asm.assembly), 0); + } +} \ No newline at end of file From 063862fa850d345f60aebaf91a9623e3fa287527 Mon Sep 17 00:00:00 2001 From: Rico Huijbers Date: Mon, 9 Feb 2026 09:46:25 +0100 Subject: [PATCH 04/13] Guess agent --- .../toolkit-lib/lib/toolkit/toolkit.ts | 93 ++++++++----------- packages/aws-cdk/lib/cli/cdk-toolkit.ts | 1 - packages/aws-cdk/lib/cli/cli.ts | 4 +- packages/aws-cdk/lib/cli/telemetry/schema.ts | 1 + packages/aws-cdk/lib/cli/telemetry/session.ts | 7 ++ .../util/{detect-agent.ts => guess-agent.ts} | 0 6 files changed, 50 insertions(+), 56 deletions(-) rename packages/aws-cdk/lib/cli/util/{detect-agent.ts => guess-agent.ts} (100%) diff --git a/packages/@aws-cdk/toolkit-lib/lib/toolkit/toolkit.ts b/packages/@aws-cdk/toolkit-lib/lib/toolkit/toolkit.ts index 46e03106a..cb0d11dc1 100644 --- a/packages/@aws-cdk/toolkit-lib/lib/toolkit/toolkit.ts +++ b/packages/@aws-cdk/toolkit-lib/lib/toolkit/toolkit.ts @@ -60,7 +60,7 @@ import { import { sdkRequestHandler } from '../api/aws-auth/awscli-compatible'; import { IoHostSdkLogger, SdkProvider } from '../api/aws-auth/private'; import { Bootstrapper } from '../api/bootstrap'; -import type { ICloudAssemblySource, StackSelector } from '../api/cloud-assembly'; +import type { ICloudAssemblySource } from '../api/cloud-assembly'; import { CachedCloudAssembly, StackSelectionStrategy } from '../api/cloud-assembly'; import type { StackAssembly } from '../api/cloud-assembly/private'; import { ALL_STACKS } from '../api/cloud-assembly/private'; @@ -71,7 +71,7 @@ import { DiffFormatter } from '../api/diff'; import { detectStackDrift } from '../api/drift'; import { DriftFormatter } from '../api/drift/drift-formatter'; import type { IIoHost, IoMessageLevel, ToolkitAction } from '../api/io'; -import type { ElapsedTime, IoHelper } from '../api/io/private'; +import type { IoHelper } from '../api/io/private'; import { asIoHelper, IO, SPAN, withoutColor, withoutEmojis, withTrimmedWhitespace } from '../api/io/private'; import { CloudWatchLogEventMonitor, findCloudWatchLogGroups } from '../api/logs-monitor'; import { Mode, PluginHost } from '../api/plugin'; @@ -317,13 +317,16 @@ export class Toolkit extends CloudAssemblySourceBuilder { */ public async synth(cx: ICloudAssemblySource, options: SynthOptions = {}): Promise { const ioHelper = asIoHelper(this.ioHost, 'synth'); + const selectStacks = options.stacks ?? ALL_STACKS; + const synthSpan = await ioHelper.span(SPAN.SYNTH_ASSEMBLY).begin({ stacks: selectStacks }); // NOTE: NOT 'await using' because we return ownership to the caller - const assembly = await synthAndMeasure(ioHelper, cx, stacksOpt(options)); + const assembly = await assemblyFromSource(synthSpan.asHelper, cx); - const stacks = await assembly.selectStacksV2(stacksOpt(options)); + const stacks = await assembly.selectStacksV2(selectStacks); const autoValidateStacks = options.validateStacks ? [assembly.selectStacksForValidation()] : []; - await this.validateStacksMetadata(stacks.concat(...autoValidateStacks), ioHelper); + await this.validateStacksMetadata(stacks.concat(...autoValidateStacks), synthSpan.asHelper); + await synthSpan.end(); // if we have a single stack, print it to STDOUT const message = `Successfully synthesized to ${chalk.blue(path.resolve(stacks.assembly.directory))}`; @@ -361,10 +364,12 @@ export class Toolkit extends CloudAssemblySourceBuilder { */ public async diff(cx: ICloudAssemblySource, options: DiffOptions = {}): Promise<{ [name: string]: TemplateDiff }> { const ioHelper = asIoHelper(this.ioHost, 'diff'); - const selectStacks = stacksOpt(options); - await using assembly = await synthAndMeasure(ioHelper, cx, selectStacks); - + const selectStacks = options.stacks ?? ALL_STACKS; + const synthSpan = await ioHelper.span(SPAN.SYNTH_ASSEMBLY).begin({ stacks: selectStacks }); + await using assembly = await assemblyFromSource(synthSpan.asHelper, cx); const stacks = await assembly.selectStacksV2(selectStacks); + await synthSpan.end(); + const diffSpan = await ioHelper.span(SPAN.DIFF_STACK).begin({ stacks: selectStacks }); const deployments = await this.deploymentsForAction('diff'); @@ -417,10 +422,11 @@ export class Toolkit extends CloudAssemblySourceBuilder { */ public async drift(cx: ICloudAssemblySource, options: DriftOptions = {}): Promise<{ [name: string]: DriftResult }> { const ioHelper = asIoHelper(this.ioHost, 'drift'); - const selectStacks = stacksOpt(options); - await using assembly = await synthAndMeasure(ioHelper, cx, selectStacks); - + const selectStacks = options.stacks ?? ALL_STACKS; + const synthSpan = await ioHelper.span(SPAN.SYNTH_ASSEMBLY).begin({ stacks: selectStacks }); + await using assembly = await assemblyFromSource(synthSpan.asHelper, cx); const stacks = await assembly.selectStacksV2(selectStacks); + await synthSpan.end(); const driftSpan = await ioHelper.span(SPAN.DRIFT_APP).begin({ stacks: selectStacks }); const allDriftResults: { [name: string]: DriftResult } = {}; @@ -494,10 +500,12 @@ export class Toolkit extends CloudAssemblySourceBuilder { */ public async list(cx: ICloudAssemblySource, options: ListOptions = {}): Promise { const ioHelper = asIoHelper(this.ioHost, 'list'); - const selectStacks = stacksOpt(options); - await using assembly = await synthAndMeasure(ioHelper, cx, selectStacks); - + const selectStacks = options.stacks ?? ALL_STACKS; + const synthSpan = await ioHelper.span(SPAN.SYNTH_ASSEMBLY).begin({ stacks: selectStacks }); + await using assembly = await assemblyFromSource(ioHelper, cx); const stackCollection = await assembly.selectStacksV2(selectStacks); + await synthSpan.end(); + const stacks = stackCollection.withDependencies(); const message = stacks.map(s => s.id).join('\n'); @@ -512,19 +520,20 @@ export class Toolkit extends CloudAssemblySourceBuilder { */ public async deploy(cx: ICloudAssemblySource, options: DeployOptions = {}): Promise { const ioHelper = asIoHelper(this.ioHost, 'deploy'); - await using assembly = await synthAndMeasure(ioHelper, cx, stacksOpt(options)); - - return await this._deploy(assembly, 'deploy', assembly.synthDuration, options); + await using assembly = await assemblyFromSource(ioHelper, cx); + return await this._deploy(assembly, 'deploy', options); } /** * Helper to allow deploy being called as part of the watch action. */ - private async _deploy(assembly: StackAssembly, action: 'deploy' | 'watch', synthDuration: ElapsedTime, options: PrivateDeployOptions = {}): Promise { + private async _deploy(assembly: StackAssembly, action: 'deploy' | 'watch', options: PrivateDeployOptions = {}): Promise { const ioHelper = asIoHelper(this.ioHost, action); - const selectStacks = stacksOpt(options); + const selectStacks = options.stacks ?? ALL_STACKS; + const synthSpan = await ioHelper.span(SPAN.SYNTH_ASSEMBLY).begin({ stacks: selectStacks }); const stackCollection = await assembly.selectStacksV2(selectStacks); await this.validateStacksMetadata(stackCollection, ioHelper); + const synthDuration = await synthSpan.end(); const ret: DeployResult = { stacks: [], @@ -988,8 +997,7 @@ export class Toolkit extends CloudAssemblySourceBuilder { */ public async rollback(cx: ICloudAssemblySource, options: RollbackOptions = {}): Promise { const ioHelper = asIoHelper(this.ioHost, 'rollback'); - await using assembly = await synthAndMeasure(ioHelper, cx, stacksOpt(options)); - + await using assembly = await assemblyFromSource(ioHelper, cx); return await this._rollback(assembly, 'rollback', options); } @@ -997,11 +1005,12 @@ export class Toolkit extends CloudAssemblySourceBuilder { * Helper to allow rollback being called as part of the deploy or watch action. */ private async _rollback(assembly: StackAssembly, action: 'rollback' | 'deploy' | 'watch', options: RollbackOptions): Promise { - const selectStacks = stacksOpt(options); + const selectStacks = options.stacks ?? ALL_STACKS; const ioHelper = asIoHelper(this.ioHost, action); - + const synthSpan = await ioHelper.span(SPAN.SYNTH_ASSEMBLY).begin({ stacks: selectStacks }); const stacks = await assembly.selectStacksV2(selectStacks); await this.validateStacksMetadata(stacks, ioHelper); + await synthSpan.end(); const ret: RollbackResult = { stacks: [], @@ -1063,13 +1072,13 @@ export class Toolkit extends CloudAssemblySourceBuilder { this.requireUnstableFeature('refactor'); const ioHelper = asIoHelper(this.ioHost, 'refactor'); - await using assembly = await synthAndMeasure(ioHelper, cx, stacksOpt(options)); + await using assembly = await assemblyFromSource(ioHelper, cx); return await this._refactor(assembly, ioHelper, cx, options); } private async _refactor(assembly: StackAssembly, ioHelper: IoHelper, cx: ICloudAssemblySource, options: RefactorOptions = {}): Promise { const sdkProvider = await this.sdkProvider('refactor'); - const selectedStacks = await assembly.selectStacksV2(stacksOpt(options)); + const selectedStacks = await assembly.selectStacksV2(options.stacks ?? ALL_STACKS); const groups = await groupStacks(sdkProvider, selectedStacks.stackArtifacts, options.additionalStackNames ?? []); for (let { environment, localStacks, deployedStacks } of groups) { @@ -1227,7 +1236,7 @@ export class Toolkit extends CloudAssemblySourceBuilder { */ public async destroy(cx: ICloudAssemblySource, options: DestroyOptions = {}): Promise { const ioHelper = asIoHelper(this.ioHost, 'destroy'); - await using assembly = await synthAndMeasure(ioHelper, cx, stacksOpt(options)); + await using assembly = await assemblyFromSource(ioHelper, cx); return await this._destroy(assembly, 'destroy', options); } @@ -1235,10 +1244,12 @@ export class Toolkit extends CloudAssemblySourceBuilder { * Helper to allow destroy being called as part of the deploy action. */ private async _destroy(assembly: StackAssembly, action: 'deploy' | 'destroy', options: DestroyOptions): Promise { - const selectStacks = stacksOpt(options); + const selectStacks = options.stacks ?? ALL_STACKS; const ioHelper = asIoHelper(this.ioHost, action); + const synthSpan = await ioHelper.span(SPAN.SYNTH_ASSEMBLY).begin({ stacks: selectStacks }); // The stacks will have been ordered for deployment, so reverse them for deletion. const stacks = (await assembly.selectStacksV2(selectStacks)).reversed(); + await synthSpan.end(); const ret: DestroyResult = { stacks: [], @@ -1341,7 +1352,7 @@ export class Toolkit extends CloudAssemblySourceBuilder { }; try { - await this._deploy(assembly, 'watch', zeroTime(), deployOptions); + await this._deploy(assembly, 'watch', deployOptions); } catch { // just continue - deploy will show the error } @@ -1399,29 +1410,3 @@ export class Toolkit extends CloudAssemblySourceBuilder { } } -/** - * Centralize the default stack selection logic in a single place - */ -function stacksOpt(o: { stacks?: StackSelector }): StackSelector { - return o.stacks ?? ALL_STACKS; -} - -/** - * Perform synthesis and emit the time taken to a new span - */ -async function synthAndMeasure(ioHelper: IoHelper, cx: ICloudAssemblySource, selectStacks: StackSelector): Promise { - const synthSpan = await ioHelper.span(SPAN.SYNTH_ASSEMBLY).begin({ stacks: selectStacks }); - try { - const ret = await assemblyFromSource(synthSpan.asHelper, cx); - const synthDuration = await synthSpan.end({}); - return Object.assign(ret, { synthDuration }); - } catch (error: any) { - // End the span even if we had a failure - await synthSpan.end({ error }); - throw error; - } -} - -function zeroTime(): ElapsedTime { - return { asMs: 0, asSec: 0 }; -} \ No newline at end of file diff --git a/packages/aws-cdk/lib/cli/cdk-toolkit.ts b/packages/aws-cdk/lib/cli/cdk-toolkit.ts index ae155b088..afde16837 100644 --- a/packages/aws-cdk/lib/cli/cdk-toolkit.ts +++ b/packages/aws-cdk/lib/cli/cdk-toolkit.ts @@ -542,7 +542,6 @@ export class CdkToolkit { // There is already a startDeployTime constant, but that does not work with telemetry. // We should integrate the two in the future const deploySpan = await this.ioHost.asIoHelper().span(CLI_PRIVATE_SPAN.DEPLOY).begin({}); - let error: ErrorDetails | undefined; let elapsedDeployTime = 0; try { diff --git a/packages/aws-cdk/lib/cli/cli.ts b/packages/aws-cdk/lib/cli/cli.ts index b3b4e31ff..2c3c218db 100644 --- a/packages/aws-cdk/lib/cli/cli.ts +++ b/packages/aws-cdk/lib/cli/cli.ts @@ -37,6 +37,7 @@ import type { ErrorDetails } from './telemetry/schema'; import { isCI } from './util/ci'; import { isDeveloperBuildVersion, versionWithBuild, versionNumber } from './version'; import { getLanguageFromAlias } from '../commands/language'; +import { guessAgent } from './util/guess-agent'; import { guessLanguage } from './util/guess-language'; export async function exec(args: string[], synthesizer?: Synthesizer): Promise { @@ -108,12 +109,13 @@ export async function exec(args: string[], synthesizer?: Synthesizer): Promise Date: Mon, 9 Feb 2026 11:49:39 +0100 Subject: [PATCH 05/13] Version, resource count --- .../cloud-assembly/private/prepare-source.ts | 40 ++++++++++++------- packages/@aws-cdk/toolkit-lib/lib/api/tree.ts | 2 +- packages/aws-cdk/lib/cli/cdk-toolkit.ts | 4 +- packages/aws-cdk/lib/cli/cli.ts | 11 ++++- 4 files changed, 40 insertions(+), 17 deletions(-) diff --git a/packages/@aws-cdk/toolkit-lib/lib/api/cloud-assembly/private/prepare-source.ts b/packages/@aws-cdk/toolkit-lib/lib/api/cloud-assembly/private/prepare-source.ts index a4dcf1df7..2e0922665 100644 --- a/packages/@aws-cdk/toolkit-lib/lib/api/cloud-assembly/private/prepare-source.ts +++ b/packages/@aws-cdk/toolkit-lib/lib/api/cloud-assembly/private/prepare-source.ts @@ -261,10 +261,7 @@ export function writeContextToEnv(env: Env, context: Context, completeness: 'add * * @param assembly - the assembly to check */ -async function checkContextOverflowSupport(assembly: CloudAssembly, ioHelper: IoHelper): Promise { - const traceFn = (msg: string) => ioHelper.defaults.trace(msg); - const tree = await loadTree(assembly, traceFn); - +async function checkContextOverflowSupport(tree: ConstructTreeNode | undefined, ioHelper: IoHelper): Promise { // We're dealing with an old version of the framework here. It is unaware of the temporary // file, which means that it will ignore the context overflow. if (!frameworkSupportsContextOverflow(tree)) { @@ -273,19 +270,31 @@ async function checkContextOverflowSupport(assembly: CloudAssembly, ioHelper: Io } /** - * Checks if the framework supports context overflow + * Find the `aws-cdk-lib` library version by inspecting construct tree debug information */ -export function frameworkSupportsContextOverflow(tree: ConstructTreeNode | undefined): boolean { - return !some(tree, node => { +export function findConstructLibraryVersion(tree: ConstructTreeNode | undefined): string | undefined { + let ret: string | undefined = undefined; + + some(tree, node => { const fqn = node.constructInfo?.fqn; const version = node.constructInfo?.version; - return ( - fqn === 'aws-cdk-lib.App' // v2 app - && version !== '0.0.0' // ignore developer builds - && version != null && lte(version, '2.38.0') // last version not supporting large context - ) // v2 - || fqn === '@aws-cdk/core.App'; // v1 app => not supported + if (fqn?.startsWith('aws-cdk-lib.') || fqn?.startsWith('@aws-cdk/core.')) { + ret = version; + return true; + } + return false; }); + + return ret !== '0.0.0' ? ret : undefined; +} + +/** + * Checks if the framework supports context overflow + */ +export function frameworkSupportsContextOverflow(tree: ConstructTreeNode | undefined): boolean { + const version = findConstructLibraryVersion(tree); + + return !version || !lte(version, '2.38.0'); } /** @@ -299,7 +308,10 @@ export async function assemblyFromDirectory(assemblyDir: string, ioHelper: IoHel // We sort as we deploy topoSort: false, }); - await checkContextOverflowSupport(assembly, ioHelper); + + const tree = await loadTree(assembly, ioHelper.defaults.trace.bind(ioHelper.defaults)); + await checkContextOverflowSupport(tree, ioHelper); + return assembly; } catch (err: any) { if (err.message.includes(cxschema.VERSION_MISMATCH)) { diff --git a/packages/@aws-cdk/toolkit-lib/lib/api/tree.ts b/packages/@aws-cdk/toolkit-lib/lib/api/tree.ts index 13f7ffcaa..41e81f5ae 100644 --- a/packages/@aws-cdk/toolkit-lib/lib/api/tree.ts +++ b/packages/@aws-cdk/toolkit-lib/lib/api/tree.ts @@ -40,7 +40,7 @@ export async function loadTree(assembly: CloudAssembly, trace: (msg: string) => try { const outdir = assembly.directory; const fileName = assembly.tree()?.file; - return fileName ? fs.readJSONSync(path.join(outdir, fileName)).tree : ({} as ConstructTreeNode); + return fileName ? fs.readJSONSync(path.join(outdir, fileName)).tree : undefined; } catch (e) { await trace(`Failed to get tree.json file: ${e}. Proceeding with empty tree.`); return undefined; diff --git a/packages/aws-cdk/lib/cli/cdk-toolkit.ts b/packages/aws-cdk/lib/cli/cdk-toolkit.ts index afde16837..1eca8b6be 100644 --- a/packages/aws-cdk/lib/cli/cdk-toolkit.ts +++ b/packages/aws-cdk/lib/cli/cdk-toolkit.ts @@ -475,7 +475,8 @@ export class CdkToolkit { ); } - if (Object.keys(stack.template.Resources || {}).length === 0) { + const resourceCount = Object.keys(stack.template.Resources || {}).length; + if (resourceCount === 0) { // The generated stack has no resources if (!(await this.props.deployments.stackExists({ stack }))) { await this.ioHost.asIoHelper().defaults.warn('%s: stack has no resources, skipping deployment.', chalk.bold(stack.displayName)); @@ -542,6 +543,7 @@ export class CdkToolkit { // There is already a startDeployTime constant, but that does not work with telemetry. // We should integrate the two in the future const deploySpan = await this.ioHost.asIoHelper().span(CLI_PRIVATE_SPAN.DEPLOY).begin({}); + deploySpan.incCounter('resources', resourceCount); let error: ErrorDetails | undefined; let elapsedDeployTime = 0; try { diff --git a/packages/aws-cdk/lib/cli/cli.ts b/packages/aws-cdk/lib/cli/cli.ts index 2c3c218db..8660bb2f2 100644 --- a/packages/aws-cdk/lib/cli/cli.ts +++ b/packages/aws-cdk/lib/cli/cli.ts @@ -16,7 +16,7 @@ import type { Command } from './user-configuration'; import { Configuration } from './user-configuration'; import { asIoHelper } from '../../lib/api-private'; import type { IReadLock } from '../api'; -import { ToolkitInfo, Notices } from '../api'; +import { ToolkitInfo, Notices, loadTree, findConstructLibraryVersion } from '../api'; import { SdkProvider, IoHostSdkLogger, setSdkTracing, sdkRequestHandler } from '../api/aws-auth'; import type { BootstrapSource } from '../api/bootstrap'; import { Bootstrapper } from '../api/bootstrap'; @@ -191,6 +191,15 @@ export async function exec(args: string[], synthesizer?: Synthesizer): Promise Date: Mon, 9 Feb 2026 13:50:24 +0100 Subject: [PATCH 06/13] Update toolkit metrics --- .../toolkit-lib/lib/toolkit/toolkit.ts | 18 ++++++++++++++++-- 1 file changed, 16 insertions(+), 2 deletions(-) diff --git a/packages/@aws-cdk/toolkit-lib/lib/toolkit/toolkit.ts b/packages/@aws-cdk/toolkit-lib/lib/toolkit/toolkit.ts index c721f49c7..2892dce18 100644 --- a/packages/@aws-cdk/toolkit-lib/lib/toolkit/toolkit.ts +++ b/packages/@aws-cdk/toolkit-lib/lib/toolkit/toolkit.ts @@ -71,7 +71,7 @@ import { DiffFormatter } from '../api/diff'; import { detectStackDrift } from '../api/drift'; import { DriftFormatter } from '../api/drift/drift-formatter'; import type { IIoHost, IoMessageLevel, ToolkitAction } from '../api/io'; -import type { ElapsedTime, IoHelper } from '../api/io/private'; +import type { ElapsedTime, IMessageSpan, IoHelper } from '../api/io/private'; import { asIoHelper, IO, SPAN, withoutColor, withoutEmojis, withTrimmedWhitespace } from '../api/io/private'; import { CloudWatchLogEventMonitor, findCloudWatchLogGroups } from '../api/logs-monitor'; import { Mode, PluginHost } from '../api/plugin'; @@ -596,7 +596,8 @@ export class Toolkit extends CloudAssemblySourceBuilder { } // The generated stack has no resources - if (Object.keys(stack.template.Resources || {}).length === 0) { + const resourceCount = Object.keys(stack.template.Resources || {}).length; + if (resourceCount === 0) { // stack is empty and doesn't exist => do nothing const stackExists = await deployments.stackExists({ stack }); if (!stackExists) { @@ -661,6 +662,7 @@ export class Toolkit extends CloudAssemblySourceBuilder { current: stackIndex, stack, }); + deploySpan.incCounter('resources', resourceCount); let tags = options.tags; if (!tags || tags.length === 0) { @@ -1418,6 +1420,9 @@ async function synthAndMeasure( try { const ret = await assemblyFromSource(synthSpan.asHelper, cx); const synthDuration = await synthSpan.end({}); + + countAssemblyResults(synthSpan, ret.assembly); + return Object.assign(ret, { synthDuration }); } catch (error: any) { // End the span even if we had a failure @@ -1429,3 +1434,12 @@ async function synthAndMeasure( function zeroTime(): ElapsedTime { return { asMs: 0, asSec: 0 }; } + +function countAssemblyResults(span: IMessageSpan, asm: cxapi.CloudAssembly) { + span.incCounter('stacks', asm.stacksRecursively.length); + span.incCounter('assemblies', asmCount(asm)); + + function asmCount(x: cxapi.CloudAssembly): number { + return 1 + x.nestedAssemblies.reduce((acc, asm) => acc + asmCount(asm.assembly), 0); + } +} \ No newline at end of file From 01acd0d5e7ac899d328b01ed3f981e77dcf90a0a Mon Sep 17 00:00:00 2001 From: Rico Huijbers Date: Mon, 9 Feb 2026 14:03:53 +0100 Subject: [PATCH 07/13] Boop --- .../toolkit-lib/lib/toolkit/toolkit.ts | 3 +- .../api/cloud-assembly/prepare-source.test.ts | 32 ------------------- .../aws-cdk/lib/cxapp/cloud-executable.ts | 2 +- 3 files changed, 3 insertions(+), 34 deletions(-) diff --git a/packages/@aws-cdk/toolkit-lib/lib/toolkit/toolkit.ts b/packages/@aws-cdk/toolkit-lib/lib/toolkit/toolkit.ts index 2892dce18..2c8d3a7f6 100644 --- a/packages/@aws-cdk/toolkit-lib/lib/toolkit/toolkit.ts +++ b/packages/@aws-cdk/toolkit-lib/lib/toolkit/toolkit.ts @@ -1440,6 +1440,7 @@ function countAssemblyResults(span: IMessageSpan, asm: cxapi.CloudAssembly) span.incCounter('assemblies', asmCount(asm)); function asmCount(x: cxapi.CloudAssembly): number { - return 1 + x.nestedAssemblies.reduce((acc, asm) => acc + asmCount(asm.assembly), 0); + console.log(x.directory); + return 1 + x.nestedAssemblies.reduce((acc, asm) => acc + asmCount(asm.nestedAssembly), 0); } } \ No newline at end of file diff --git a/packages/@aws-cdk/toolkit-lib/test/api/cloud-assembly/prepare-source.test.ts b/packages/@aws-cdk/toolkit-lib/test/api/cloud-assembly/prepare-source.test.ts index 0a5cf2e1c..ab7730c43 100644 --- a/packages/@aws-cdk/toolkit-lib/test/api/cloud-assembly/prepare-source.test.ts +++ b/packages/@aws-cdk/toolkit-lib/test/api/cloud-assembly/prepare-source.test.ts @@ -100,38 +100,6 @@ describe('frameworkSupportsContextOverflow', () => { expect(frameworkSupportsContextOverflow(tree)).toBe(true); }); - test('returns false if any node in the tree is a v1 App', () => { - const tree: ConstructTreeNode = { - id: 'root', - path: '', - children: { - stack1: { - id: 'stack1', - path: 'stack1', - constructInfo: { - fqn: 'aws-cdk-lib.Stack', - version: '2.50.0', - }, - }, - nested: { - id: 'nested', - path: 'nested', - children: { - app: { - id: 'app', - path: 'nested/app', - constructInfo: { - fqn: '@aws-cdk/core.App', - version: '1.180.0', - }, - }, - }, - }, - }, - }; - expect(frameworkSupportsContextOverflow(tree)).toBe(false); - }); - test('returns false if any node in the tree is a v2 App with version <= 2.38.0', () => { const tree: ConstructTreeNode = { id: 'root', diff --git a/packages/aws-cdk/lib/cxapp/cloud-executable.ts b/packages/aws-cdk/lib/cxapp/cloud-executable.ts index c54a1c3c2..fd9aba0c4 100644 --- a/packages/aws-cdk/lib/cxapp/cloud-executable.ts +++ b/packages/aws-cdk/lib/cxapp/cloud-executable.ts @@ -178,6 +178,6 @@ function countAssemblyResults(span: IMessageSpan, asm: cxapi.CloudAssembly) span.incCounter('assemblies', asmCount(asm)); function asmCount(x: cxapi.CloudAssembly): number { - return 1 + x.nestedAssemblies.reduce((acc, asm) => acc + asmCount(asm.assembly), 0); + return 1 + x.nestedAssemblies.reduce((acc, asm) => acc + asmCount(asm.nestedAssembly), 0); } } \ No newline at end of file From be68914b7206895bd4b86e0bde2f5704a50a3d71 Mon Sep 17 00:00:00 2001 From: Rico Huijbers Date: Mon, 9 Feb 2026 14:14:53 +0100 Subject: [PATCH 08/13] These tests don't make sense --- .../api/cloud-assembly/prepare-source.test.ts | 30 ++++++++++++++----- 1 file changed, 23 insertions(+), 7 deletions(-) diff --git a/packages/@aws-cdk/toolkit-lib/test/api/cloud-assembly/prepare-source.test.ts b/packages/@aws-cdk/toolkit-lib/test/api/cloud-assembly/prepare-source.test.ts index ab7730c43..df3276787 100644 --- a/packages/@aws-cdk/toolkit-lib/test/api/cloud-assembly/prepare-source.test.ts +++ b/packages/@aws-cdk/toolkit-lib/test/api/cloud-assembly/prepare-source.test.ts @@ -100,19 +100,35 @@ describe('frameworkSupportsContextOverflow', () => { expect(frameworkSupportsContextOverflow(tree)).toBe(true); }); - test('returns false if any node in the tree is a v2 App with version <= 2.38.0', () => { + test('returns false if @aws-cdk/core constructs in the tree indicate v1 App', () => { const tree: ConstructTreeNode = { id: 'root', path: '', children: { - stack1: { - id: 'stack1', - path: 'stack1', - constructInfo: { - fqn: 'aws-cdk-lib.Stack', - version: '2.50.0', + nested: { + id: 'nested', + path: 'nested', + children: { + app: { + id: 'app', + path: 'nested/app', + constructInfo: { + fqn: '@aws-cdk/core.App', + version: '1.180.0', + }, + }, }, }, + }, + }; + expect(frameworkSupportsContextOverflow(tree)).toBe(false); + }); + + test('returns false if aws-cdk-lib constructs v2 App with version <= 2.38.0', () => { + const tree: ConstructTreeNode = { + id: 'root', + path: '', + children: { nested: { id: 'nested', path: 'nested', From 91e34b219db642b12affd31e3b2064c18f2ea7d6 Mon Sep 17 00:00:00 2001 From: Rico Huijbers Date: Mon, 9 Feb 2026 14:32:34 +0100 Subject: [PATCH 09/13] Some tests --- packages/aws-cdk/lib/cli/telemetry/session.ts | 15 ++++++ .../test/cli/io-host/cli-io-host.test.ts | 23 +++++++++ .../test/cli/telemetry/session.test.ts | 48 +++++++++++++++++++ 3 files changed, 86 insertions(+) diff --git a/packages/aws-cdk/lib/cli/telemetry/session.ts b/packages/aws-cdk/lib/cli/telemetry/session.ts index 66aea8daf..feace549b 100644 --- a/packages/aws-cdk/lib/cli/telemetry/session.ts +++ b/packages/aws-cdk/lib/cli/telemetry/session.ts @@ -117,6 +117,11 @@ export class TelemetrySession { * Attach a language guess */ public attachLanguage(language: string | undefined) { + // Don't want to crash accidentally + if (!this._sessionInfo) { + return; + } + if (language) { mutable(this.sessionInfo.project).language = language; } @@ -126,6 +131,11 @@ export class TelemetrySession { * Attach our best guess at running under an agent or not */ public attachAgent(isAgent: boolean | undefined) { + // Don't want to crash accidentally + if (!this._sessionInfo) { + return; + } + mutable(this.sessionInfo.environment).agent = isAgent; } @@ -141,6 +151,11 @@ export class TelemetrySession { * information becomes available that we can add in. */ public attachCdkLibVersion(libVersion: string) { + // Don't want to crash accidentally + if (!this._sessionInfo) { + return; + } + mutable(this.sessionInfo.identifiers).cdkLibraryVersion = libVersion; } diff --git a/packages/aws-cdk/test/cli/io-host/cli-io-host.test.ts b/packages/aws-cdk/test/cli/io-host/cli-io-host.test.ts index 2305a94bb..e71850f31 100644 --- a/packages/aws-cdk/test/cli/io-host/cli-io-host.test.ts +++ b/packages/aws-cdk/test/cli/io-host/cli-io-host.test.ts @@ -7,6 +7,7 @@ import * as fs from 'fs-extra'; import { Context } from '../../../lib/api/context'; import type { IoMessage, IoMessageLevel, IoRequest } from '../../../lib/cli/io-host'; import { CliIoHost } from '../../../lib/cli/io-host'; +import { CLI_PRIVATE_IO } from '../../../lib/cli/telemetry/messages'; let passThrough: PassThrough; @@ -382,6 +383,28 @@ describe('CliIoHost', () => { expect(telemetryEmitSpy).not.toHaveBeenCalled(); }); + test('emit telemetry with counters', async () => { + // Create a message that should trigger telemetry using the actual message code + const message = { + ...CLI_PRIVATE_IO.CDK_CLI_I1001.msg('telemetry message', { + duration: 123, + counters: { + tests: 15, + }, + }), + action: 'synth' as const, + }; + + // Send the notification + await telemetryIoHost.notify(message); + + // Verify that the emit method was called with the correct parameters + expect(telemetryEmitSpy).toHaveBeenCalledWith(expect.objectContaining({ + eventType: 'SYNTH', + counters: { tests: 15 }, + })); + }); + test('emit telemetry with error name', async () => { // Create a message that should trigger telemetry using the actual message code const message: IoMessage = { diff --git a/packages/aws-cdk/test/cli/telemetry/session.test.ts b/packages/aws-cdk/test/cli/telemetry/session.test.ts index 009b504f7..5343161ff 100644 --- a/packages/aws-cdk/test/cli/telemetry/session.test.ts +++ b/packages/aws-cdk/test/cli/telemetry/session.test.ts @@ -137,6 +137,54 @@ describe('TelemetrySession', () => { // THEN expect(clientFlushSpy).toHaveBeenCalledTimes(1); }); + + test('attach cdk library version', async () => { + session.attachCdkLibVersion('1.2.3'); + + await session.emit({ + eventType: 'SYNTH', + duration: 1234, + }); + + // THEN + expect(clientEmitSpy).toHaveBeenCalledWith(expect.objectContaining({ + identifiers: expect.objectContaining({ + cdkLibraryVersion: '1.2.3', + }), + })); + }); + + test('attach language', async () => { + session.attachLanguage('basic'); + + await session.emit({ + eventType: 'SYNTH', + duration: 1234, + }); + + // THEN + expect(clientEmitSpy).toHaveBeenCalledWith(expect.objectContaining({ + project: expect.objectContaining({ + language: 'basic', + }), + })); + }); + + test('attack agent', async () => { + session.attachAgent(true); + + await session.emit({ + eventType: 'SYNTH', + duration: 1234, + }); + + // THEN + expect(clientEmitSpy).toHaveBeenCalledWith(expect.objectContaining({ + environment: expect.objectContaining({ + agent: true, + }), + })); + }); }); test('ci is recorded properly - true', async () => { From 2203e37908b17ead762f9b87318e090904d1a2a1 Mon Sep 17 00:00:00 2001 From: Rico Huijbers Date: Mon, 9 Feb 2026 15:12:06 +0100 Subject: [PATCH 10/13] Tests --- .../toolkit-lib/lib/toolkit/toolkit.ts | 3 +- .../toolkit-lib/test/actions/deploy.test.ts | 16 +++ .../toolkit-lib/test/actions/synth.test.ts | 16 +++ packages/aws-cdk/test/cli/cdk-toolkit.test.ts | 21 ++++ .../test/cxapp/cloud-executable.test.ts | 111 ++++++++++++------ 5 files changed, 132 insertions(+), 35 deletions(-) diff --git a/packages/@aws-cdk/toolkit-lib/lib/toolkit/toolkit.ts b/packages/@aws-cdk/toolkit-lib/lib/toolkit/toolkit.ts index 2c8d3a7f6..c70f00911 100644 --- a/packages/@aws-cdk/toolkit-lib/lib/toolkit/toolkit.ts +++ b/packages/@aws-cdk/toolkit-lib/lib/toolkit/toolkit.ts @@ -1419,9 +1419,8 @@ async function synthAndMeasure( const synthSpan = await ioHelper.span(SPAN.SYNTH_ASSEMBLY).begin({ stacks: selectStacks }); try { const ret = await assemblyFromSource(synthSpan.asHelper, cx); - const synthDuration = await synthSpan.end({}); - countAssemblyResults(synthSpan, ret.assembly); + const synthDuration = await synthSpan.end({}); return Object.assign(ret, { synthDuration }); } catch (error: any) { diff --git a/packages/@aws-cdk/toolkit-lib/test/actions/deploy.test.ts b/packages/@aws-cdk/toolkit-lib/test/actions/deploy.test.ts index ab2a6c76b..3ecbe3fef 100644 --- a/packages/@aws-cdk/toolkit-lib/test/actions/deploy.test.ts +++ b/packages/@aws-cdk/toolkit-lib/test/actions/deploy.test.ts @@ -41,6 +41,22 @@ describe('deploy', () => { successfulDeployment(); }); + test('emits resource counters', async () => { + // WHEN + const cx = await builderFixture(toolkit, 'two-empty-stacks'); + await toolkit.deploy(cx); + + // THEN + const deploy = ioHost.notifySpy.mock.calls.map(cs => cs[0]).filter(c => c.code === 'CDK_TOOLKIT_I5001'); + expect(deploy).toContainEqual(expect.objectContaining({ + data: expect.objectContaining({ + counters: expect.objectContaining({ + resources: 1, + }), + }), + })); + }); + test('request response contains security diff', async () => { // WHEN const cx = await builderFixture(toolkit, 'stack-with-role'); diff --git a/packages/@aws-cdk/toolkit-lib/test/actions/synth.test.ts b/packages/@aws-cdk/toolkit-lib/test/actions/synth.test.ts index 5ee8cfe6c..70aef1fc7 100644 --- a/packages/@aws-cdk/toolkit-lib/test/actions/synth.test.ts +++ b/packages/@aws-cdk/toolkit-lib/test/actions/synth.test.ts @@ -23,6 +23,22 @@ describe('synth', () => { })); }); + test('emits stack counters', async () => { + // WHEN + const cx = await builderFixture(toolkit, 'two-empty-stacks'); + await toolkit.synth(cx); + + // Separate tests as colorizing hampers detection + expect(ioHost.notifySpy).toHaveBeenCalledWith(expect.objectContaining({ + data: expect.objectContaining({ + counters: expect.objectContaining({ + assemblies: 1, + stacks: 2, + }), + }), + })); + }); + test('synth from app', async () => { // WHEN const cx = await appFixture(toolkit, 'two-empty-stacks'); diff --git a/packages/aws-cdk/test/cli/cdk-toolkit.test.ts b/packages/aws-cdk/test/cli/cdk-toolkit.test.ts index f43942cea..7ade34186 100644 --- a/packages/aws-cdk/test/cli/cdk-toolkit.test.ts +++ b/packages/aws-cdk/test/cli/cdk-toolkit.test.ts @@ -571,6 +571,27 @@ describe('deploy', () => { }); }); + test('emits resource counters', async () => { + // GIVEN + const toolkit = defaultToolkitSetup(); + + // WHEN + await toolkit.deploy({ + selector: { patterns: ['Test-Stack-B'] }, + deploymentMethod: { method: 'change-set' }, + }); + + // THEN + const deploy = notifySpy.mock.calls.map(cs => cs[0]).filter(c => c.code === 'CDK_CLI_I3001'); + expect(deploy).toContainEqual(expect.objectContaining({ + data: expect.objectContaining({ + counters: expect.objectContaining({ + resources: 1, + }), + }), + })); + }); + test('globless bootstrap uses environment without question', async () => { // GIVEN const toolkit = defaultToolkitSetup(); diff --git a/packages/aws-cdk/test/cxapp/cloud-executable.test.ts b/packages/aws-cdk/test/cxapp/cloud-executable.test.ts index ff1323065..8a13f128a 100644 --- a/packages/aws-cdk/test/cxapp/cloud-executable.test.ts +++ b/packages/aws-cdk/test/cxapp/cloud-executable.test.ts @@ -3,6 +3,13 @@ import * as cxschema from '@aws-cdk/cloud-assembly-schema'; import { DefaultSelection } from '../../lib/cxapp/cloud-assembly'; import { registerContextProvider } from '../../lib/context-providers'; import { MockCloudExecutable } from '../_helpers/assembly'; +import { TestIoHost } from '../_helpers/io-host'; + +let ioHost: TestIoHost; +beforeEach(() => { + jest.restoreAllMocks(); + ioHost = new TestIoHost('trace'); +}); describe('AWS::CDK::Metadata', () => { test('is not generated for new frameworks', async () => { @@ -27,13 +34,19 @@ test('stop executing if context providers are not making progress', async () => }); const cloudExecutable = await MockCloudExecutable.create({ - stacks: [{ - stackName: 'thestack', - template: { resource: 'noerrorresource' }, - }], + stacks: [ + { + stackName: 'thestack', + template: { resource: 'noerrorresource' }, + }, + ], // Always return the same missing keys, synthesis should still finish. missing: [ - { key: 'abcdef', props: { account: '1324', region: 'us-east-1' }, provider: cxschema.ContextProvider.AVAILABILITY_ZONE_PROVIDER }, + { + key: 'abcdef', + props: { account: '1324', region: 'us-east-1' }, + provider: cxschema.ContextProvider.AVAILABILITY_ZONE_PROVIDER, + }, ], }); const cxasm = await cloudExecutable.synthesize(); @@ -47,13 +60,19 @@ test('stop executing if context providers are not making progress', async () => test('fails if lookups are disabled and missing context is synthesized', async () => { // GIVEN const cloudExecutable = await MockCloudExecutable.create({ - stacks: [{ - stackName: 'thestack', - template: { resource: 'noerrorresource' }, - }], + stacks: [ + { + stackName: 'thestack', + template: { resource: 'noerrorresource' }, + }, + ], // Always return the same missing keys, synthesis should still finish. missing: [ - { key: 'abcdef', props: { account: '1324', region: 'us-east-1' }, provider: cxschema.ContextProvider.AVAILABILITY_ZONE_PROVIDER }, + { + key: 'abcdef', + props: { account: '1324', region: 'us-east-1' }, + provider: cxschema.ContextProvider.AVAILABILITY_ZONE_PROVIDER, + }, ], }); cloudExecutable.configuration.settings.set(['lookups'], false); @@ -62,31 +81,57 @@ test('fails if lookups are disabled and missing context is synthesized', async ( await expect(cloudExecutable.synthesize()).rejects.toThrow(/Context lookups have been disabled/); }); -async function testCloudExecutable( - { env, versionReporting = true, schemaVersion }: - { env?: string; versionReporting?: boolean; schemaVersion?: string } = {}, -) { - const cloudExec = await MockCloudExecutable.create({ - stacks: [{ - stackName: 'withouterrors', - env, - template: { resource: 'noerrorresource' }, - }, +test('emits stack counters', async () => { + const cx = await testCloudExecutable({ + env: 'aws://012345678912/us-east-1', + versionReporting: true, + schemaVersion: '8.0.0', + }); + await cx.synthesize(); + + // Separate tests as colorizing hampers detection + expect(ioHost.notifySpy).toHaveBeenCalledWith(expect.objectContaining({ + data: expect.objectContaining({ + counters: expect.objectContaining({ + assemblies: 1, + stacks: 2, + }), + }), + })); +}); + +async function testCloudExecutable({ + env, + versionReporting = true, + schemaVersion, +}: { env?: string; versionReporting?: boolean; schemaVersion?: string } = {}) { + const cloudExec = await MockCloudExecutable.create( { - stackName: 'witherrors', - env, - template: { resource: 'errorresource' }, - metadata: { - '/resource': [ - { - type: cxschema.ArtifactMetadataEntryType.ERROR, - data: 'this is an error', + stacks: [ + { + stackName: 'withouterrors', + env, + template: { resource: 'noerrorresource' }, + }, + { + stackName: 'witherrors', + env, + template: { resource: 'errorresource' }, + metadata: { + '/resource': [ + { + type: cxschema.ArtifactMetadataEntryType.ERROR, + data: 'this is an error', + }, + ], }, - ], - }, - }], - schemaVersion, - }); + }, + ], + schemaVersion, + }, + undefined, + ioHost, + ); cloudExec.configuration.settings.set(['versionReporting'], versionReporting); return cloudExec; From d2994a07202fdbaa094a4ad6702c9184377e1ffc Mon Sep 17 00:00:00 2001 From: Rico Huijbers Date: Mon, 9 Feb 2026 16:14:44 +0100 Subject: [PATCH 11/13] Errors and warnings --- packages/@aws-cdk/toolkit-lib/lib/toolkit/toolkit.ts | 10 +++++++++- packages/aws-cdk/lib/cxapp/cloud-executable.ts | 12 +++++++++++- 2 files changed, 20 insertions(+), 2 deletions(-) diff --git a/packages/@aws-cdk/toolkit-lib/lib/toolkit/toolkit.ts b/packages/@aws-cdk/toolkit-lib/lib/toolkit/toolkit.ts index c70f00911..7ef6d2046 100644 --- a/packages/@aws-cdk/toolkit-lib/lib/toolkit/toolkit.ts +++ b/packages/@aws-cdk/toolkit-lib/lib/toolkit/toolkit.ts @@ -95,6 +95,7 @@ import { PermissionChangeType } from '../payloads'; import { formatErrorMessage, formatTime, obscureTemplate, serializeStructure, validateSnsTopicArn } from '../util'; import { pLimit } from '../util/concurrency'; import { promiseWithResolvers } from '../util/promises'; +import { SynthesisMessageLevel } from '@aws-cdk/cloud-assembly-api'; export interface ToolkitOptions { /** @@ -1435,11 +1436,18 @@ function zeroTime(): ElapsedTime { } function countAssemblyResults(span: IMessageSpan, asm: cxapi.CloudAssembly) { - span.incCounter('stacks', asm.stacksRecursively.length); + const stacksRecursively = asm.stacksRecursively; + span.incCounter('stacks', stacksRecursively.length); span.incCounter('assemblies', asmCount(asm)); + span.incCounter('errorAnns', sum(stacksRecursively.map(s => s.messages.filter(m => m.level === SynthesisMessageLevel.ERROR).length))); + span.incCounter('warnings', sum(stacksRecursively.map(s => s.messages.filter(m => m.level === SynthesisMessageLevel.WARNING).length))); function asmCount(x: cxapi.CloudAssembly): number { console.log(x.directory); return 1 + x.nestedAssemblies.reduce((acc, asm) => acc + asmCount(asm.nestedAssembly), 0); } +} + +function sum(xs: number[]) { + return xs.reduce((a, b) => a + b, 0); } \ No newline at end of file diff --git a/packages/aws-cdk/lib/cxapp/cloud-executable.ts b/packages/aws-cdk/lib/cxapp/cloud-executable.ts index fd9aba0c4..64ca1242a 100644 --- a/packages/aws-cdk/lib/cxapp/cloud-executable.ts +++ b/packages/aws-cdk/lib/cxapp/cloud-executable.ts @@ -11,6 +11,7 @@ import { CLI_PRIVATE_SPAN } from '../cli/telemetry/messages'; import type { ErrorDetails } from '../cli/telemetry/schema'; import type { Configuration } from '../cli/user-configuration'; import * as contextproviders from '../context-providers'; +import { SynthesisMessageLevel } from '@aws-cdk/cloud-assembly-api'; /** * @returns output directory @@ -174,10 +175,19 @@ function setsEqual(a: Set, b: Set) { } function countAssemblyResults(span: IMessageSpan, asm: cxapi.CloudAssembly) { - span.incCounter('stacks', asm.stacksRecursively.length); + const stacksRecursively = asm.stacksRecursively; + + span.incCounter('stacks', stacksRecursively.length); + span.incCounter('errorAnns', sum(stacksRecursively.map(s => s.messages.filter(m => m.level === SynthesisMessageLevel.ERROR).length))); + span.incCounter('warnings', sum(stacksRecursively.map(s => s.messages.filter(m => m.level === SynthesisMessageLevel.WARNING).length))); + span.incCounter('assemblies', asmCount(asm)); function asmCount(x: cxapi.CloudAssembly): number { return 1 + x.nestedAssemblies.reduce((acc, asm) => acc + asmCount(asm.nestedAssembly), 0); } +} + +function sum(xs: number[]) { + return xs.reduce((a, b) => a + b, 0); } \ No newline at end of file From 99ba6e48382ec887e45ed68ded29ae8566b3ef59 Mon Sep 17 00:00:00 2001 From: Rico Huijbers Date: Mon, 9 Feb 2026 16:45:27 +0100 Subject: [PATCH 12/13] linter issues --- packages/@aws-cdk/toolkit-lib/lib/toolkit/toolkit.ts | 9 ++++----- packages/aws-cdk/lib/cxapp/cloud-executable.ts | 6 +++--- 2 files changed, 7 insertions(+), 8 deletions(-) diff --git a/packages/@aws-cdk/toolkit-lib/lib/toolkit/toolkit.ts b/packages/@aws-cdk/toolkit-lib/lib/toolkit/toolkit.ts index 7ef6d2046..810f5f50e 100644 --- a/packages/@aws-cdk/toolkit-lib/lib/toolkit/toolkit.ts +++ b/packages/@aws-cdk/toolkit-lib/lib/toolkit/toolkit.ts @@ -1,6 +1,7 @@ import '../private/dispose-polyfill'; import * as path from 'node:path'; import * as cxapi from '@aws-cdk/cloud-assembly-api'; +import { SynthesisMessageLevel } from '@aws-cdk/cloud-assembly-api'; import type { FeatureFlagReportProperties } from '@aws-cdk/cloud-assembly-schema'; import { ArtifactType } from '@aws-cdk/cloud-assembly-schema'; import type { TemplateDiff } from '@aws-cdk/cloudformation-diff'; @@ -95,7 +96,6 @@ import { PermissionChangeType } from '../payloads'; import { formatErrorMessage, formatTime, obscureTemplate, serializeStructure, validateSnsTopicArn } from '../util'; import { pLimit } from '../util/concurrency'; import { promiseWithResolvers } from '../util/promises'; -import { SynthesisMessageLevel } from '@aws-cdk/cloud-assembly-api'; export interface ToolkitOptions { /** @@ -1435,15 +1435,14 @@ function zeroTime(): ElapsedTime { return { asMs: 0, asSec: 0 }; } -function countAssemblyResults(span: IMessageSpan, asm: cxapi.CloudAssembly) { - const stacksRecursively = asm.stacksRecursively; +function countAssemblyResults(span: IMessageSpan, assembly: cxapi.CloudAssembly) { + const stacksRecursively = assembly.stacksRecursively; span.incCounter('stacks', stacksRecursively.length); - span.incCounter('assemblies', asmCount(asm)); + span.incCounter('assemblies', asmCount(assembly)); span.incCounter('errorAnns', sum(stacksRecursively.map(s => s.messages.filter(m => m.level === SynthesisMessageLevel.ERROR).length))); span.incCounter('warnings', sum(stacksRecursively.map(s => s.messages.filter(m => m.level === SynthesisMessageLevel.WARNING).length))); function asmCount(x: cxapi.CloudAssembly): number { - console.log(x.directory); return 1 + x.nestedAssemblies.reduce((acc, asm) => acc + asmCount(asm.nestedAssembly), 0); } } diff --git a/packages/aws-cdk/lib/cxapp/cloud-executable.ts b/packages/aws-cdk/lib/cxapp/cloud-executable.ts index 64ca1242a..40a1ead30 100644 --- a/packages/aws-cdk/lib/cxapp/cloud-executable.ts +++ b/packages/aws-cdk/lib/cxapp/cloud-executable.ts @@ -174,14 +174,14 @@ function setsEqual(a: Set, b: Set) { return true; } -function countAssemblyResults(span: IMessageSpan, asm: cxapi.CloudAssembly) { - const stacksRecursively = asm.stacksRecursively; +function countAssemblyResults(span: IMessageSpan, assembly: cxapi.CloudAssembly) { + const stacksRecursively = assembly.stacksRecursively; span.incCounter('stacks', stacksRecursively.length); span.incCounter('errorAnns', sum(stacksRecursively.map(s => s.messages.filter(m => m.level === SynthesisMessageLevel.ERROR).length))); span.incCounter('warnings', sum(stacksRecursively.map(s => s.messages.filter(m => m.level === SynthesisMessageLevel.WARNING).length))); - span.incCounter('assemblies', asmCount(asm)); + span.incCounter('assemblies', asmCount(assembly)); function asmCount(x: cxapi.CloudAssembly): number { return 1 + x.nestedAssemblies.reduce((acc, asm) => acc + asmCount(asm.nestedAssembly), 0); From a12cc87d99fd3011caf1856e58486ce49c897eba Mon Sep 17 00:00:00 2001 From: Rico Huijbers Date: Tue, 10 Feb 2026 10:17:09 +0100 Subject: [PATCH 13/13] Lintlint --- .../@aws-cdk/toolkit-lib/lib/api/io/private/span.ts | 1 - packages/@aws-cdk/toolkit-lib/lib/toolkit/toolkit.ts | 2 +- packages/aws-cdk/lib/cli/telemetry/session.ts | 2 +- packages/aws-cdk/lib/cli/util/guess-agent.ts | 2 +- packages/aws-cdk/lib/cli/util/guess-language.ts | 11 ++++++----- packages/aws-cdk/lib/cxapp/cloud-executable.ts | 4 ++-- 6 files changed, 11 insertions(+), 11 deletions(-) diff --git a/packages/@aws-cdk/toolkit-lib/lib/api/io/private/span.ts b/packages/@aws-cdk/toolkit-lib/lib/api/io/private/span.ts index 03e7895cd..b8a22b2e4 100644 --- a/packages/@aws-cdk/toolkit-lib/lib/api/io/private/span.ts +++ b/packages/@aws-cdk/toolkit-lib/lib/api/io/private/span.ts @@ -27,7 +27,6 @@ export interface SpanDefinition { readonly end: make.IoMessageMaker; } - /** * Arguments to the span.end() function * diff --git a/packages/@aws-cdk/toolkit-lib/lib/toolkit/toolkit.ts b/packages/@aws-cdk/toolkit-lib/lib/toolkit/toolkit.ts index 810f5f50e..2468ab691 100644 --- a/packages/@aws-cdk/toolkit-lib/lib/toolkit/toolkit.ts +++ b/packages/@aws-cdk/toolkit-lib/lib/toolkit/toolkit.ts @@ -1449,4 +1449,4 @@ function countAssemblyResults(span: IMessageSpan, assembly: cxapi.CloudAsse function sum(xs: number[]) { return xs.reduce((a, b) => a + b, 0); -} \ No newline at end of file +} diff --git a/packages/aws-cdk/lib/cli/telemetry/session.ts b/packages/aws-cdk/lib/cli/telemetry/session.ts index feace549b..cf18273a3 100644 --- a/packages/aws-cdk/lib/cli/telemetry/session.ts +++ b/packages/aws-cdk/lib/cli/telemetry/session.ts @@ -222,4 +222,4 @@ function isAbortedError(error?: ErrorDetails) { function mutable(x: A): { -readonly [k in keyof A]: A[k] } { return x; -} \ No newline at end of file +} diff --git a/packages/aws-cdk/lib/cli/util/guess-agent.ts b/packages/aws-cdk/lib/cli/util/guess-agent.ts index dd610fd80..f87e8f29a 100644 --- a/packages/aws-cdk/lib/cli/util/guess-agent.ts +++ b/packages/aws-cdk/lib/cli/util/guess-agent.ts @@ -31,4 +31,4 @@ export function guessAgent(): true | undefined { // Copilot doesn't set an envvar (at least not in VS Code) return undefined; -} \ No newline at end of file +} diff --git a/packages/aws-cdk/lib/cli/util/guess-language.ts b/packages/aws-cdk/lib/cli/util/guess-language.ts index ad49fc5be..0bd0a18e6 100644 --- a/packages/aws-cdk/lib/cli/util/guess-language.ts +++ b/packages/aws-cdk/lib/cli/util/guess-language.ts @@ -1,5 +1,5 @@ -import * as fs from 'fs-extra'; import * as path from 'path'; +import * as fs from 'fs-extra'; /** * Guess the CDK app language based on the files in the given directory @@ -43,18 +43,19 @@ export async function guessLanguage(dir: string): Promise { } return undefined; - async function listFiles(dir: string, depth: number): Promise { - const ret = await fs.readdir(dir, { encoding: 'utf-8', withFileTypes: true }); + async function listFiles(dirName: string, depth: number): Promise { + const ret = await fs.readdir(dirName, { encoding: 'utf-8', withFileTypes: true }); + // eslint-disable-next-line @cdklabs/promiseall-no-unbounded-parallelism return (await Promise.all(ret.map(async (f) => { if (f.isDirectory()) { if (depth <= 1) { return Promise.resolve([]); } - return await listFiles(path.join(dir, f.name), depth - 1); + return listFiles(path.join(dirName, f.name), depth - 1); } else { return Promise.resolve([f.name]); } }))).flatMap(xs => xs); } -} \ No newline at end of file +} diff --git a/packages/aws-cdk/lib/cxapp/cloud-executable.ts b/packages/aws-cdk/lib/cxapp/cloud-executable.ts index 40a1ead30..6c692914f 100644 --- a/packages/aws-cdk/lib/cxapp/cloud-executable.ts +++ b/packages/aws-cdk/lib/cxapp/cloud-executable.ts @@ -1,4 +1,5 @@ import type * as cxapi from '@aws-cdk/cloud-assembly-api'; +import { SynthesisMessageLevel } from '@aws-cdk/cloud-assembly-api'; import { ToolkitError } from '@aws-cdk/toolkit-lib'; import { CloudAssembly } from './cloud-assembly'; import type { ICloudAssemblySource, IReadableCloudAssembly } from '../../lib/api'; @@ -11,7 +12,6 @@ import { CLI_PRIVATE_SPAN } from '../cli/telemetry/messages'; import type { ErrorDetails } from '../cli/telemetry/schema'; import type { Configuration } from '../cli/user-configuration'; import * as contextproviders from '../context-providers'; -import { SynthesisMessageLevel } from '@aws-cdk/cloud-assembly-api'; /** * @returns output directory @@ -190,4 +190,4 @@ function countAssemblyResults(span: IMessageSpan, assembly: cxapi.CloudAsse function sum(xs: number[]) { return xs.reduce((a, b) => a + b, 0); -} \ No newline at end of file +}