Skip to content

Commit bceddde

Browse files
authored
feat: instrument telemetry for create command (CLI + TUI) (#1202)
Add telemetry recording to the create command in both CLI and TUI paths: - CLI: wrap handleCreateCLI with runCliCommand to emit CreateAttrs on success/failure - TUI: wrap useCreateFlow's run() with withCommandRunTelemetry - Add telemetry assertions to existing integration tests (frameworks + edge cases)
1 parent ce50d52 commit bceddde

5 files changed

Lines changed: 170 additions & 83 deletions

File tree

integ-tests/create-edge-cases.test.ts

Lines changed: 20 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
/* eslint-disable security/detect-non-literal-fs-filename */
22
import { exists, prereqs, runCLI } from '../src/test-utils/index.js';
3+
import { createTelemetryHelper } from '../src/test-utils/telemetry-helper.js';
34
import { randomUUID } from 'node:crypto';
45
import { mkdir, rm } from 'node:fs/promises';
56
import { tmpdir } from 'node:os';
@@ -9,18 +10,21 @@ import { afterAll, beforeAll, describe, expect, it } from 'vitest';
910
describe.skipIf(!prereqs.npm || !prereqs.git)('integration: create edge cases', () => {
1011
let testDir: string;
1112

13+
const telemetry = createTelemetryHelper();
14+
1215
beforeAll(async () => {
1316
testDir = join(tmpdir(), `agentcore-integ-edge-${randomUUID()}`);
1417
await mkdir(testDir, { recursive: true });
1518
});
1619

1720
afterAll(async () => {
21+
telemetry.destroy();
1822
await rm(testDir, { recursive: true, force: true });
1923
});
2024

2125
describe('reserved names', () => {
2226
it('rejects reserved name "Test"', async () => {
23-
const result = await runCLI(['create', '--name', 'Test', '--json'], testDir);
27+
const result = await runCLI(['create', '--name', 'Test', '--json'], testDir, { env: telemetry.env });
2428

2529
expect(result.exitCode).toBe(1);
2630
const json = JSON.parse(result.stdout);
@@ -30,6 +34,11 @@ describe.skipIf(!prereqs.npm || !prereqs.git)('integration: create edge cases',
3034
json.error.toLowerCase().includes('reserved') || json.error.toLowerCase().includes('conflict'),
3135
`Error should mention reserved/conflict: ${json.error}`
3236
).toBeTruthy();
37+
38+
telemetry.assertMetricEmitted({
39+
command: 'create',
40+
exit_reason: 'failure',
41+
});
3342
});
3443

3544
it('rejects reserved name "bedrock"', async () => {
@@ -121,12 +130,21 @@ describe.skipIf(!prereqs.npm || !prereqs.git)('integration: create edge cases',
121130
describe('flag interactions', () => {
122131
it('--defaults creates project with default settings', async () => {
123132
const name = `Def${Date.now().toString().slice(-6)}`;
124-
const result = await runCLI(['create', '--name', name, '--defaults', '--json'], testDir);
133+
const result = await runCLI(['create', '--name', name, '--defaults', '--json'], testDir, { env: telemetry.env });
125134

126135
expect(result.exitCode, `stderr: ${result.stderr}`).toBe(0);
127136
const json = JSON.parse(result.stdout);
128137
expect(json.success).toBe(true);
129138
expect(json.projectPath).toBeTruthy();
139+
140+
telemetry.assertMetricEmitted({
141+
command: 'create',
142+
exit_reason: 'success',
143+
language: 'python',
144+
framework: 'strands',
145+
model_provider: 'bedrock',
146+
has_agent: 'true',
147+
});
130148
});
131149

132150
it('--dry-run shows what would be created without writing files', async () => {

integ-tests/create-frameworks.test.ts

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import { exists, prereqs, readProjectConfig, runCLI } from '../src/test-utils/index.js';
2+
import { createTelemetryHelper } from '../src/test-utils/telemetry-helper.js';
23
import { randomUUID } from 'node:crypto';
34
import { mkdir, readFile, rm } from 'node:fs/promises';
45
import { tmpdir } from 'node:os';
@@ -8,12 +9,15 @@ import { afterAll, beforeAll, describe, expect, it } from 'vitest';
89
describe.skipIf(!prereqs.npm || !prereqs.git)('integration: create with different frameworks', () => {
910
let testDir: string;
1011

12+
const telemetry = createTelemetryHelper();
13+
1114
beforeAll(async () => {
1215
testDir = join(tmpdir(), `agentcore-integ-frameworks-${randomUUID()}`);
1316
await mkdir(testDir, { recursive: true });
1417
});
1518

1619
afterAll(async () => {
20+
telemetry.destroy();
1721
await rm(testDir, { recursive: true, force: true });
1822
});
1923

@@ -34,7 +38,8 @@ describe.skipIf(!prereqs.npm || !prereqs.git)('integration: create with differen
3438
'none',
3539
'--json',
3640
],
37-
testDir
41+
testDir,
42+
{ env: telemetry.env }
3843
);
3944

4045
expect(result.exitCode, `stderr: ${result.stderr}`).toBe(0);
@@ -60,6 +65,15 @@ describe.skipIf(!prereqs.npm || !prereqs.git)('integration: create with differen
6065
expect(agents).toBeDefined();
6166
expect(agents.length).toBe(1);
6267
expect(agents[0]!.name).toBe(agentName);
68+
69+
telemetry.assertMetricEmitted({
70+
command: 'create',
71+
exit_reason: 'success',
72+
language: 'python',
73+
framework: 'langchain_langgraph',
74+
model_provider: 'bedrock',
75+
has_agent: 'true',
76+
});
6377
});
6478

6579
it('creates GoogleADK project with Gemini provider', async () => {

src/cli/commands/create/command.tsx

Lines changed: 102 additions & 73 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,18 @@ import type {
99
} from '../../../schema';
1010
import { LIFECYCLE_TIMEOUT_MAX, LIFECYCLE_TIMEOUT_MIN } from '../../../schema';
1111
import { getErrorMessage } from '../../errors';
12+
import { runCliCommand } from '../../telemetry/cli-command-run.js';
13+
import {
14+
AgentType,
15+
Build,
16+
Framework,
17+
Language,
18+
Memory,
19+
ModelProvider as ModelProviderEnum,
20+
NetworkMode as NetworkModeEnum,
21+
Protocol,
22+
standardize,
23+
} from '../../telemetry/schemas/common-shapes.js';
1224
import { COMMAND_DESCRIPTIONS } from '../../tui/copy';
1325
import { requireTTY } from '../../tui/guards';
1426
import { CreateScreen } from '../../tui/screens/create';
@@ -79,18 +91,17 @@ async function handleCreateCLI(options: CreateOptions): Promise<void> {
7991
const name = options.name ?? options.projectName;
8092
const projectName = options.projectName ?? name;
8193

82-
const validation = validateCreateOptions(options, cwd);
83-
if (!validation.valid) {
84-
if (options.json) {
85-
console.log(JSON.stringify({ success: false, error: validation.error }));
86-
} else {
87-
console.error(validation.error);
88-
}
89-
process.exit(1);
90-
}
91-
92-
// Handle dry-run mode
94+
// Handle dry-run mode (no telemetry for dry-run)
9395
if (options.dryRun) {
96+
const validation = validateCreateOptions(options, cwd);
97+
if (!validation.valid) {
98+
if (options.json) {
99+
console.log(JSON.stringify({ success: false, error: validation.error }));
100+
} else {
101+
console.error(validation.error);
102+
}
103+
process.exit(1);
104+
}
94105
const result = getDryRunInfo({ name: name!, projectName, cwd, language: options.language });
95106
if (options.json) {
96107
console.log(JSON.stringify(result));
@@ -103,74 +114,92 @@ async function handleCreateCLI(options: CreateOptions): Promise<void> {
103114
process.exit(0);
104115
}
105116

106-
const green = '\x1b[32m';
107-
const reset = '\x1b[0m';
117+
await runCliCommand('create', !!options.json, async () => {
118+
const validation = validateCreateOptions(options, cwd);
119+
if (!validation.valid) {
120+
throw new Error(validation.error);
121+
}
122+
const green = '\x1b[32m';
123+
const reset = '\x1b[0m';
108124

109-
// Progress callback for real-time output
110-
const onProgress: ProgressCallback | undefined = options.json
111-
? undefined
112-
: (step, status) => {
113-
if (status === 'done') {
114-
console.log(`${green}[done]${reset} ${step}`);
115-
} else if (status === 'error') {
116-
console.log(`\x1b[31m[error]${reset} ${step}`);
117-
}
118-
// 'start' is silent - we only show when done
119-
};
125+
// Progress callback for real-time output
126+
const onProgress: ProgressCallback | undefined = options.json
127+
? undefined
128+
: (step, status) => {
129+
if (status === 'done') {
130+
console.log(`${green}[done]${reset} ${step}`);
131+
} else if (status === 'error') {
132+
console.log(`\x1b[31m[error]${reset} ${step}`);
133+
}
134+
// 'start' is silent - we only show when done
135+
};
120136

121-
// Commander.js --no-agent sets agent=false, not noAgent=true
122-
const skipAgent = options.agent === false;
137+
// Commander.js --no-agent sets agent=false, not noAgent=true
138+
const skipAgent = options.agent === false;
123139

124-
const result = skipAgent
125-
? await createProject({
126-
name: projectName!,
127-
cwd,
128-
skipGit: options.skipGit,
129-
skipInstall: options.skipInstall,
130-
onProgress,
131-
})
132-
: await createProjectWithAgent({
133-
name: name!,
134-
projectName,
135-
cwd,
136-
type: options.type as 'create' | 'import' | undefined,
137-
buildType: (options.build as BuildType) ?? 'CodeZip',
138-
language: (options.language as TargetLanguage) ?? (options.type === 'import' ? 'Python' : undefined),
139-
framework: options.framework as SDKFramework | undefined,
140-
modelProvider: options.modelProvider as ModelProvider | undefined,
141-
apiKey: options.apiKey,
142-
memory: (options.memory as 'none' | 'shortTerm' | 'longAndShortTerm') ?? 'none',
143-
protocol: options.protocol as ProtocolMode | undefined,
144-
agentId: options.agentId,
145-
agentAliasId: options.agentAliasId,
146-
region: options.region,
147-
networkMode: options.networkMode as NetworkMode | undefined,
148-
subnets: parseCommaSeparatedList(options.subnets),
149-
securityGroups: parseCommaSeparatedList(options.securityGroups),
150-
idleTimeout: options.idleTimeout ? Number(options.idleTimeout) : undefined,
151-
maxLifetime: options.maxLifetime ? Number(options.maxLifetime) : undefined,
152-
sessionStorageMountPath: options.sessionStorageMountPath,
153-
withConfigBundle: options.withConfigBundle,
154-
skipGit: options.skipGit,
155-
skipInstall: options.skipInstall,
156-
skipPythonSetup: options.skipPythonSetup,
157-
onProgress,
158-
});
140+
const result = skipAgent
141+
? await createProject({
142+
name: projectName!,
143+
cwd,
144+
skipGit: options.skipGit,
145+
skipInstall: options.skipInstall,
146+
onProgress,
147+
})
148+
: await createProjectWithAgent({
149+
name: name!,
150+
projectName,
151+
cwd,
152+
type: options.type as 'create' | 'import' | undefined,
153+
buildType: (options.build as BuildType) ?? 'CodeZip',
154+
language: (options.language as TargetLanguage) ?? (options.type === 'import' ? 'Python' : undefined),
155+
framework: options.framework as SDKFramework | undefined,
156+
modelProvider: options.modelProvider as ModelProvider | undefined,
157+
apiKey: options.apiKey,
158+
memory: (options.memory as 'none' | 'shortTerm' | 'longAndShortTerm') ?? 'none',
159+
protocol: options.protocol as ProtocolMode | undefined,
160+
agentId: options.agentId,
161+
agentAliasId: options.agentAliasId,
162+
region: options.region,
163+
networkMode: options.networkMode as NetworkMode | undefined,
164+
subnets: parseCommaSeparatedList(options.subnets),
165+
securityGroups: parseCommaSeparatedList(options.securityGroups),
166+
idleTimeout: options.idleTimeout ? Number(options.idleTimeout) : undefined,
167+
maxLifetime: options.maxLifetime ? Number(options.maxLifetime) : undefined,
168+
sessionStorageMountPath: options.sessionStorageMountPath,
169+
withConfigBundle: options.withConfigBundle,
170+
skipGit: options.skipGit,
171+
skipInstall: options.skipInstall,
172+
skipPythonSetup: options.skipPythonSetup,
173+
onProgress,
174+
});
159175

160-
if (options.json) {
161-
console.log(JSON.stringify(result));
162-
} else if (result.success) {
163-
printCreateSummary(projectName!, result.agentName, options.language, options.framework);
164-
if (options.skipInstall) {
165-
console.log(
166-
"\nDependency installation was skipped. Run 'npm install' in agentcore/cdk/ and 'uv sync' in your agent directory manually."
167-
);
176+
if (!result.success) {
177+
throw new Error(result.error);
178+
}
179+
180+
if (options.json) {
181+
console.log(JSON.stringify(result));
182+
} else {
183+
printCreateSummary(projectName!, result.agentName, options.language, options.framework);
184+
if (options.skipInstall) {
185+
console.log(
186+
"\nDependency installation was skipped. Run 'npm install' in agentcore/cdk/ and 'uv sync' in your agent directory manually."
187+
);
188+
}
168189
}
169-
} else {
170-
console.error(result.error);
171-
}
172190

173-
process.exit(result.success ? 0 : 1);
191+
return {
192+
language: standardize(Language, options.language),
193+
framework: standardize(Framework, options.framework),
194+
model_provider: standardize(ModelProviderEnum, options.modelProvider),
195+
memory: standardize(Memory, options.memory ?? 'none'),
196+
protocol: standardize(Protocol, options.protocol ?? 'http'),
197+
build: standardize(Build, options.build ?? 'codezip'),
198+
agent_type: standardize(AgentType, options.type ?? 'create'),
199+
network_mode: standardize(NetworkModeEnum, options.networkMode ?? 'public'),
200+
has_agent: options.agent !== false,
201+
};
202+
});
174203
}
175204

176205
export const registerCreate = (program: Command) => {

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,7 @@ const CreateAttrs = safeSchema({
4141
memory: Memory,
4242
protocol: Protocol,
4343
build: Build,
44-
agent_type: z.enum(['create', 'import']),
44+
agent_type: AgentType,
4545
network_mode: NetworkMode,
4646
has_agent: z.boolean(),
4747
});

0 commit comments

Comments
 (0)