Skip to content

Commit ed0c3dd

Browse files
committed
feat: instrument telemetry for status command
Wrap status command execution paths with withCommandRunTelemetry: - Validation errors (invalid --type/--state) - Runtime lookup by ID (--runtime-id) - Default project status Schema changes: - Add runtime-endpoint, config-bundle, ab-test to FilterType enum Error typing: - Use ValidationError for invalid --type/--state input Tests: - Integration tests verifying telemetry emission for all paths
1 parent fc135b0 commit ed0c3dd

3 files changed

Lines changed: 164 additions & 33 deletions

File tree

integ-tests/status.test.ts

Lines changed: 115 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,115 @@
1+
import { createTelemetryHelper, runCLI } from '../src/test-utils/index.js';
2+
import { randomUUID } from 'node:crypto';
3+
import { mkdir, rm } from 'node:fs/promises';
4+
import { tmpdir } from 'node:os';
5+
import { join } from 'node:path';
6+
import { afterAll, afterEach, beforeAll, describe, expect, it } from 'vitest';
7+
8+
describe('status command', () => {
9+
let testDir: string;
10+
let projectDir: string;
11+
const telemetry = createTelemetryHelper();
12+
13+
beforeAll(async () => {
14+
testDir = join(tmpdir(), `agentcore-status-telemetry-${randomUUID()}`);
15+
await mkdir(testDir, { recursive: true });
16+
17+
const projectName = 'StatusTelemetryProj';
18+
const result = await runCLI(['create', '--name', projectName, '--no-agent'], testDir);
19+
if (result.exitCode !== 0) {
20+
throw new Error(`Failed to create project: ${result.stdout} ${result.stderr}`);
21+
}
22+
projectDir = join(testDir, projectName);
23+
});
24+
25+
afterEach(() => {
26+
telemetry.clearEntries();
27+
});
28+
29+
afterAll(async () => {
30+
telemetry.destroy();
31+
await rm(testDir, { recursive: true, force: true });
32+
});
33+
34+
it('emits success telemetry for basic status', async () => {
35+
const result = await runCLI(['status', '--json'], projectDir, { env: telemetry.env });
36+
expect(result.exitCode).toBe(0);
37+
telemetry.assertMetricEmitted({
38+
command: 'status',
39+
exit_reason: 'success',
40+
filter_type: 'none',
41+
filter_state: 'none',
42+
});
43+
});
44+
45+
it('emits success telemetry with filter attrs', async () => {
46+
const result = await runCLI(['status', '--type', 'agent', '--state', 'deployed', '--json'], projectDir, {
47+
env: telemetry.env,
48+
});
49+
expect(result.exitCode).toBe(0);
50+
telemetry.assertMetricEmitted({
51+
command: 'status',
52+
exit_reason: 'success',
53+
filter_type: 'agent',
54+
filter_state: 'deployed',
55+
});
56+
});
57+
58+
it('emits success telemetry for runtime-endpoint filter', async () => {
59+
const result = await runCLI(['status', '--type', 'runtime-endpoint', '--json'], projectDir, {
60+
env: telemetry.env,
61+
});
62+
expect(result.exitCode).toBe(0);
63+
telemetry.assertMetricEmitted({
64+
command: 'status',
65+
exit_reason: 'success',
66+
filter_type: 'runtime-endpoint',
67+
});
68+
});
69+
70+
it('emits failure telemetry for invalid --type', async () => {
71+
const result = await runCLI(['status', '--type', 'bogus'], projectDir, { env: telemetry.env });
72+
expect(result.exitCode).toBe(0);
73+
telemetry.assertMetricEmitted({
74+
command: 'status',
75+
exit_reason: 'failure',
76+
filter_type: 'unknown',
77+
filter_state: 'none',
78+
});
79+
});
80+
81+
it('emits failure telemetry for invalid --state', async () => {
82+
const result = await runCLI(['status', '--state', 'bogus'], projectDir, { env: telemetry.env });
83+
expect(result.exitCode).toBe(0);
84+
telemetry.assertMetricEmitted({
85+
command: 'status',
86+
exit_reason: 'failure',
87+
filter_type: 'none',
88+
filter_state: 'unknown',
89+
});
90+
});
91+
92+
it('emits failure telemetry for nonexistent target', async () => {
93+
const result = await runCLI(['status', '--target', 'nonexistent', '--json'], projectDir, {
94+
env: telemetry.env,
95+
});
96+
expect(result.exitCode).toBe(1);
97+
telemetry.assertMetricEmitted({
98+
command: 'status',
99+
exit_reason: 'failure',
100+
filter_type: 'none',
101+
filter_state: 'none',
102+
});
103+
});
104+
105+
it('emits failure telemetry for --runtime-id lookup', async () => {
106+
const result = await runCLI(['status', '--runtime-id', 'fake-id', '--json'], projectDir, {
107+
env: telemetry.env,
108+
});
109+
expect(result.exitCode).toBe(1);
110+
telemetry.assertMetricEmitted({
111+
command: 'status',
112+
exit_reason: 'failure',
113+
});
114+
});
115+
});

src/cli/commands/status/command.tsx

Lines changed: 46 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
1-
import { serializeResult } from '../../../lib';
1+
import { ValidationError, serializeResult } from '../../../lib';
22
import { getErrorMessage } from '../../errors';
3+
import { withCommandRunTelemetry } from '../../telemetry/cli-command-run.js';
4+
import { FilterState, FilterType, standardize } from '../../telemetry/schemas/common-shapes.js';
35
import { COMMAND_DESCRIPTIONS } from '../../tui/copy';
46
import { requireProject } from '../../tui/guards';
57
import type { ResourceStatusEntry } from './action';
@@ -70,48 +72,58 @@ export const registerStatus = (program: Command) => {
7072
.action(async (cliOptions: StatusCliOptions) => {
7173
requireProject();
7274

75+
const telemetryAttrs = {
76+
filter_type: standardize(FilterType, cliOptions.type ?? 'none'),
77+
filter_state: standardize(FilterState, cliOptions.state ?? 'none'),
78+
};
79+
7380
// Validate --type
7481
if (cliOptions.type && !(VALID_RESOURCE_TYPES as readonly string[]).includes(cliOptions.type)) {
75-
render(
76-
<Text color="red">
77-
Invalid resource type &apos;{cliOptions.type}&apos;. Valid types: {VALID_RESOURCE_TYPES.join(', ')}
78-
</Text>
79-
);
82+
const msg = `Invalid resource type '${cliOptions.type}'. Valid types: ${VALID_RESOURCE_TYPES.join(', ')}`;
83+
await withCommandRunTelemetry('status', telemetryAttrs, () => ({
84+
success: false as const,
85+
error: new ValidationError(msg),
86+
}));
87+
render(<Text color="red">{msg}</Text>);
8088
return;
8189
}
8290

8391
// Validate --state
8492
if (cliOptions.state && !(VALID_STATES as readonly string[]).includes(cliOptions.state)) {
85-
render(
86-
<Text color="red">
87-
Invalid state &apos;{cliOptions.state}&apos;. Valid states: {VALID_STATES.join(', ')}
88-
</Text>
89-
);
93+
const msg = `Invalid state '${cliOptions.state}'. Valid states: ${VALID_STATES.join(', ')}`;
94+
await withCommandRunTelemetry('status', telemetryAttrs, () => ({
95+
success: false as const,
96+
error: new ValidationError(msg),
97+
}));
98+
render(<Text color="red">{msg}</Text>);
9099
return;
91100
}
92101

93102
try {
94-
const context = await loadStatusConfig();
95-
96103
// Direct runtime lookup by ID
97104
if (cliOptions.runtimeId) {
98-
const result = await handleRuntimeLookup(context, {
99-
agentRuntimeId: cliOptions.runtimeId,
100-
targetName: cliOptions.target,
105+
const result = await withCommandRunTelemetry('status', telemetryAttrs, async () => {
106+
const context = await loadStatusConfig();
107+
return handleRuntimeLookup(context, {
108+
agentRuntimeId: cliOptions.runtimeId!,
109+
targetName: cliOptions.target,
110+
});
101111
});
102112

103-
if (cliOptions.json) {
104-
console.log(JSON.stringify(serializeResult(result), null, 2));
105-
return;
113+
if (!result.success) {
114+
if (cliOptions.json) {
115+
console.log(JSON.stringify(serializeResult(result), null, 2));
116+
} else {
117+
render(<Text color="red">{result.error.message}</Text>);
118+
}
119+
process.exit(1);
106120
}
107121

108-
if (!result.success) {
109-
render(<Text color="red">{result.error.message}</Text>);
122+
if (cliOptions.json) {
123+
console.log(JSON.stringify(serializeResult(result), null, 2));
110124
return;
111125
}
112-
113126
const runtimeStatus = result.runtimeStatus ? `Runtime status: ${result.runtimeStatus}` : '';
114-
115127
render(
116128
<Text>
117129
AgentCore Status - {result.runtimeId} (target: {result.targetName})
@@ -122,22 +134,23 @@ export const registerStatus = (program: Command) => {
122134
}
123135

124136
// Default path: show all resource types with deployment state
125-
const result = await handleProjectStatus(context, {
126-
targetName: cliOptions.target,
137+
const result = await withCommandRunTelemetry('status', telemetryAttrs, async () => {
138+
const context = await loadStatusConfig();
139+
return handleProjectStatus(context, { targetName: cliOptions.target });
127140
});
128141

129-
if (cliOptions.json) {
130-
if (result.success) {
131-
const filtered = filterResources(result.resources, cliOptions);
132-
console.log(JSON.stringify({ ...result, resources: filtered }, null, 2));
133-
} else {
142+
if (!result.success) {
143+
if (cliOptions.json) {
134144
console.log(JSON.stringify(serializeResult(result), null, 2));
145+
} else {
146+
render(<Text color="red">{result.error.message}</Text>);
135147
}
136-
return;
148+
process.exit(1);
137149
}
138150

139-
if (!result.success) {
140-
render(<Text color="red">{result.error.message}</Text>);
151+
if (cliOptions.json) {
152+
const filtered = filterResources(result.resources, cliOptions);
153+
console.log(JSON.stringify({ ...serializeResult(result), resources: filtered }, null, 2));
141154
return;
142155
}
143156

src/cli/telemetry/schemas/common-shapes.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,13 +57,16 @@ export const ExitReason = z.enum(['success', 'failure']);
5757
export const FilterState = z.enum(['deployed', 'local-only', 'pending-removal', 'none']);
5858
export const FilterType = z.enum([
5959
'agent',
60+
'runtime-endpoint',
6061
'memory',
6162
'credential',
6263
'gateway',
6364
'evaluator',
6465
'online-eval',
6566
'policy-engine',
6667
'policy',
68+
'config-bundle',
69+
'ab-test',
6770
'none',
6871
]);
6972
export const AgentFramework = z.enum(['strands', 'langchain_langgraph', 'googleadk', 'openaiagents']);

0 commit comments

Comments
 (0)