Skip to content

Commit df2fa61

Browse files
committed
feat: instrument telemetry for deploy command (CLI + TUI)
1 parent bceddde commit df2fa61

6 files changed

Lines changed: 182 additions & 44 deletions

File tree

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
import type { AgentCoreProjectSpec } from '../../../../schema';
2+
import { computeDeployAttrs } from '../utils.js';
3+
import { describe, expect, it } from 'vitest';
4+
5+
describe('computeDeployAttrs', () => {
6+
it('computes counts from a populated spec', () => {
7+
const projectSpec = {
8+
runtimes: [{}, {}],
9+
memories: [{}],
10+
credentials: [{}, {}, {}],
11+
evaluators: [{}],
12+
onlineEvalConfigs: [{}, {}],
13+
agentCoreGateways: [{ targets: [{}, {}] }, { targets: [{}] }],
14+
policyEngines: [{ policies: [{}, {}] }, { policies: [{}] }],
15+
} as unknown as Partial<AgentCoreProjectSpec>;
16+
17+
expect(computeDeployAttrs(projectSpec, 'diff')).toEqual({
18+
runtime_count: 2,
19+
memory_count: 1,
20+
credential_count: 3,
21+
evaluator_count: 1,
22+
online_eval_count: 2,
23+
gateway_count: 2,
24+
gateway_target_count: 3,
25+
policy_engine_count: 2,
26+
policy_count: 3,
27+
mode: 'diff',
28+
});
29+
});
30+
31+
it('returns zeros for empty spec', () => {
32+
expect(computeDeployAttrs({}, 'deploy')).toEqual({
33+
runtime_count: 0,
34+
memory_count: 0,
35+
credential_count: 0,
36+
evaluator_count: 0,
37+
online_eval_count: 0,
38+
gateway_count: 0,
39+
gateway_target_count: 0,
40+
policy_engine_count: 0,
41+
policy_count: 0,
42+
mode: 'deploy',
43+
});
44+
});
45+
46+
it('handles dry-run mode', () => {
47+
const projectSpec = { runtimes: [{}] } as unknown as Partial<AgentCoreProjectSpec>;
48+
const attrs = computeDeployAttrs(projectSpec, 'dry-run');
49+
50+
expect(attrs.runtime_count).toBe(1);
51+
expect(attrs.memory_count).toBe(0);
52+
expect(attrs.mode).toBe('dry-run');
53+
});
54+
});

src/cli/commands/deploy/command.tsx

Lines changed: 75 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,12 @@
1+
import { ConfigIO } from '../../../lib/index.js';
12
import { getErrorMessage } from '../../errors';
3+
import { withCommandRunTelemetry } from '../../telemetry/cli-command-run.js';
24
import { COMMAND_DESCRIPTIONS } from '../../tui/copy';
35
import { requireProject, requireTTY } from '../../tui/guards';
46
import { DeployScreen } from '../../tui/screens/deploy/DeployScreen';
57
import { handleDeploy } from './actions';
6-
import type { DeployOptions } from './types';
8+
import type { DeployOptions, DeployResult } from './types';
9+
import { DEFAULT_DEPLOY_ATTRS, computeDeployAttrs } from './utils';
710
import { validateDeployOptions } from './validate';
811
import type { Command } from '@commander-js/extra-typings';
912
import { Text, render } from 'ink';
@@ -38,6 +41,45 @@ async function handleDeployCLI(options: DeployOptions): Promise<void> {
3841
process.exit(1);
3942
}
4043

44+
// Compute attrs upfront from project spec (available before deploy)
45+
const mode = options.diff ? 'diff' : options.plan ? 'dry-run' : 'deploy';
46+
const attrs = await new ConfigIO()
47+
.readProjectSpec()
48+
.then(spec => computeDeployAttrs(spec, mode))
49+
.catch(() => ({ ...DEFAULT_DEPLOY_ATTRS, mode }) as const);
50+
51+
const { deployResult } = await withCommandRunTelemetry('deploy', attrs, async () => {
52+
const result = await executeDeploy(options).catch(
53+
(e): DeployResult => ({ success: false, error: getErrorMessage(e) })
54+
);
55+
if (!result.success) {
56+
return { success: false as const, error: result.error ?? 'Deploy failed', deployResult: result };
57+
}
58+
return { success: true as const, deployResult: result };
59+
});
60+
61+
// ALL output happens here, after telemetry
62+
if (!deployResult.success) {
63+
if (options.json) {
64+
console.log(JSON.stringify(deployResult));
65+
} else {
66+
console.error(deployResult.error ?? 'Deploy failed');
67+
if (deployResult.logPath) {
68+
console.error(`Log: ${deployResult.logPath}`);
69+
}
70+
}
71+
process.exit(1);
72+
}
73+
74+
printDeployResult(deployResult, options);
75+
76+
if (deployResult.postDeployWarnings && deployResult.postDeployWarnings.length > 0) {
77+
process.exit(2);
78+
}
79+
process.exit(0);
80+
}
81+
82+
async function executeDeploy(options: DeployOptions): Promise<DeployResult> {
4183
let spinner: NodeJS.Timeout | undefined;
4284

4385
// Progress callback for --progress mode
@@ -84,55 +126,52 @@ async function handleDeployCLI(options: DeployOptions): Promise<void> {
84126
process.stdout.write('\r\x1b[K');
85127
}
86128

129+
return result;
130+
}
131+
132+
function printDeployResult(result: DeployResult, options: DeployOptions): void {
87133
if (options.json) {
88134
console.log(JSON.stringify(result));
89-
} else if (result.success) {
90-
if (options.diff) {
91-
console.log(`\n✓ Diff complete for '${result.targetName}' (stack: ${result.stackName})`);
92-
} else if (options.plan) {
93-
console.log(`\n✓ Dry run complete for '${result.targetName}' (stack: ${result.stackName})`);
94-
console.log('\nRun `agentcore deploy` to deploy.');
95-
} else {
96-
console.log(`\n✓ Deployed to '${result.targetName}' (stack: ${result.stackName})`);
135+
return;
136+
}
97137

98-
// Show stack outputs in non-JSON mode
99-
if (result.outputs && Object.keys(result.outputs).length > 0) {
100-
console.log('\nOutputs:');
101-
for (const [key, value] of Object.entries(result.outputs)) {
102-
console.log(` ${key}: ${value}`);
103-
}
104-
}
138+
if (options.diff) {
139+
console.log(`\n✓ Diff complete for '${result.targetName}' (stack: ${result.stackName})`);
140+
} else if (options.plan) {
141+
console.log(`\n✓ Dry run complete for '${result.targetName}' (stack: ${result.stackName})`);
142+
console.log('\nRun `agentcore deploy` to deploy.');
143+
} else {
144+
console.log(`\n✓ Deployed to '${result.targetName}' (stack: ${result.stackName})`);
105145

106-
if (result.postDeployWarnings && result.postDeployWarnings.length > 0) {
107-
console.log('\n⚠ Post-deploy warnings:');
108-
for (const warning of result.postDeployWarnings) {
109-
console.log(` ${warning}`);
110-
}
146+
// Show stack outputs in non-JSON mode
147+
if (result.outputs && Object.keys(result.outputs).length > 0) {
148+
console.log('\nOutputs:');
149+
for (const [key, value] of Object.entries(result.outputs)) {
150+
console.log(` ${key}: ${value}`);
111151
}
152+
}
112153

113-
if (result.notes && result.notes.length > 0) {
114-
for (const note of result.notes) {
115-
console.log(`\nNote: ${note}`);
116-
}
154+
if (result.postDeployWarnings && result.postDeployWarnings.length > 0) {
155+
console.log('\n⚠ Post-deploy warnings:');
156+
for (const warning of result.postDeployWarnings) {
157+
console.log(` ${warning}`);
117158
}
159+
}
118160

119-
if (result.nextSteps && result.nextSteps.length > 0) {
120-
console.log(`Next: ${result.nextSteps.join(' | ')}`);
161+
if (result.notes && result.notes.length > 0) {
162+
for (const note of result.notes) {
163+
console.log(`\nNote: ${note}`);
121164
}
122165
}
123166

124-
if (result.logPath) {
125-
console.log(`\nLog: ${result.logPath}`);
126-
}
127-
} else {
128-
console.error(result.error);
129-
if (result.logPath) {
130-
console.error(`Log: ${result.logPath}`);
167+
if (result.nextSteps && result.nextSteps.length > 0) {
168+
console.log(`Next: ${result.nextSteps.join(' | ')}`);
131169
}
132170
}
133171

134-
const hasPostDeployWarnings = result.success && result.postDeployWarnings && result.postDeployWarnings.length > 0;
135-
process.exit(result.success ? (hasPostDeployWarnings ? 2 : 0) : 1);
172+
if (result.logPath) {
173+
console.log(`\nLog: ${result.logPath}`);
174+
}
136175
}
137176

138177
export const registerDeploy = (program: Command) => {

src/cli/commands/deploy/utils.ts

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
import type { AgentCoreProjectSpec } from '../../../schema';
2+
3+
export type DeployMode = 'deploy' | 'dry-run' | 'diff';
4+
5+
export const DEFAULT_DEPLOY_ATTRS = {
6+
runtime_count: 0,
7+
memory_count: 0,
8+
credential_count: 0,
9+
evaluator_count: 0,
10+
online_eval_count: 0,
11+
gateway_count: 0,
12+
gateway_target_count: 0,
13+
policy_engine_count: 0,
14+
policy_count: 0,
15+
mode: 'deploy' as DeployMode,
16+
};
17+
18+
export function computeDeployAttrs(projectSpec: Partial<AgentCoreProjectSpec>, mode: DeployMode) {
19+
const gateways = projectSpec.agentCoreGateways ?? [];
20+
const policyEngines = projectSpec.policyEngines ?? [];
21+
return {
22+
runtime_count: (projectSpec.runtimes ?? []).length,
23+
memory_count: (projectSpec.memories ?? []).length,
24+
credential_count: (projectSpec.credentials ?? []).length,
25+
evaluator_count: (projectSpec.evaluators ?? []).length,
26+
online_eval_count: (projectSpec.onlineEvalConfigs ?? []).length,
27+
gateway_count: gateways.length,
28+
gateway_target_count: gateways.reduce((sum, g) => sum + (g.targets ?? []).length, 0),
29+
policy_engine_count: policyEngines.length,
30+
policy_count: policyEngines.reduce((sum, pe) => sum + (pe.policies ?? []).length, 0),
31+
mode,
32+
};
33+
}

src/cli/telemetry/schemas/__tests__/command-run.test.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,7 @@ describe('COMMAND_SCHEMAS', () => {
4747
gateway_target_count: 3,
4848
policy_engine_count: 0,
4949
policy_count: 0,
50-
has_diff: true,
50+
mode: 'diff',
5151
};
5252
expect(COMMAND_SCHEMAS.deploy.parse(attrs)).toEqual(attrs);
5353
});
@@ -64,7 +64,7 @@ describe('COMMAND_SCHEMAS', () => {
6464
gateway_target_count: 0,
6565
policy_engine_count: 0,
6666
policy_count: 0,
67-
has_diff: false,
67+
mode: 'deploy',
6868
})
6969
).toThrow();
7070
});
@@ -81,7 +81,7 @@ describe('COMMAND_SCHEMAS', () => {
8181
gateway_target_count: 0,
8282
policy_engine_count: 0,
8383
policy_count: 0,
84-
has_diff: false,
84+
mode: 'deploy',
8585
})
8686
).toThrow();
8787
});

src/cli/telemetry/schemas/command-run.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -100,7 +100,7 @@ const DeployAttrs = safeSchema({
100100
gateway_target_count: Count,
101101
policy_engine_count: Count,
102102
policy_count: Count,
103-
has_diff: z.boolean(),
103+
mode: z.enum(['deploy', 'dry-run', 'diff']),
104104
});
105105

106106
const DevAttrs = safeSchema({

src/cli/tui/screens/deploy/useDeployFlow.ts

Lines changed: 16 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import {
1111
parsePolicyEngineOutputs,
1212
parsePolicyOutputs,
1313
} from '../../../cloudformation';
14+
import { DEFAULT_DEPLOY_ATTRS, computeDeployAttrs } from '../../../commands/deploy/utils.js';
1415
import { getErrorMessage, isChangesetInProgressError, isExpiredTokenError } from '../../../errors';
1516
import { ExecLogger } from '../../../logging';
1617
import { performStackTeardown, setupTransactionSearch } from '../../../operations/deploy';
@@ -22,6 +23,7 @@ import {
2223
} from '../../../operations/deploy/post-deploy-config-bundles';
2324
import { setupHttpGateways } from '../../../operations/deploy/post-deploy-http-gateways';
2425
import { enableOnlineEvalConfigs } from '../../../operations/deploy/post-deploy-online-evals';
26+
import { withCommandRunTelemetry } from '../../../telemetry/cli-command-run.js';
2527
import {
2628
type StackDiffSummary,
2729
type Step,
@@ -550,7 +552,9 @@ export function useDeployFlow(options: DeployFlowOptions = {}): DeployFlowState
550552
if (deployStep.status !== 'pending') return;
551553
if (!cdkToolkitWrapper) return;
552554

553-
const run = async () => {
555+
const attrs = context ? computeDeployAttrs(context.projectSpec, 'deploy') : { ...DEFAULT_DEPLOY_ATTRS };
556+
557+
const run = async (): Promise<{ success: true } | { success: false; error: string }> => {
554558
// Run diff before deploy to capture pre-deploy differences
555559
if (!isDiffRunningRef.current) {
556560
isDiffRunningRef.current = true;
@@ -669,6 +673,7 @@ export function useDeployFlow(options: DeployFlowOptions = {}): DeployFlowState
669673
// Mark both steps as success (in case CFn events were never received)
670674
setPublishAssetsStep(prev => ({ ...prev, status: 'success' }));
671675
setDeployStep(prev => ({ ...prev, status: 'success' }));
676+
return { success: true } as const;
672677
} catch (err) {
673678
const errorMsg = getErrorMessage(err);
674679

@@ -700,6 +705,7 @@ export function useDeployFlow(options: DeployFlowOptions = {}): DeployFlowState
700705
error: logger.getFailureMessage('Publish assets'),
701706
}));
702707
}
708+
return { success: false, error: errorMsg } as const;
703709
} finally {
704710
// Disable verbose output and clear callback after deploy
705711
switchableIoHost?.setVerbose(false);
@@ -709,7 +715,7 @@ export function useDeployFlow(options: DeployFlowOptions = {}): DeployFlowState
709715
}
710716
};
711717

712-
void run();
718+
void withCommandRunTelemetry('deploy', attrs, run);
713719
}, [
714720
preflight.phase,
715721
cdkToolkitWrapper,
@@ -734,7 +740,11 @@ export function useDeployFlow(options: DeployFlowOptions = {}): DeployFlowState
734740
if (diffStep.status !== 'pending') return;
735741
if (!cdkToolkitWrapper) return;
736742

737-
const run = async () => {
743+
const attrs = context
744+
? computeDeployAttrs(context.projectSpec, 'diff')
745+
: { ...DEFAULT_DEPLOY_ATTRS, mode: 'diff' as const };
746+
747+
const run = async (): Promise<{ success: true } | { success: false; error: string }> => {
738748
setDiffStep(prev => ({ ...prev, status: 'running' }));
739749
setShouldStartDeploy(false);
740750
setDiffSummaries([]);
@@ -755,6 +765,7 @@ export function useDeployFlow(options: DeployFlowOptions = {}): DeployFlowState
755765
logger.endStep('success');
756766
logger.finalize(true);
757767
setDiffStep(prev => ({ ...prev, status: 'success' }));
768+
return { success: true };
758769
} catch (err) {
759770
const errorMsg = getErrorMessage(err);
760771
logger.endStep('error', errorMsg);
@@ -769,14 +780,15 @@ export function useDeployFlow(options: DeployFlowOptions = {}): DeployFlowState
769780
status: 'error',
770781
error: logger.getFailureMessage('Run CDK diff'),
771782
}));
783+
return { success: false, error: errorMsg };
772784
} finally {
773785
switchableIoHost?.setVerbose(false);
774786
switchableIoHost?.setOnRawMessage(null);
775787
void cdkToolkitWrapper.dispose();
776788
}
777789
};
778790

779-
void run();
791+
void withCommandRunTelemetry('deploy', attrs, run);
780792
}, [
781793
diffMode,
782794
preflight.phase,

0 commit comments

Comments
 (0)