mirror of
https://github.com/google-gemini/gemini-cli.git
synced 2026-05-01 07:24:38 -07:00
Respect logPrompts flag for logging sensitive fields (#26153)
Co-authored-by: David Pierce <davidapierce@google.com> Co-authored-by: Gal Zahavi <38544478+galz10@users.noreply.github.com>
This commit is contained in:
@@ -19,6 +19,7 @@ import {
|
|||||||
import type { Config } from '../config/config.js';
|
import type { Config } from '../config/config.js';
|
||||||
import * as sdk from './sdk.js';
|
import * as sdk from './sdk.js';
|
||||||
import { ClearcutLogger } from './clearcut-logger/clearcut-logger.js';
|
import { ClearcutLogger } from './clearcut-logger/clearcut-logger.js';
|
||||||
|
import { EventMetadataKey } from './clearcut-logger/event-metadata-key.js';
|
||||||
|
|
||||||
vi.mock('@opentelemetry/api-logs');
|
vi.mock('@opentelemetry/api-logs');
|
||||||
vi.mock('./sdk.js');
|
vi.mock('./sdk.js');
|
||||||
@@ -144,4 +145,174 @@ describe('conseca-logger', () => {
|
|||||||
|
|
||||||
expect(mockLogger.emit).not.toHaveBeenCalled();
|
expect(mockLogger.emit).not.toHaveBeenCalled();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('should omit user_prompt/trusted_content/policy from OTEL when logPrompts is disabled', () => {
|
||||||
|
const configNoPrompts = {
|
||||||
|
getTelemetryEnabled: vi.fn().mockReturnValue(true),
|
||||||
|
getSessionId: vi.fn().mockReturnValue('test-session-id'),
|
||||||
|
getTelemetryLogPromptsEnabled: vi.fn().mockReturnValue(false),
|
||||||
|
getTelemetryTracesEnabled: vi.fn().mockReturnValue(false),
|
||||||
|
isInteractive: vi.fn().mockReturnValue(true),
|
||||||
|
getExperiments: vi.fn().mockReturnValue({ experimentIds: [] }),
|
||||||
|
getContentGeneratorConfig: vi.fn().mockReturnValue({ authType: 'oauth' }),
|
||||||
|
} as unknown as Config;
|
||||||
|
|
||||||
|
const event = new ConsecaPolicyGenerationEvent(
|
||||||
|
'sensitive prompt',
|
||||||
|
'sensitive content',
|
||||||
|
'sensitive policy',
|
||||||
|
);
|
||||||
|
|
||||||
|
logConsecaPolicyGeneration(configNoPrompts, event);
|
||||||
|
|
||||||
|
const attrs = mockLogger.emit.mock.calls[0][0].attributes as Record<
|
||||||
|
string,
|
||||||
|
unknown
|
||||||
|
>;
|
||||||
|
expect(attrs['user_prompt']).toBeUndefined();
|
||||||
|
expect(attrs['trusted_content']).toBeUndefined();
|
||||||
|
expect(attrs['policy']).toBeUndefined();
|
||||||
|
expect(attrs['event.name']).toBe(EVENT_CONSECA_POLICY_GENERATION);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should omit user_prompt/trusted_content/policy from Clearcut when logPrompts is disabled', () => {
|
||||||
|
const configNoPrompts = {
|
||||||
|
getTelemetryEnabled: vi.fn().mockReturnValue(true),
|
||||||
|
getSessionId: vi.fn().mockReturnValue('test-session-id'),
|
||||||
|
getTelemetryLogPromptsEnabled: vi.fn().mockReturnValue(false),
|
||||||
|
getTelemetryTracesEnabled: vi.fn().mockReturnValue(false),
|
||||||
|
isInteractive: vi.fn().mockReturnValue(true),
|
||||||
|
getExperiments: vi.fn().mockReturnValue({ experimentIds: [] }),
|
||||||
|
getContentGeneratorConfig: vi.fn().mockReturnValue({ authType: 'oauth' }),
|
||||||
|
} as unknown as Config;
|
||||||
|
|
||||||
|
const event = new ConsecaPolicyGenerationEvent(
|
||||||
|
'sensitive prompt',
|
||||||
|
'sensitive content',
|
||||||
|
'sensitive policy',
|
||||||
|
'some error',
|
||||||
|
);
|
||||||
|
|
||||||
|
logConsecaPolicyGeneration(configNoPrompts, event);
|
||||||
|
|
||||||
|
expect(mockClearcutLogger.createLogEvent).toHaveBeenCalledWith(
|
||||||
|
expect.anything(),
|
||||||
|
[
|
||||||
|
{
|
||||||
|
gemini_cli_key: EventMetadataKey.CONSECA_ERROR,
|
||||||
|
value: 'some error',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should include user_prompt/trusted_content/policy in OTEL when logPrompts is enabled', () => {
|
||||||
|
const event = new ConsecaPolicyGenerationEvent(
|
||||||
|
'visible prompt',
|
||||||
|
'visible content',
|
||||||
|
'visible policy',
|
||||||
|
);
|
||||||
|
|
||||||
|
logConsecaPolicyGeneration(mockConfig, event);
|
||||||
|
|
||||||
|
const attrs = mockLogger.emit.mock.calls[0][0].attributes as Record<
|
||||||
|
string,
|
||||||
|
unknown
|
||||||
|
>;
|
||||||
|
expect(attrs['user_prompt']).toBe('visible prompt');
|
||||||
|
expect(attrs['trusted_content']).toBe('visible content');
|
||||||
|
expect(attrs['policy']).toBe('visible policy');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should omit sensitive fields from verdict OTEL when logPrompts is disabled', () => {
|
||||||
|
const configNoPrompts = {
|
||||||
|
getTelemetryEnabled: vi.fn().mockReturnValue(true),
|
||||||
|
getSessionId: vi.fn().mockReturnValue('test-session-id'),
|
||||||
|
getTelemetryLogPromptsEnabled: vi.fn().mockReturnValue(false),
|
||||||
|
getTelemetryTracesEnabled: vi.fn().mockReturnValue(false),
|
||||||
|
isInteractive: vi.fn().mockReturnValue(true),
|
||||||
|
getExperiments: vi.fn().mockReturnValue({ experimentIds: [] }),
|
||||||
|
getContentGeneratorConfig: vi.fn().mockReturnValue({ authType: 'oauth' }),
|
||||||
|
} as unknown as Config;
|
||||||
|
|
||||||
|
const event = new ConsecaVerdictEvent(
|
||||||
|
'sensitive prompt',
|
||||||
|
'sensitive policy',
|
||||||
|
'sensitive tool call',
|
||||||
|
'allow',
|
||||||
|
'sensitive rationale',
|
||||||
|
);
|
||||||
|
|
||||||
|
logConsecaVerdict(configNoPrompts, event);
|
||||||
|
|
||||||
|
const attrs = mockLogger.emit.mock.calls[0][0].attributes as Record<
|
||||||
|
string,
|
||||||
|
unknown
|
||||||
|
>;
|
||||||
|
expect(attrs['user_prompt']).toBeUndefined();
|
||||||
|
expect(attrs['policy']).toBeUndefined();
|
||||||
|
expect(attrs['tool_call']).toBeUndefined();
|
||||||
|
expect(attrs['verdict_rationale']).toBeUndefined();
|
||||||
|
// verdict (the allow/deny result) is not sensitive and should be present
|
||||||
|
expect(attrs['verdict']).toBe('allow');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should omit sensitive fields from verdict Clearcut when logPrompts is disabled', () => {
|
||||||
|
const configNoPrompts = {
|
||||||
|
getTelemetryEnabled: vi.fn().mockReturnValue(true),
|
||||||
|
getSessionId: vi.fn().mockReturnValue('test-session-id'),
|
||||||
|
getTelemetryLogPromptsEnabled: vi.fn().mockReturnValue(false),
|
||||||
|
getTelemetryTracesEnabled: vi.fn().mockReturnValue(false),
|
||||||
|
isInteractive: vi.fn().mockReturnValue(true),
|
||||||
|
getExperiments: vi.fn().mockReturnValue({ experimentIds: [] }),
|
||||||
|
getContentGeneratorConfig: vi.fn().mockReturnValue({ authType: 'oauth' }),
|
||||||
|
} as unknown as Config;
|
||||||
|
|
||||||
|
const event = new ConsecaVerdictEvent(
|
||||||
|
'sensitive prompt',
|
||||||
|
'sensitive policy',
|
||||||
|
'sensitive tool call',
|
||||||
|
'allow',
|
||||||
|
'sensitive rationale',
|
||||||
|
'some error',
|
||||||
|
);
|
||||||
|
|
||||||
|
logConsecaVerdict(configNoPrompts, event);
|
||||||
|
|
||||||
|
expect(mockClearcutLogger.createLogEvent).toHaveBeenCalledWith(
|
||||||
|
expect.anything(),
|
||||||
|
[
|
||||||
|
{
|
||||||
|
gemini_cli_key: EventMetadataKey.CONSECA_VERDICT_RESULT,
|
||||||
|
value: '"allow"',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
gemini_cli_key: EventMetadataKey.CONSECA_ERROR,
|
||||||
|
value: 'some error',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should include sensitive fields in verdict OTEL when logPrompts is enabled', () => {
|
||||||
|
const event = new ConsecaVerdictEvent(
|
||||||
|
'visible prompt',
|
||||||
|
'visible policy',
|
||||||
|
'visible tool call',
|
||||||
|
'deny',
|
||||||
|
'visible rationale',
|
||||||
|
);
|
||||||
|
|
||||||
|
logConsecaVerdict(mockConfig, event);
|
||||||
|
|
||||||
|
const attrs = mockLogger.emit.mock.calls[0][0].attributes as Record<
|
||||||
|
string,
|
||||||
|
unknown
|
||||||
|
>;
|
||||||
|
expect(attrs['user_prompt']).toBe('visible prompt');
|
||||||
|
expect(attrs['policy']).toBe('visible policy');
|
||||||
|
expect(attrs['tool_call']).toBe('visible tool call');
|
||||||
|
expect(attrs['verdict_rationale']).toBe('visible rationale');
|
||||||
|
expect(attrs['verdict']).toBe('deny');
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ import { isTelemetrySdkInitialized } from './sdk.js';
|
|||||||
import {
|
import {
|
||||||
ClearcutLogger,
|
ClearcutLogger,
|
||||||
EventNames,
|
EventNames,
|
||||||
|
type EventValue,
|
||||||
} from './clearcut-logger/clearcut-logger.js';
|
} from './clearcut-logger/clearcut-logger.js';
|
||||||
import { EventMetadataKey } from './clearcut-logger/event-metadata-key.js';
|
import { EventMetadataKey } from './clearcut-logger/event-metadata-key.js';
|
||||||
import { safeJsonStringify } from '../utils/safeJsonStringify.js';
|
import { safeJsonStringify } from '../utils/safeJsonStringify.js';
|
||||||
@@ -27,20 +28,24 @@ export function logConsecaPolicyGeneration(
|
|||||||
debugLogger.debug('Conseca Policy Generation Event:', event);
|
debugLogger.debug('Conseca Policy Generation Event:', event);
|
||||||
const clearcutLogger = ClearcutLogger.getInstance(config);
|
const clearcutLogger = ClearcutLogger.getInstance(config);
|
||||||
if (clearcutLogger) {
|
if (clearcutLogger) {
|
||||||
const data = [
|
const data: EventValue[] = [];
|
||||||
{
|
|
||||||
gemini_cli_key: EventMetadataKey.CONSECA_USER_PROMPT,
|
if (config.getTelemetryLogPromptsEnabled()) {
|
||||||
value: safeJsonStringify(event.user_prompt),
|
data.push(
|
||||||
},
|
{
|
||||||
{
|
gemini_cli_key: EventMetadataKey.CONSECA_USER_PROMPT,
|
||||||
gemini_cli_key: EventMetadataKey.CONSECA_TRUSTED_CONTENT,
|
value: safeJsonStringify(event.user_prompt),
|
||||||
value: safeJsonStringify(event.trusted_content),
|
},
|
||||||
},
|
{
|
||||||
{
|
gemini_cli_key: EventMetadataKey.CONSECA_TRUSTED_CONTENT,
|
||||||
gemini_cli_key: EventMetadataKey.CONSECA_GENERATED_POLICY,
|
value: safeJsonStringify(event.trusted_content),
|
||||||
value: safeJsonStringify(event.policy),
|
},
|
||||||
},
|
{
|
||||||
];
|
gemini_cli_key: EventMetadataKey.CONSECA_GENERATED_POLICY,
|
||||||
|
value: safeJsonStringify(event.policy),
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
if (event.error) {
|
if (event.error) {
|
||||||
data.push({
|
data.push({
|
||||||
@@ -71,29 +76,34 @@ export function logConsecaVerdict(
|
|||||||
debugLogger.debug('Conseca Verdict Event:', event);
|
debugLogger.debug('Conseca Verdict Event:', event);
|
||||||
const clearcutLogger = ClearcutLogger.getInstance(config);
|
const clearcutLogger = ClearcutLogger.getInstance(config);
|
||||||
if (clearcutLogger) {
|
if (clearcutLogger) {
|
||||||
const data = [
|
const data: EventValue[] = [
|
||||||
{
|
|
||||||
gemini_cli_key: EventMetadataKey.CONSECA_USER_PROMPT,
|
|
||||||
value: safeJsonStringify(event.user_prompt),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
gemini_cli_key: EventMetadataKey.CONSECA_GENERATED_POLICY,
|
|
||||||
value: safeJsonStringify(event.policy),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
gemini_cli_key: EventMetadataKey.GEMINI_CLI_TOOL_CALL_NAME,
|
|
||||||
value: safeJsonStringify(event.tool_call),
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
gemini_cli_key: EventMetadataKey.CONSECA_VERDICT_RESULT,
|
gemini_cli_key: EventMetadataKey.CONSECA_VERDICT_RESULT,
|
||||||
value: safeJsonStringify(event.verdict),
|
value: safeJsonStringify(event.verdict),
|
||||||
},
|
},
|
||||||
{
|
|
||||||
gemini_cli_key: EventMetadataKey.CONSECA_VERDICT_RATIONALE,
|
|
||||||
value: event.verdict_rationale,
|
|
||||||
},
|
|
||||||
];
|
];
|
||||||
|
|
||||||
|
if (config.getTelemetryLogPromptsEnabled()) {
|
||||||
|
data.push(
|
||||||
|
{
|
||||||
|
gemini_cli_key: EventMetadataKey.CONSECA_USER_PROMPT,
|
||||||
|
value: safeJsonStringify(event.user_prompt),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
gemini_cli_key: EventMetadataKey.CONSECA_GENERATED_POLICY,
|
||||||
|
value: safeJsonStringify(event.policy),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
gemini_cli_key: EventMetadataKey.GEMINI_CLI_TOOL_CALL_NAME,
|
||||||
|
value: safeJsonStringify(event.tool_call),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
gemini_cli_key: EventMetadataKey.CONSECA_VERDICT_RATIONALE,
|
||||||
|
value: event.verdict_rationale,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
if (event.error) {
|
if (event.error) {
|
||||||
data.push({
|
data.push({
|
||||||
gemini_cli_key: EventMetadataKey.CONSECA_ERROR,
|
gemini_cli_key: EventMetadataKey.CONSECA_ERROR,
|
||||||
|
|||||||
@@ -642,6 +642,54 @@ describe('loggers', () => {
|
|||||||
}),
|
}),
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
it('should not include response_text when logPrompts is disabled', () => {
|
||||||
|
const mockConfigNoPrompts = {
|
||||||
|
getSessionId: () => 'test-session-id',
|
||||||
|
getTargetDir: () => 'target-dir',
|
||||||
|
getUsageStatisticsEnabled: () => true,
|
||||||
|
getTelemetryEnabled: () => true,
|
||||||
|
getTelemetryLogPromptsEnabled: () => false,
|
||||||
|
getTelemetryTracesEnabled: () => false,
|
||||||
|
isInteractive: () => false,
|
||||||
|
getExperiments: () => undefined,
|
||||||
|
getExperimentsAsync: async () => undefined,
|
||||||
|
getContentGeneratorConfig: () => undefined,
|
||||||
|
} as unknown as Config;
|
||||||
|
|
||||||
|
const event = new ApiResponseEvent(
|
||||||
|
'test-model',
|
||||||
|
100,
|
||||||
|
{ prompt_id: 'prompt-id-noprompts', contents: [] },
|
||||||
|
{ candidates: [] },
|
||||||
|
AuthType.LOGIN_WITH_GOOGLE,
|
||||||
|
{},
|
||||||
|
'this response should be hidden',
|
||||||
|
);
|
||||||
|
|
||||||
|
logApiResponse(mockConfigNoPrompts, event);
|
||||||
|
|
||||||
|
const firstEmitCall = mockLogger.emit.mock.calls[0][0];
|
||||||
|
expect(firstEmitCall.attributes['response_text']).toBeUndefined();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should include response_text when logPrompts is enabled', () => {
|
||||||
|
const event = new ApiResponseEvent(
|
||||||
|
'test-model',
|
||||||
|
100,
|
||||||
|
{ prompt_id: 'prompt-id-withprompts', contents: [] },
|
||||||
|
{ candidates: [] },
|
||||||
|
AuthType.LOGIN_WITH_GOOGLE,
|
||||||
|
{},
|
||||||
|
'this response should be visible',
|
||||||
|
);
|
||||||
|
|
||||||
|
logApiResponse(mockConfig, event);
|
||||||
|
|
||||||
|
const firstEmitCall = mockLogger.emit.mock.calls[0][0];
|
||||||
|
expect(firstEmitCall.attributes['response_text']).toBe(
|
||||||
|
'this response should be visible',
|
||||||
|
);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('logApiError', () => {
|
describe('logApiError', () => {
|
||||||
@@ -1076,6 +1124,10 @@ describe('loggers', () => {
|
|||||||
expect(attributes['gen_ai.provider.name']).toBe('gcp.vertex_ai');
|
expect(attributes['gen_ai.provider.name']).toBe('gcp.vertex_ai');
|
||||||
// Ensure prompt messages are NOT included
|
// Ensure prompt messages are NOT included
|
||||||
expect(attributes['gen_ai.input.messages']).toBeUndefined();
|
expect(attributes['gen_ai.input.messages']).toBeUndefined();
|
||||||
|
|
||||||
|
// Ensure request_text is also NOT included in the first (toLogRecord) log
|
||||||
|
const firstLogCall = mockLogger.emit.mock.calls[0][0];
|
||||||
|
expect(firstLogCall.attributes['request_text']).toBeUndefined();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should correctly derive model from prompt details if available in semantic log', () => {
|
it('should correctly derive model from prompt details if available in semantic log', () => {
|
||||||
@@ -1373,16 +1425,20 @@ describe('loggers', () => {
|
|||||||
error_type: undefined,
|
error_type: undefined,
|
||||||
mcp_server_name: undefined,
|
mcp_server_name: undefined,
|
||||||
extension_id: undefined,
|
extension_id: undefined,
|
||||||
metadata: {
|
metadata: JSON.stringify(
|
||||||
model_added_lines: 1,
|
{
|
||||||
model_removed_lines: 2,
|
model_added_lines: 1,
|
||||||
model_added_chars: 3,
|
model_removed_lines: 2,
|
||||||
model_removed_chars: 4,
|
model_added_chars: 3,
|
||||||
user_added_lines: 5,
|
model_removed_chars: 4,
|
||||||
user_removed_lines: 6,
|
user_added_lines: 5,
|
||||||
user_added_chars: 7,
|
user_removed_lines: 6,
|
||||||
user_removed_chars: 8,
|
user_added_chars: 7,
|
||||||
},
|
user_removed_chars: 8,
|
||||||
|
},
|
||||||
|
null,
|
||||||
|
2,
|
||||||
|
),
|
||||||
content_length: 13,
|
content_length: 13,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
@@ -1455,12 +1511,16 @@ describe('loggers', () => {
|
|||||||
body: 'Tool call: ask_user. Decision: accept. Success: true. Duration: 100ms.',
|
body: 'Tool call: ask_user. Decision: accept. Success: true. Duration: 100ms.',
|
||||||
attributes: expect.objectContaining({
|
attributes: expect.objectContaining({
|
||||||
function_name: 'ask_user',
|
function_name: 'ask_user',
|
||||||
metadata: expect.objectContaining({
|
metadata: JSON.stringify(
|
||||||
ask_user: {
|
{
|
||||||
question_types: ['choice'],
|
ask_user: {
|
||||||
dismissed: false,
|
question_types: ['choice'],
|
||||||
|
dismissed: false,
|
||||||
|
},
|
||||||
},
|
},
|
||||||
}),
|
null,
|
||||||
|
2,
|
||||||
|
),
|
||||||
}),
|
}),
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@@ -1867,6 +1927,99 @@ describe('loggers', () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('logToolCall — logPrompts flag', () => {
|
||||||
|
it('should omit function_args when logPrompts is disabled', () => {
|
||||||
|
const mockConfigNoPrompts = {
|
||||||
|
getSessionId: () => 'test-session-id',
|
||||||
|
getTargetDir: () => 'target-dir',
|
||||||
|
getUsageStatisticsEnabled: () => true,
|
||||||
|
getTelemetryEnabled: () => true,
|
||||||
|
getTelemetryLogPromptsEnabled: () => false,
|
||||||
|
getTelemetryTracesEnabled: () => false,
|
||||||
|
isInteractive: () => false,
|
||||||
|
getExperiments: () => undefined,
|
||||||
|
getExperimentsAsync: async () => undefined,
|
||||||
|
getContentGeneratorConfig: () => undefined,
|
||||||
|
} as unknown as Config;
|
||||||
|
|
||||||
|
const call: CompletedToolCall = {
|
||||||
|
status: CoreToolCallStatus.Success,
|
||||||
|
request: {
|
||||||
|
name: 'run_bash',
|
||||||
|
args: { command: 'echo sensitive' },
|
||||||
|
callId: 'call-1',
|
||||||
|
isClientInitiated: false,
|
||||||
|
prompt_id: 'prompt-noprompts',
|
||||||
|
},
|
||||||
|
response: {
|
||||||
|
callId: 'call-1',
|
||||||
|
responseParts: [],
|
||||||
|
resultDisplay: undefined,
|
||||||
|
error: undefined,
|
||||||
|
errorType: undefined,
|
||||||
|
contentLength: undefined,
|
||||||
|
},
|
||||||
|
tool: undefined as unknown as AnyDeclarativeTool,
|
||||||
|
invocation: {} as AnyToolInvocation,
|
||||||
|
durationMs: 50,
|
||||||
|
};
|
||||||
|
const event = new ToolCallEvent(call);
|
||||||
|
logToolCall(mockConfigNoPrompts, event);
|
||||||
|
|
||||||
|
const emitted = mockLogger.emit.mock.calls[0][0] as {
|
||||||
|
attributes: Record<string, unknown>;
|
||||||
|
};
|
||||||
|
expect(emitted.attributes['function_args']).toBeUndefined();
|
||||||
|
expect(emitted.attributes['function_name']).toBe('run_bash');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should include function_args when logPrompts is enabled', () => {
|
||||||
|
const mockConfigWithPrompts = {
|
||||||
|
getSessionId: () => 'test-session-id',
|
||||||
|
getTargetDir: () => 'target-dir',
|
||||||
|
getUsageStatisticsEnabled: () => true,
|
||||||
|
getTelemetryEnabled: () => true,
|
||||||
|
getTelemetryLogPromptsEnabled: () => true,
|
||||||
|
getTelemetryTracesEnabled: () => false,
|
||||||
|
isInteractive: () => false,
|
||||||
|
getExperiments: () => undefined,
|
||||||
|
getExperimentsAsync: async () => undefined,
|
||||||
|
getContentGeneratorConfig: () => undefined,
|
||||||
|
} as unknown as Config;
|
||||||
|
|
||||||
|
const call: CompletedToolCall = {
|
||||||
|
status: CoreToolCallStatus.Success,
|
||||||
|
request: {
|
||||||
|
name: 'run_bash',
|
||||||
|
args: { command: 'echo visible' },
|
||||||
|
callId: 'call-2',
|
||||||
|
isClientInitiated: false,
|
||||||
|
prompt_id: 'prompt-withprompts',
|
||||||
|
},
|
||||||
|
response: {
|
||||||
|
callId: 'call-2',
|
||||||
|
responseParts: [],
|
||||||
|
resultDisplay: undefined,
|
||||||
|
error: undefined,
|
||||||
|
errorType: undefined,
|
||||||
|
contentLength: undefined,
|
||||||
|
},
|
||||||
|
tool: undefined as unknown as AnyDeclarativeTool,
|
||||||
|
invocation: {} as AnyToolInvocation,
|
||||||
|
durationMs: 50,
|
||||||
|
};
|
||||||
|
const event = new ToolCallEvent(call);
|
||||||
|
logToolCall(mockConfigWithPrompts, event);
|
||||||
|
|
||||||
|
const emitted = mockLogger.emit.mock.calls[0][0] as {
|
||||||
|
attributes: Record<string, unknown>;
|
||||||
|
};
|
||||||
|
expect(emitted.attributes['function_args']).toBe(
|
||||||
|
JSON.stringify({ command: 'echo visible' }, null, 2),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
describe('logMalformedJsonResponse', () => {
|
describe('logMalformedJsonResponse', () => {
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
vi.spyOn(ClearcutLogger.prototype, 'logMalformedJsonResponseEvent');
|
vi.spyOn(ClearcutLogger.prototype, 'logMalformedJsonResponseEvent');
|
||||||
|
|||||||
@@ -231,6 +231,17 @@ export class UserPromptEvent implements BaseTelemetryEvent {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export const EVENT_TOOL_CALL = 'gemini_cli.tool_call';
|
export const EVENT_TOOL_CALL = 'gemini_cli.tool_call';
|
||||||
|
|
||||||
|
const TOOL_CALL_METADATA_SAFE_KEYS = [
|
||||||
|
'model_added_lines',
|
||||||
|
'model_removed_lines',
|
||||||
|
'model_added_chars',
|
||||||
|
'model_removed_chars',
|
||||||
|
'user_added_lines',
|
||||||
|
'user_removed_lines',
|
||||||
|
'user_added_chars',
|
||||||
|
'user_removed_chars',
|
||||||
|
] as const;
|
||||||
export class ToolCallEvent implements BaseTelemetryEvent {
|
export class ToolCallEvent implements BaseTelemetryEvent {
|
||||||
'event.name': 'tool_call';
|
'event.name': 'tool_call';
|
||||||
'event.timestamp': string;
|
'event.timestamp': string;
|
||||||
@@ -355,7 +366,6 @@ export class ToolCallEvent implements BaseTelemetryEvent {
|
|||||||
'event.name': EVENT_TOOL_CALL,
|
'event.name': EVENT_TOOL_CALL,
|
||||||
'event.timestamp': this['event.timestamp'],
|
'event.timestamp': this['event.timestamp'],
|
||||||
function_name: this.function_name,
|
function_name: this.function_name,
|
||||||
function_args: safeJsonStringify(this.function_args, 2),
|
|
||||||
duration_ms: this.duration_ms,
|
duration_ms: this.duration_ms,
|
||||||
success: this.success,
|
success: this.success,
|
||||||
decision: this.decision,
|
decision: this.decision,
|
||||||
@@ -367,8 +377,22 @@ export class ToolCallEvent implements BaseTelemetryEvent {
|
|||||||
extension_id: this.extension_id,
|
extension_id: this.extension_id,
|
||||||
start_time: this.start_time,
|
start_time: this.start_time,
|
||||||
end_time: this.end_time,
|
end_time: this.end_time,
|
||||||
metadata: this.metadata,
|
|
||||||
};
|
};
|
||||||
|
if (config.getTelemetryLogPromptsEnabled() && this.function_args) {
|
||||||
|
attributes['function_args'] = safeJsonStringify(this.function_args, 2);
|
||||||
|
}
|
||||||
|
if (this.metadata) {
|
||||||
|
const metadata = config.getTelemetryLogPromptsEnabled()
|
||||||
|
? this.metadata
|
||||||
|
: Object.fromEntries(
|
||||||
|
Object.entries(this.metadata).filter(([k]) =>
|
||||||
|
(TOOL_CALL_METADATA_SAFE_KEYS as readonly string[]).includes(k),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
if (Object.keys(metadata).length > 0) {
|
||||||
|
attributes['metadata'] = safeJsonStringify(metadata, 2);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (this.error) {
|
if (this.error) {
|
||||||
attributes[CoreToolCallStatus.Error] = this.error;
|
attributes[CoreToolCallStatus.Error] = this.error;
|
||||||
@@ -423,8 +447,10 @@ export class ApiRequestEvent implements BaseTelemetryEvent {
|
|||||||
'event.timestamp': this['event.timestamp'],
|
'event.timestamp': this['event.timestamp'],
|
||||||
model: this.model,
|
model: this.model,
|
||||||
prompt_id: this.prompt.prompt_id,
|
prompt_id: this.prompt.prompt_id,
|
||||||
request_text: this.request_text,
|
|
||||||
};
|
};
|
||||||
|
if (config.getTelemetryLogPromptsEnabled() && this.request_text) {
|
||||||
|
attributes['request_text'] = this.request_text;
|
||||||
|
}
|
||||||
if (this.role) {
|
if (this.role) {
|
||||||
attributes['role'] = this.role;
|
attributes['role'] = this.role;
|
||||||
}
|
}
|
||||||
@@ -692,7 +718,7 @@ export class ApiResponseEvent implements BaseTelemetryEvent {
|
|||||||
if (this.role) {
|
if (this.role) {
|
||||||
attributes['role'] = this.role;
|
attributes['role'] = this.role;
|
||||||
}
|
}
|
||||||
if (this.response_text) {
|
if (config.getTelemetryLogPromptsEnabled() && this.response_text) {
|
||||||
attributes['response_text'] = this.response_text;
|
attributes['response_text'] = this.response_text;
|
||||||
}
|
}
|
||||||
if (this.status_code) {
|
if (this.status_code) {
|
||||||
@@ -954,11 +980,20 @@ export class ConsecaPolicyGenerationEvent implements BaseTelemetryEvent {
|
|||||||
...getCommonAttributes(config),
|
...getCommonAttributes(config),
|
||||||
'event.name': EVENT_CONSECA_POLICY_GENERATION,
|
'event.name': EVENT_CONSECA_POLICY_GENERATION,
|
||||||
'event.timestamp': this['event.timestamp'],
|
'event.timestamp': this['event.timestamp'],
|
||||||
user_prompt: this.user_prompt,
|
|
||||||
trusted_content: this.trusted_content,
|
|
||||||
policy: this.policy,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
|
if (config.getTelemetryLogPromptsEnabled()) {
|
||||||
|
if (this.user_prompt) {
|
||||||
|
attributes['user_prompt'] = this.user_prompt;
|
||||||
|
}
|
||||||
|
if (this.trusted_content) {
|
||||||
|
attributes['trusted_content'] = this.trusted_content;
|
||||||
|
}
|
||||||
|
if (this.policy) {
|
||||||
|
attributes['policy'] = this.policy;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (this.error) {
|
if (this.error) {
|
||||||
attributes['error'] = this.error;
|
attributes['error'] = this.error;
|
||||||
}
|
}
|
||||||
@@ -1005,13 +1040,24 @@ export class ConsecaVerdictEvent implements BaseTelemetryEvent {
|
|||||||
...getCommonAttributes(config),
|
...getCommonAttributes(config),
|
||||||
'event.name': EVENT_CONSECA_VERDICT,
|
'event.name': EVENT_CONSECA_VERDICT,
|
||||||
'event.timestamp': this['event.timestamp'],
|
'event.timestamp': this['event.timestamp'],
|
||||||
user_prompt: this.user_prompt,
|
|
||||||
policy: this.policy,
|
|
||||||
tool_call: this.tool_call,
|
|
||||||
verdict: this.verdict,
|
verdict: this.verdict,
|
||||||
verdict_rationale: this.verdict_rationale,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
|
if (config.getTelemetryLogPromptsEnabled()) {
|
||||||
|
if (this.user_prompt) {
|
||||||
|
attributes['user_prompt'] = this.user_prompt;
|
||||||
|
}
|
||||||
|
if (this.policy) {
|
||||||
|
attributes['policy'] = this.policy;
|
||||||
|
}
|
||||||
|
if (this.tool_call) {
|
||||||
|
attributes['tool_call'] = this.tool_call;
|
||||||
|
}
|
||||||
|
if (this.verdict_rationale) {
|
||||||
|
attributes['verdict_rationale'] = this.verdict_rationale;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (this.error) {
|
if (this.error) {
|
||||||
attributes['error'] = this.error;
|
attributes['error'] = this.error;
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user