Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
31 changes: 19 additions & 12 deletions src/cli/cli.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ import { COMMAND_DESCRIPTIONS, PACKAGE_VERSION } from './constants';
import { printPostCommandNotices, printTelemetryNotice } from './notices';
import { ALL_PRIMITIVES } from './primitives';
import { TelemetryClientAccessor } from './telemetry';
import { finalizeAndExit, registerPostCommandFinalize } from './telemetry/cli-command-run.js';
import { renderTUI, setupAltScreenCleanup } from './tui';
import { LayoutProvider } from './tui/context';
import { clearExitMessage, getExitMessage } from './tui/exit-message';
Expand Down Expand Up @@ -165,18 +166,24 @@ export const main = async (argv: string[]) => {
}

await TelemetryClientAccessor.init(args[0] ?? 'unknown');
try {
await program.parseAsync(argv);
} finally {
await TelemetryClientAccessor.shutdown();
}

// Telemetry notice already printed above; only run update check here.
await printPostCommandNotices(false, updateCheck);
// Post-command notices + exit message run once, whether the command returns
// normally or calls process.exit() inside its action. finalizeAndExit invokes
// this after telemetry shutdown (which prints the audit-mode line), so the
// tail output is never dropped by an early process.exit().
registerPostCommandFinalize(async () => {
// Telemetry notice already printed above; only run update check here.
await printPostCommandNotices(false, updateCheck);

const exitMessage = getExitMessage();
if (exitMessage) {
console.log(`\n${exitMessage}`);
clearExitMessage();
}
});

await program.parseAsync(argv);

const exitMessage = getExitMessage();
if (exitMessage) {
console.log(`\n${exitMessage}`);
clearExitMessage();
}
// Command returned without calling process.exit(); run the same finalize path.
await finalizeAndExit(0);
};
17 changes: 15 additions & 2 deletions src/cli/commands/config/command.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
import { COMMAND_DESCRIPTIONS } from '../../constants.js';
import { finalizeAndExit, withCommandRunTelemetry } from '../../telemetry/cli-command-run.js';
import type { CommandAttrs } from '../../telemetry/schemas/command-run.js';
import { handleConfigGet, handleConfigList, handleConfigSet } from './actions.js';
import type { ConfigResult } from './types.js';
import type { Command } from '@commander-js/extra-typings';
Expand All @@ -9,6 +11,13 @@ function resolveAction(key?: string, value?: string): () => Promise<ConfigResult
return () => handleConfigSet(key, value);
}

// key/value are never recorded (PII/secret risk); only the derived action verb.
function deriveConfigAction(key?: string, value?: string): CommandAttrs<'config'>['config_action'] {
if (!key) return 'list';
if (value === undefined) return 'get';
return 'set';
}

function printResult(result: ConfigResult): void {
if (result.success) {
console.log(result.message);
Expand All @@ -24,8 +33,12 @@ export function registerConfig(program: Command) {
.argument('[key]', 'Config key in dot notation (e.g. telemetry.enabled)')
.argument('[value]', 'Value to set')
.action(async (key?: string, value?: string) => {
const result = await resolveAction(key, value)();
const result = await withCommandRunTelemetry(
'config',
{ config_action: deriveConfigAction(key, value) },
() => resolveAction(key, value)()
);
printResult(result);
if (!result.success) process.exit(1);
await finalizeAndExit(result.success ? 0 : 1);
});
}
6 changes: 3 additions & 3 deletions src/cli/commands/fetch/command.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { COMMAND_DESCRIPTIONS } from '../../constants';
import { getErrorMessage } from '../../errors';
import { withCommandRunTelemetry } from '../../telemetry/cli-command-run.js';
import { finalizeAndExit, withCommandRunTelemetry } from '../../telemetry/cli-command-run.js';
import { ResourceType, standardize } from '../../telemetry/schemas/common-shapes.js';
import { requireProject } from '../../tui/guards';
import { handleFetchAccess } from './action';
Expand Down Expand Up @@ -48,7 +48,7 @@ export const registerFetch = (program: Command) => {
} else {
render(<Text color="red">Error: {getErrorMessage(error)}</Text>);
}
process.exit(1);
await finalizeAndExit(1);
return;
}

Expand Down Expand Up @@ -77,7 +77,7 @@ export const registerFetch = (program: Command) => {
</Box>
);
}
process.exit(1);
await finalizeAndExit(1);
return;
}

Expand Down
22 changes: 15 additions & 7 deletions src/cli/commands/pause/command.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import { getErrorMessage } from '../../errors';
import { handlePauseResume } from '../../operations/eval';
import type { OnlineEvalActionOptions } from '../../operations/eval';
import { createJobEngine } from '../../operations/jobs';
import { runCliCommand } from '../../telemetry/cli-command-run';
import { finalizeAndExit, runCliCommand, withCommandRunTelemetry } from '../../telemetry/cli-command-run';
import { COMMAND_DESCRIPTIONS } from '../../tui/copy';
import { requireProject } from '../../tui/guards';
import type { Command } from '@commander-js/extra-typings';
Expand Down Expand Up @@ -47,7 +47,11 @@ function registerOnlineEvalSubcommand(parent: Command, action: 'pause' | 'resume
};

try {
const result = await handlePauseResume(options, action);
const result = await withCommandRunTelemetry(
action === 'pause' ? 'pause.online-eval' : 'resume.online-eval',
{ ref_type: cliOptions.arn ? 'arn' : 'name' },
() => handlePauseResume(options, action)
);

if (cliOptions.json) {
console.log(JSON.stringify(serializeResult(result)));
Expand All @@ -58,14 +62,14 @@ function registerOnlineEvalSubcommand(parent: Command, action: 'pause' | 'resume
render(<Text color="red">{result.error.message}</Text>);
}

process.exit(result.success ? 0 : 1);
await finalizeAndExit(result.success ? 0 : 1);
} catch (error) {
if (cliOptions.json) {
console.log(JSON.stringify({ success: false, error: getErrorMessage(error) }));
} else {
render(<Text color="red">Error: {getErrorMessage(error)}</Text>);
}
process.exit(1);
await finalizeAndExit(1);
}
});
}
Expand Down Expand Up @@ -138,7 +142,11 @@ function registerOnlineInsightsSubcommand(parent: Command, action: 'pause' | 're
};

try {
const result = await handlePauseResume(options, action);
const result = await withCommandRunTelemetry(
action === 'pause' ? 'pause.online-insights' : 'resume.online-insights',
{ ref_type: cliOptions.arn ? 'arn' : 'name' },
() => handlePauseResume(options, action)
);

if (cliOptions.json) {
console.log(JSON.stringify(serializeResult(result)));
Expand All @@ -149,14 +157,14 @@ function registerOnlineInsightsSubcommand(parent: Command, action: 'pause' | 're
render(<Text color="red">{result.error.message}</Text>);
}

process.exit(result.success ? 0 : 1);
await finalizeAndExit(result.success ? 0 : 1);
} catch (error) {
if (cliOptions.json) {
console.log(JSON.stringify({ success: false, error: getErrorMessage(error) }));
} else {
render(<Text color="red">Error: {getErrorMessage(error)}</Text>);
}
process.exit(1);
await finalizeAndExit(1);
}
});
}
Expand Down
18 changes: 14 additions & 4 deletions src/cli/commands/run/command.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ import type {
StartBatchEvaluationJobOptions,
} from '../../operations/jobs';
import { printABTestDetail } from '../../operations/jobs/ab-test/format';
import { runCliCommand } from '../../telemetry/cli-command-run';
import { finalizeAndExit, runCliCommand, withCommandRunTelemetry } from '../../telemetry/cli-command-run';
import { requireProject } from '../../tui/guards';
import type { Command } from '@commander-js/extra-typings';
import { Text, render } from 'ink';
Expand Down Expand Up @@ -168,7 +168,17 @@ export const registerRun = (program: Command) => {
};

try {
const result = await handleRunEval(options);
const result = await withCommandRunTelemetry(
'run.eval',
{
evaluator_count: options.evaluator.length + (options.evaluatorArn?.length ?? 0),
ref_type: options.agentArn ? 'arn' : 'name',
has_assertions: !!options.assertions?.length,
has_expected_trajectory: !!options.expectedTrajectory?.length,
has_expected_response: !!options.expectedResponse,
},
() => handleRunEval(options)
);

if (cliOptions.json) {
console.log(JSON.stringify(serializeResult(result)));
Expand All @@ -179,14 +189,14 @@ export const registerRun = (program: Command) => {
render(<Text color="red">{result.error.message}</Text>);
}

process.exit(result.success ? 0 : 1);
await finalizeAndExit(result.success ? 0 : 1);
} catch (error) {
if (cliOptions.json) {
console.log(JSON.stringify({ success: false, error: getErrorMessage(error) }));
} else {
render(<Text color="red">Error: {getErrorMessage(error)}</Text>);
}
process.exit(1);
await finalizeAndExit(1);
}
}
);
Expand Down
21 changes: 13 additions & 8 deletions src/cli/commands/traces/command.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { COMMAND_DESCRIPTIONS } from '../../constants';
import { getErrorMessage } from '../../errors';
import { loadDeployedProjectConfig } from '../../operations/resolve-agent';
import { finalizeAndExit, withCommandRunTelemetry } from '../../telemetry/cli-command-run.js';
import { requireProject } from '../../tui/guards';
import { handleTracesGet, handleTracesList } from './action';
import type { TracesGetOptions, TracesListOptions } from './types';
Expand Down Expand Up @@ -31,8 +32,10 @@ export const registerTraces = (program: Command) => {
requireProject();

try {
const context = await loadDeployedProjectConfig();
const result = await handleTracesList(context, cliOptions);
const result = await withCommandRunTelemetry('traces.list', {}, async () => {
const context = await loadDeployedProjectConfig();
return handleTracesList(context, cliOptions);
});

if (!result.success) {
render(
Expand All @@ -41,7 +44,7 @@ export const registerTraces = (program: Command) => {
{result.consoleUrl && <Text color="gray">Console: {result.consoleUrl}</Text>}
</Box>
);
process.exit(1);
await finalizeAndExit(1);
return;
}

Expand Down Expand Up @@ -88,7 +91,7 @@ export const registerTraces = (program: Command) => {
);
} catch (error) {
render(<Text color="red">Error: {getErrorMessage(error)}</Text>);
process.exit(1);
await finalizeAndExit(1);
}
});

Expand All @@ -103,8 +106,10 @@ export const registerTraces = (program: Command) => {
requireProject();

try {
const context = await loadDeployedProjectConfig();
const result = await handleTracesGet(context, traceId, cliOptions);
const result = await withCommandRunTelemetry('traces.get', {}, async () => {
const context = await loadDeployedProjectConfig();
return handleTracesGet(context, traceId, cliOptions);
});

if (!result.success) {
render(
Expand All @@ -113,7 +118,7 @@ export const registerTraces = (program: Command) => {
{result.consoleUrl && <Text color="gray">Console: {result.consoleUrl}</Text>}
</Box>
);
process.exit(1);
await finalizeAndExit(1);
return;
}

Expand All @@ -125,7 +130,7 @@ export const registerTraces = (program: Command) => {
);
} catch (error) {
render(<Text color="red">Error: {getErrorMessage(error)}</Text>);
process.exit(1);
await finalizeAndExit(1);
}
});
};
38 changes: 37 additions & 1 deletion src/cli/telemetry/__tests__/client.test.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,12 @@
/* eslint-disable @typescript-eslint/require-await */
import { AccessDeniedError, DependencyCheckError } from '../../../lib/errors/types';
import { withCommandRunTelemetry } from '../cli-command-run';
import { finalizeAndExit, registerPostCommandFinalize, withCommandRunTelemetry } from '../cli-command-run';
import { TelemetryClient } from '../client';
import { TelemetryClientAccessor } from '../client-accessor';
import { FileSystemSink } from '../sinks/filesystem-sink';
import { InMemorySink } from '../sinks/in-memory-sink';
import { tmpdir } from 'node:os';
import { join } from 'node:path';
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';

let sink: InMemorySink;
Expand Down Expand Up @@ -300,3 +303,36 @@ describe('withCommandRunTelemetry', () => {
});
});
});

describe('finalizeAndExit', () => {
afterEach(() => {
registerPostCommandFinalize(async () => undefined);
vi.restoreAllMocks();
});

it('flushes the audit sink and runs post-command notices before process.exit', async () => {
// process.exit is mocked to throw so the test runner survives the exit-path call.
const exit = vi.spyOn(process, 'exit').mockImplementation((() => {
throw new Error('exit');
}) as never);

const logged: string[] = [];
const auditSink = new FileSystemSink({
filePath: join(tmpdir(), 'finalize-audit-test.jsonl'),
log: msg => logged.push(msg),
});
auditSink.record('cli.command_run', 1, { command: 'config', exit_reason: 'success' });
// shutdown() is the only path that emits the audit-mode line; route the accessor through it.
vi.spyOn(TelemetryClientAccessor, 'shutdown').mockImplementation(() => new TelemetryClient(auditSink).shutdown());

const notices = vi.fn().mockResolvedValue(undefined);
registerPostCommandFinalize(notices);

await expect(finalizeAndExit(0)).rejects.toThrow('exit');

expect(logged).toHaveLength(1);
expect(logged[0]).toContain('[audit mode]');
expect(notices).toHaveBeenCalledOnce();
expect(exit).toHaveBeenCalledWith(0);
});
});
Loading
Loading