Log all session metadata (#6423)

This commit is contained in:
owenofbrien 2025-08-19 15:06:00 -05:00 committed by GitHub
parent b9cf1ea3ce
commit b561d3bbed
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
6 changed files with 108 additions and 112 deletions

View File

@ -55,6 +55,11 @@ const mockConfig = {
getApprovalMode: vi.fn(() => ApprovalMode.DEFAULT), getApprovalMode: vi.fn(() => ApprovalMode.DEFAULT),
getUsageStatisticsEnabled: () => true, getUsageStatisticsEnabled: () => true,
getDebugMode: () => false, getDebugMode: () => false,
getSessionId: () => 'test-session-id',
getContentGeneratorConfig: () => ({
model: 'test-model',
authType: 'oauth-personal',
}),
}; };
class MockToolInvocation extends BaseToolInvocation<object, ToolResult> { class MockToolInvocation extends BaseToolInvocation<object, ToolResult> {

View File

@ -53,6 +53,10 @@ describe('CoreToolScheduler', () => {
getUsageStatisticsEnabled: () => true, getUsageStatisticsEnabled: () => true,
getDebugMode: () => false, getDebugMode: () => false,
getApprovalMode: () => ApprovalMode.DEFAULT, getApprovalMode: () => ApprovalMode.DEFAULT,
getContentGeneratorConfig: () => ({
model: 'test-model',
authType: 'oauth-personal',
}),
} as unknown as Config; } as unknown as Config;
const scheduler = new CoreToolScheduler({ const scheduler = new CoreToolScheduler({
@ -109,6 +113,10 @@ describe('CoreToolScheduler with payload', () => {
getUsageStatisticsEnabled: () => true, getUsageStatisticsEnabled: () => true,
getDebugMode: () => false, getDebugMode: () => false,
getApprovalMode: () => ApprovalMode.DEFAULT, getApprovalMode: () => ApprovalMode.DEFAULT,
getContentGeneratorConfig: () => ({
model: 'test-model',
authType: 'oauth-personal',
}),
} as unknown as Config; } as unknown as Config;
const scheduler = new CoreToolScheduler({ const scheduler = new CoreToolScheduler({
@ -405,6 +413,10 @@ describe('CoreToolScheduler edit cancellation', () => {
getUsageStatisticsEnabled: () => true, getUsageStatisticsEnabled: () => true,
getDebugMode: () => false, getDebugMode: () => false,
getApprovalMode: () => ApprovalMode.DEFAULT, getApprovalMode: () => ApprovalMode.DEFAULT,
getContentGeneratorConfig: () => ({
model: 'test-model',
authType: 'oauth-personal',
}),
} as unknown as Config; } as unknown as Config;
const scheduler = new CoreToolScheduler({ const scheduler = new CoreToolScheduler({
@ -493,6 +505,10 @@ describe('CoreToolScheduler YOLO mode', () => {
getUsageStatisticsEnabled: () => true, getUsageStatisticsEnabled: () => true,
getDebugMode: () => false, getDebugMode: () => false,
getApprovalMode: () => ApprovalMode.YOLO, getApprovalMode: () => ApprovalMode.YOLO,
getContentGeneratorConfig: () => ({
model: 'test-model',
authType: 'oauth-personal',
}),
} as unknown as Config; } as unknown as Config;
const scheduler = new CoreToolScheduler({ const scheduler = new CoreToolScheduler({
@ -578,6 +594,10 @@ describe('CoreToolScheduler request queueing', () => {
getUsageStatisticsEnabled: () => true, getUsageStatisticsEnabled: () => true,
getDebugMode: () => false, getDebugMode: () => false,
getApprovalMode: () => ApprovalMode.YOLO, // Use YOLO to avoid confirmation prompts getApprovalMode: () => ApprovalMode.YOLO, // Use YOLO to avoid confirmation prompts
getContentGeneratorConfig: () => ({
model: 'test-model',
authType: 'oauth-personal',
}),
} as unknown as Config; } as unknown as Config;
const scheduler = new CoreToolScheduler({ const scheduler = new CoreToolScheduler({
@ -687,6 +707,10 @@ describe('CoreToolScheduler request queueing', () => {
getUsageStatisticsEnabled: () => true, getUsageStatisticsEnabled: () => true,
getDebugMode: () => false, getDebugMode: () => false,
getApprovalMode: () => ApprovalMode.YOLO, getApprovalMode: () => ApprovalMode.YOLO,
getContentGeneratorConfig: () => ({
model: 'test-model',
authType: 'oauth-personal',
}),
} as unknown as Config; } as unknown as Config;
const scheduler = new CoreToolScheduler({ const scheduler = new CoreToolScheduler({

View File

@ -20,6 +20,10 @@ const mockConfig = {
getSessionId: () => 'test-session-id', getSessionId: () => 'test-session-id',
getUsageStatisticsEnabled: () => true, getUsageStatisticsEnabled: () => true,
getDebugMode: () => false, getDebugMode: () => false,
getContentGeneratorConfig: () => ({
model: 'test-model',
authType: 'oauth-personal',
}),
} as unknown as Config; } as unknown as Config;
describe('executeToolCall', () => { describe('executeToolCall', () => {

View File

@ -181,12 +181,25 @@ describe('ClearcutLogger', () => {
const event = logger?.createLogEvent(EventNames.API_ERROR, []); const event = logger?.createLogEvent(EventNames.API_ERROR, []);
expect(event?.event_metadata[0][0]).toEqual({ expect(event?.event_metadata[0]).toContainEqual({
gemini_cli_key: EventMetadataKey.GEMINI_CLI_GOOGLE_ACCOUNTS_COUNT, gemini_cli_key: EventMetadataKey.GEMINI_CLI_GOOGLE_ACCOUNTS_COUNT,
value: '9001', value: '9001',
}); });
}); });
it('logs the current surface from a github action', () => {
const { logger } = setup({});
vi.stubEnv('GITHUB_SHA', '8675309');
const event = logger?.createLogEvent(EventNames.CHAT_COMPRESSION, []);
expect(event?.event_metadata[0]).toContainEqual({
gemini_cli_key: EventMetadataKey.GEMINI_CLI_SURFACE,
value: 'GitHub',
});
});
it('logs the current surface', () => { it('logs the current surface', () => {
const { logger } = setup({}); const { logger } = setup({});
@ -195,7 +208,7 @@ describe('ClearcutLogger', () => {
const event = logger?.createLogEvent(EventNames.API_ERROR, []); const event = logger?.createLogEvent(EventNames.API_ERROR, []);
expect(event?.event_metadata[0][1]).toEqual({ expect(event?.event_metadata[0]).toContainEqual({
gemini_cli_key: EventMetadataKey.GEMINI_CLI_SURFACE, gemini_cli_key: EventMetadataKey.GEMINI_CLI_SURFACE,
value: 'ide-1234', value: 'ide-1234',
}); });
@ -238,7 +251,7 @@ describe('ClearcutLogger', () => {
expectedValue: 'cloudshell', expectedValue: 'cloudshell',
}, },
])( ])(
'logs the current surface for as $expectedValue, preempting vscode detection', 'logs the current surface as $expectedValue, preempting vscode detection',
({ env, expectedValue }) => { ({ env, expectedValue }) => {
const { logger } = setup({}); const { logger } = setup({});
for (const [key, value] of Object.entries(env)) { for (const [key, value] of Object.entries(env)) {
@ -246,7 +259,7 @@ describe('ClearcutLogger', () => {
} }
vi.stubEnv('TERM_PROGRAM', 'vscode'); vi.stubEnv('TERM_PROGRAM', 'vscode');
const event = logger?.createLogEvent(EventNames.API_ERROR, []); const event = logger?.createLogEvent(EventNames.API_ERROR, []);
expect(event?.event_metadata[0][1]).toEqual({ expect(event?.event_metadata[0][3]).toEqual({
gemini_cli_key: EventMetadataKey.GEMINI_CLI_SURFACE, gemini_cli_key: EventMetadataKey.GEMINI_CLI_SURFACE,
value: expectedValue, value: expectedValue,
}); });

View File

@ -7,13 +7,11 @@
import { HttpsProxyAgent } from 'https-proxy-agent'; import { HttpsProxyAgent } from 'https-proxy-agent';
import { import {
StartSessionEvent, StartSessionEvent,
EndSessionEvent,
UserPromptEvent, UserPromptEvent,
ToolCallEvent, ToolCallEvent,
ApiRequestEvent, ApiRequestEvent,
ApiResponseEvent, ApiResponseEvent,
ApiErrorEvent, ApiErrorEvent,
FlashFallbackEvent,
LoopDetectedEvent, LoopDetectedEvent,
NextSpeakerCheckEvent, NextSpeakerCheckEvent,
SlashCommandEvent, SlashCommandEvent,
@ -129,6 +127,8 @@ const MAX_RETRY_EVENTS = 100;
export class ClearcutLogger { export class ClearcutLogger {
private static instance: ClearcutLogger; private static instance: ClearcutLogger;
private config?: Config; private config?: Config;
private sessionData: EventValue[] = [];
private promptId: string = '';
/** /**
* Queue of pending events that need to be flushed to the server. New events * Queue of pending events that need to be flushed to the server. New events
@ -155,6 +155,7 @@ export class ClearcutLogger {
private constructor(config?: Config) { private constructor(config?: Config) {
this.config = config; this.config = config;
this.events = new FixedDeque<LogEventEntry[]>(Array, MAX_EVENTS); this.events = new FixedDeque<LogEventEntry[]>(Array, MAX_EVENTS);
this.promptId = config?.getSessionId() ?? '';
} }
static getInstance(config?: Config): ClearcutLogger | undefined { static getInstance(config?: Config): ClearcutLogger | undefined {
@ -203,7 +204,10 @@ export class ClearcutLogger {
createLogEvent(eventName: EventNames, data: EventValue[] = []): LogEvent { createLogEvent(eventName: EventNames, data: EventValue[] = []): LogEvent {
const email = getCachedGoogleAccount(); const email = getCachedGoogleAccount();
data = addDefaultFields(data); if (eventName !== EventNames.START_SESSION) {
data.push(...this.sessionData);
}
data = this.addDefaultFields(data);
const logEvent: LogEvent = { const logEvent: LogEvent = {
console_type: 'GEMINI_CLI', console_type: 'GEMINI_CLI',
@ -319,10 +323,6 @@ export class ClearcutLogger {
gemini_cli_key: EventMetadataKey.GEMINI_CLI_START_SESSION_MODEL, gemini_cli_key: EventMetadataKey.GEMINI_CLI_START_SESSION_MODEL,
value: event.model, value: event.model,
}, },
{
gemini_cli_key: EventMetadataKey.GEMINI_CLI_SESSION_ID,
value: this.config?.getSessionId() ?? '',
},
{ {
gemini_cli_key: gemini_cli_key:
EventMetadataKey.GEMINI_CLI_START_SESSION_EMBEDDING_MODEL, EventMetadataKey.GEMINI_CLI_START_SESSION_EMBEDDING_MODEL,
@ -379,15 +379,8 @@ export class ClearcutLogger {
EventMetadataKey.GEMINI_CLI_START_SESSION_TELEMETRY_LOG_USER_PROMPTS_ENABLED, EventMetadataKey.GEMINI_CLI_START_SESSION_TELEMETRY_LOG_USER_PROMPTS_ENABLED,
value: event.telemetry_log_user_prompts_enabled.toString(), value: event.telemetry_log_user_prompts_enabled.toString(),
}, },
{
gemini_cli_key: EventMetadataKey.GEMINI_CLI_VERSION,
value: CLI_VERSION,
},
{
gemini_cli_key: EventMetadataKey.GEMINI_CLI_GIT_COMMIT_HASH,
value: GIT_COMMIT_INFO,
},
]; ];
this.sessionData = data;
// Flush start event immediately // Flush start event immediately
this.enqueueLogEvent(this.createLogEvent(EventNames.START_SESSION, data)); this.enqueueLogEvent(this.createLogEvent(EventNames.START_SESSION, data));
@ -397,23 +390,12 @@ export class ClearcutLogger {
} }
logNewPromptEvent(event: UserPromptEvent): void { logNewPromptEvent(event: UserPromptEvent): void {
this.promptId = event.prompt_id;
const data: EventValue[] = [ const data: EventValue[] = [
{ {
gemini_cli_key: EventMetadataKey.GEMINI_CLI_USER_PROMPT_LENGTH, gemini_cli_key: EventMetadataKey.GEMINI_CLI_USER_PROMPT_LENGTH,
value: JSON.stringify(event.prompt_length), value: JSON.stringify(event.prompt_length),
}, },
{
gemini_cli_key: EventMetadataKey.GEMINI_CLI_SESSION_ID,
value: this.config?.getSessionId() ?? '',
},
{
gemini_cli_key: EventMetadataKey.GEMINI_CLI_PROMPT_ID,
value: JSON.stringify(event.prompt_id),
},
{
gemini_cli_key: EventMetadataKey.GEMINI_CLI_AUTH_TYPE,
value: JSON.stringify(event.auth_type),
},
]; ];
this.enqueueLogEvent(this.createLogEvent(EventNames.NEW_PROMPT, data)); this.enqueueLogEvent(this.createLogEvent(EventNames.NEW_PROMPT, data));
@ -426,10 +408,6 @@ export class ClearcutLogger {
gemini_cli_key: EventMetadataKey.GEMINI_CLI_TOOL_CALL_NAME, gemini_cli_key: EventMetadataKey.GEMINI_CLI_TOOL_CALL_NAME,
value: JSON.stringify(event.function_name), value: JSON.stringify(event.function_name),
}, },
{
gemini_cli_key: EventMetadataKey.GEMINI_CLI_PROMPT_ID,
value: JSON.stringify(event.prompt_id),
},
{ {
gemini_cli_key: EventMetadataKey.GEMINI_CLI_TOOL_CALL_DECISION, gemini_cli_key: EventMetadataKey.GEMINI_CLI_TOOL_CALL_DECISION,
value: JSON.stringify(event.decision), value: JSON.stringify(event.decision),
@ -485,10 +463,6 @@ export class ClearcutLogger {
gemini_cli_key: EventMetadataKey.GEMINI_CLI_API_REQUEST_MODEL, gemini_cli_key: EventMetadataKey.GEMINI_CLI_API_REQUEST_MODEL,
value: JSON.stringify(event.model), value: JSON.stringify(event.model),
}, },
{
gemini_cli_key: EventMetadataKey.GEMINI_CLI_PROMPT_ID,
value: JSON.stringify(event.prompt_id),
},
]; ];
this.enqueueLogEvent(this.createLogEvent(EventNames.API_REQUEST, data)); this.enqueueLogEvent(this.createLogEvent(EventNames.API_REQUEST, data));
@ -501,10 +475,6 @@ export class ClearcutLogger {
gemini_cli_key: EventMetadataKey.GEMINI_CLI_API_RESPONSE_MODEL, gemini_cli_key: EventMetadataKey.GEMINI_CLI_API_RESPONSE_MODEL,
value: JSON.stringify(event.model), value: JSON.stringify(event.model),
}, },
{
gemini_cli_key: EventMetadataKey.GEMINI_CLI_PROMPT_ID,
value: JSON.stringify(event.prompt_id),
},
{ {
gemini_cli_key: EventMetadataKey.GEMINI_CLI_API_RESPONSE_STATUS_CODE, gemini_cli_key: EventMetadataKey.GEMINI_CLI_API_RESPONSE_STATUS_CODE,
value: JSON.stringify(event.status_code), value: JSON.stringify(event.status_code),
@ -542,10 +512,6 @@ export class ClearcutLogger {
EventMetadataKey.GEMINI_CLI_API_RESPONSE_TOOL_TOKEN_COUNT, EventMetadataKey.GEMINI_CLI_API_RESPONSE_TOOL_TOKEN_COUNT,
value: JSON.stringify(event.tool_token_count), value: JSON.stringify(event.tool_token_count),
}, },
{
gemini_cli_key: EventMetadataKey.GEMINI_CLI_AUTH_TYPE,
value: JSON.stringify(event.auth_type),
},
]; ];
this.enqueueLogEvent(this.createLogEvent(EventNames.API_RESPONSE, data)); this.enqueueLogEvent(this.createLogEvent(EventNames.API_RESPONSE, data));
@ -558,10 +524,6 @@ export class ClearcutLogger {
gemini_cli_key: EventMetadataKey.GEMINI_CLI_API_ERROR_MODEL, gemini_cli_key: EventMetadataKey.GEMINI_CLI_API_ERROR_MODEL,
value: JSON.stringify(event.model), value: JSON.stringify(event.model),
}, },
{
gemini_cli_key: EventMetadataKey.GEMINI_CLI_PROMPT_ID,
value: JSON.stringify(event.prompt_id),
},
{ {
gemini_cli_key: EventMetadataKey.GEMINI_CLI_API_ERROR_TYPE, gemini_cli_key: EventMetadataKey.GEMINI_CLI_API_ERROR_TYPE,
value: JSON.stringify(event.error_type), value: JSON.stringify(event.error_type),
@ -574,10 +536,6 @@ export class ClearcutLogger {
gemini_cli_key: EventMetadataKey.GEMINI_CLI_API_ERROR_DURATION_MS, gemini_cli_key: EventMetadataKey.GEMINI_CLI_API_ERROR_DURATION_MS,
value: JSON.stringify(event.duration_ms), value: JSON.stringify(event.duration_ms),
}, },
{
gemini_cli_key: EventMetadataKey.GEMINI_CLI_AUTH_TYPE,
value: JSON.stringify(event.auth_type),
},
]; ];
this.enqueueLogEvent(this.createLogEvent(EventNames.API_ERROR, data)); this.enqueueLogEvent(this.createLogEvent(EventNames.API_ERROR, data));
@ -601,19 +559,8 @@ export class ClearcutLogger {
); );
} }
logFlashFallbackEvent(event: FlashFallbackEvent): void { logFlashFallbackEvent(): void {
const data: EventValue[] = [ this.enqueueLogEvent(this.createLogEvent(EventNames.FLASH_FALLBACK, []));
{
gemini_cli_key: EventMetadataKey.GEMINI_CLI_AUTH_TYPE,
value: JSON.stringify(event.auth_type),
},
{
gemini_cli_key: EventMetadataKey.GEMINI_CLI_SESSION_ID,
value: this.config?.getSessionId() ?? '',
},
];
this.enqueueLogEvent(this.createLogEvent(EventNames.FLASH_FALLBACK, data));
this.flushToClearcut().catch((error) => { this.flushToClearcut().catch((error) => {
console.debug('Error flushing to Clearcut:', error); console.debug('Error flushing to Clearcut:', error);
}); });
@ -621,10 +568,6 @@ export class ClearcutLogger {
logLoopDetectedEvent(event: LoopDetectedEvent): void { logLoopDetectedEvent(event: LoopDetectedEvent): void {
const data: EventValue[] = [ const data: EventValue[] = [
{
gemini_cli_key: EventMetadataKey.GEMINI_CLI_PROMPT_ID,
value: JSON.stringify(event.prompt_id),
},
{ {
gemini_cli_key: EventMetadataKey.GEMINI_CLI_LOOP_DETECTED_TYPE, gemini_cli_key: EventMetadataKey.GEMINI_CLI_LOOP_DETECTED_TYPE,
value: JSON.stringify(event.loop_type), value: JSON.stringify(event.loop_type),
@ -637,10 +580,6 @@ export class ClearcutLogger {
logNextSpeakerCheck(event: NextSpeakerCheckEvent): void { logNextSpeakerCheck(event: NextSpeakerCheckEvent): void {
const data: EventValue[] = [ const data: EventValue[] = [
{
gemini_cli_key: EventMetadataKey.GEMINI_CLI_PROMPT_ID,
value: JSON.stringify(event.prompt_id),
},
{ {
gemini_cli_key: EventMetadataKey.GEMINI_CLI_RESPONSE_FINISH_REASON, gemini_cli_key: EventMetadataKey.GEMINI_CLI_RESPONSE_FINISH_REASON,
value: JSON.stringify(event.finish_reason), value: JSON.stringify(event.finish_reason),
@ -649,10 +588,6 @@ export class ClearcutLogger {
gemini_cli_key: EventMetadataKey.GEMINI_CLI_NEXT_SPEAKER_CHECK_RESULT, gemini_cli_key: EventMetadataKey.GEMINI_CLI_NEXT_SPEAKER_CHECK_RESULT,
value: JSON.stringify(event.result), value: JSON.stringify(event.result),
}, },
{
gemini_cli_key: EventMetadataKey.GEMINI_CLI_SESSION_ID,
value: this.config?.getSessionId() ?? '',
},
]; ];
this.enqueueLogEvent( this.enqueueLogEvent(
@ -732,21 +667,57 @@ export class ClearcutLogger {
this.flushIfNeeded(); this.flushIfNeeded();
} }
logEndSessionEvent(event: EndSessionEvent): void { logEndSessionEvent(): void {
const data: EventValue[] = [
{
gemini_cli_key: EventMetadataKey.GEMINI_CLI_SESSION_ID,
value: event?.session_id?.toString() ?? '',
},
];
// Flush immediately on session end. // Flush immediately on session end.
this.enqueueLogEvent(this.createLogEvent(EventNames.END_SESSION, data)); this.enqueueLogEvent(this.createLogEvent(EventNames.END_SESSION, []));
this.flushToClearcut().catch((error) => { this.flushToClearcut().catch((error) => {
console.debug('Error flushing to Clearcut:', error); console.debug('Error flushing to Clearcut:', error);
}); });
} }
/**
* Adds default fields to data, and returns a new data array. This fields
* should exist on all log events.
*/
addDefaultFields(data: EventValue[]): EventValue[] {
const totalAccounts = getLifetimeGoogleAccounts();
const surface = determineSurface();
const defaultLogMetadata: EventValue[] = [
{
gemini_cli_key: EventMetadataKey.GEMINI_CLI_SESSION_ID,
value: this.config?.getSessionId() ?? '',
},
{
gemini_cli_key: EventMetadataKey.GEMINI_CLI_AUTH_TYPE,
value: JSON.stringify(
this.config?.getContentGeneratorConfig()?.authType,
),
},
{
gemini_cli_key: EventMetadataKey.GEMINI_CLI_GOOGLE_ACCOUNTS_COUNT,
value: `${totalAccounts}`,
},
{
gemini_cli_key: EventMetadataKey.GEMINI_CLI_SURFACE,
value: surface,
},
{
gemini_cli_key: EventMetadataKey.GEMINI_CLI_VERSION,
value: CLI_VERSION,
},
{
gemini_cli_key: EventMetadataKey.GEMINI_CLI_GIT_COMMIT_HASH,
value: GIT_COMMIT_INFO,
},
{
gemini_cli_key: EventMetadataKey.GEMINI_CLI_PROMPT_ID,
value: this.promptId,
},
];
return [...data, ...defaultLogMetadata];
}
getProxyAgent() { getProxyAgent() {
const proxyUrl = this.config?.getProxy(); const proxyUrl = this.config?.getProxy();
if (!proxyUrl) return undefined; if (!proxyUrl) return undefined;
@ -760,8 +731,7 @@ export class ClearcutLogger {
} }
shutdown() { shutdown() {
const event = new EndSessionEvent(this.config); this.logEndSessionEvent();
this.logEndSessionEvent(event);
} }
private requeueFailedEvents(eventsToSend: LogEventEntry[][]): void { private requeueFailedEvents(eventsToSend: LogEventEntry[][]): void {
@ -815,26 +785,6 @@ export class ClearcutLogger {
} }
} }
/**
* Adds default fields to data, and returns a new data array. This fields
* should exist on all log events.
*/
function addDefaultFields(data: EventValue[]): EventValue[] {
const totalAccounts = getLifetimeGoogleAccounts();
const surface = determineSurface();
const defaultLogMetadata: EventValue[] = [
{
gemini_cli_key: EventMetadataKey.GEMINI_CLI_GOOGLE_ACCOUNTS_COUNT,
value: `${totalAccounts}`,
},
{
gemini_cli_key: EventMetadataKey.GEMINI_CLI_SURFACE,
value: surface,
},
];
return [...data, ...defaultLogMetadata];
}
export const TEST_ONLY = { export const TEST_ONLY = {
MAX_RETRY_EVENTS, MAX_RETRY_EVENTS,
MAX_EVENTS, MAX_EVENTS,

View File

@ -175,7 +175,7 @@ export function logFlashFallback(
config: Config, config: Config,
event: FlashFallbackEvent, event: FlashFallbackEvent,
): void { ): void {
ClearcutLogger.getInstance(config)?.logFlashFallbackEvent(event); ClearcutLogger.getInstance(config)?.logFlashFallbackEvent();
if (!isTelemetrySdkInitialized()) return; if (!isTelemetrySdkInitialized()) return;
const attributes: LogAttributes = { const attributes: LogAttributes = {