Skip to content

Commit 6915c47

Browse files
feat: instrument telemetry for status command (#1317)
* 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 * fix: include deployedState in ProjectStatusResult to fix scope error After rebasing, the dataset enrichment code referenced context.deployedState outside the withCommandRunTelemetry callback where context was defined. Rather than hoisting context outside the callback (which would lose telemetry coverage for loadStatusConfig failures), include deployedState in the ProjectStatusResult so it flows through the Result type naturally. --------- Co-authored-by: Jesse Turner <57651174+jesseturner21@users.noreply.github.com>
1 parent 1afe035 commit 6915c47

4 files changed

Lines changed: 167 additions & 34 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/action.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@ export interface ResourceStatusEntry {
3737
export type ProjectStatusResult = Result<{
3838
targetRegion?: string;
3939
resources: ResourceStatusEntry[];
40+
deployedState: DeployedState;
4041
}> & { projectName?: string; targetName?: string; logPath?: string; resources?: ResourceStatusEntry[] };
4142

4243
export interface StatusContext {
@@ -490,6 +491,7 @@ export async function handleProjectStatus(
490491
targetName: selectedTargetName ?? '',
491492
targetRegion: targetConfig?.region,
492493
resources,
494+
deployedState,
493495
logPath: logger.getRelativeLogPath(),
494496
};
495497
}

src/cli/commands/status/command.tsx

Lines changed: 47 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,9 @@
1-
import { serializeResult } from '../../../lib';
1+
import { ValidationError, serializeResult } from '../../../lib';
22
import { getErrorMessage } from '../../errors';
33
import { getDatasetStatus } from '../../operations/dataset';
44
import type { DatasetStatusResult } from '../../operations/dataset';
5+
import { withCommandRunTelemetry } from '../../telemetry/cli-command-run.js';
6+
import { FilterState, FilterType, standardize } from '../../telemetry/schemas/common-shapes.js';
57
import { COMMAND_DESCRIPTIONS } from '../../tui/copy';
68
import { requireProject } from '../../tui/guards';
79
import type { ResourceStatusEntry } from './action';
@@ -73,48 +75,58 @@ export const registerStatus = (program: Command) => {
7375
.action(async (cliOptions: StatusCliOptions) => {
7476
requireProject();
7577

78+
const telemetryAttrs = {
79+
filter_type: standardize(FilterType, cliOptions.type ?? 'none'),
80+
filter_state: standardize(FilterState, cliOptions.state ?? 'none'),
81+
};
82+
7683
// Validate --type
7784
if (cliOptions.type && !(VALID_RESOURCE_TYPES as readonly string[]).includes(cliOptions.type)) {
78-
render(
79-
<Text color="red">
80-
Invalid resource type &apos;{cliOptions.type}&apos;. Valid types: {VALID_RESOURCE_TYPES.join(', ')}
81-
</Text>
82-
);
85+
const msg = `Invalid resource type '${cliOptions.type}'. Valid types: ${VALID_RESOURCE_TYPES.join(', ')}`;
86+
await withCommandRunTelemetry('status', telemetryAttrs, () => ({
87+
success: false as const,
88+
error: new ValidationError(msg),
89+
}));
90+
render(<Text color="red">{msg}</Text>);
8391
return;
8492
}
8593

8694
// Validate --state
8795
if (cliOptions.state && !(VALID_STATES as readonly string[]).includes(cliOptions.state)) {
88-
render(
89-
<Text color="red">
90-
Invalid state &apos;{cliOptions.state}&apos;. Valid states: {VALID_STATES.join(', ')}
91-
</Text>
92-
);
96+
const msg = `Invalid state '${cliOptions.state}'. Valid states: ${VALID_STATES.join(', ')}`;
97+
await withCommandRunTelemetry('status', telemetryAttrs, () => ({
98+
success: false as const,
99+
error: new ValidationError(msg),
100+
}));
101+
render(<Text color="red">{msg}</Text>);
93102
return;
94103
}
95104

96105
try {
97-
const context = await loadStatusConfig();
98-
99106
// Direct runtime lookup by ID
100107
if (cliOptions.runtimeId) {
101-
const result = await handleRuntimeLookup(context, {
102-
agentRuntimeId: cliOptions.runtimeId,
103-
targetName: cliOptions.target,
108+
const result = await withCommandRunTelemetry('status', telemetryAttrs, async () => {
109+
const context = await loadStatusConfig();
110+
return handleRuntimeLookup(context, {
111+
agentRuntimeId: cliOptions.runtimeId!,
112+
targetName: cliOptions.target,
113+
});
104114
});
105115

106-
if (cliOptions.json) {
107-
console.log(JSON.stringify(serializeResult(result), null, 2));
108-
return;
116+
if (!result.success) {
117+
if (cliOptions.json) {
118+
console.log(JSON.stringify(serializeResult(result), null, 2));
119+
} else {
120+
render(<Text color="red">{result.error.message}</Text>);
121+
}
122+
process.exit(1);
109123
}
110124

111-
if (!result.success) {
112-
render(<Text color="red">{result.error.message}</Text>);
125+
if (cliOptions.json) {
126+
console.log(JSON.stringify(serializeResult(result), null, 2));
113127
return;
114128
}
115-
116129
const runtimeStatus = result.runtimeStatus ? `Runtime status: ${result.runtimeStatus}` : '';
117-
118130
render(
119131
<Text>
120132
AgentCore Status - {result.runtimeId} (target: {result.targetName})
@@ -125,22 +137,23 @@ export const registerStatus = (program: Command) => {
125137
}
126138

127139
// Default path: show all resource types with deployment state
128-
const result = await handleProjectStatus(context, {
129-
targetName: cliOptions.target,
140+
const result = await withCommandRunTelemetry('status', telemetryAttrs, async () => {
141+
const context = await loadStatusConfig();
142+
return handleProjectStatus(context, { targetName: cliOptions.target });
130143
});
131144

132-
if (cliOptions.json) {
133-
if (result.success) {
134-
const filtered = filterResources(result.resources, cliOptions);
135-
console.log(JSON.stringify({ ...result, resources: filtered }, null, 2));
136-
} else {
145+
if (!result.success) {
146+
if (cliOptions.json) {
137147
console.log(JSON.stringify(serializeResult(result), null, 2));
148+
} else {
149+
render(<Text color="red">{result.error.message}</Text>);
138150
}
139-
return;
151+
process.exit(1);
140152
}
141153

142-
if (!result.success) {
143-
render(<Text color="red">{result.error.message}</Text>);
154+
if (cliOptions.json) {
155+
const filtered = filterResources(result.resources, cliOptions);
156+
console.log(JSON.stringify({ ...serializeResult(result), resources: filtered }, null, 2));
144157
return;
145158
}
146159

@@ -162,7 +175,7 @@ export const registerStatus = (program: Command) => {
162175
// Fetch enriched dataset info when --type dataset is specified
163176
let datasetDetails: DatasetStatusResult[] = [];
164177
if (cliOptions.type === 'dataset' && datasets.length > 0 && result.targetRegion && result.targetName) {
165-
const deployedState = context.deployedState;
178+
const deployedState = result.deployedState;
166179
const targetResources = deployedState.targets?.[result.targetName]?.resources;
167180
const deployedDatasets = targetResources?.datasets ?? {};
168181

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)