diff --git a/packages/cli/src/ui/hooks/useToolScheduler.test.ts b/packages/cli/src/ui/hooks/useToolScheduler.test.ts index b2071931..8e060ee8 100644 --- a/packages/cli/src/ui/hooks/useToolScheduler.test.ts +++ b/packages/cli/src/ui/hooks/useToolScheduler.test.ts @@ -55,6 +55,11 @@ const mockConfig = { getApprovalMode: vi.fn(() => ApprovalMode.DEFAULT), getUsageStatisticsEnabled: () => true, getDebugMode: () => false, + getSessionId: () => 'test-session-id', + getContentGeneratorConfig: () => ({ + model: 'test-model', + authType: 'oauth-personal', + }), }; class MockToolInvocation extends BaseToolInvocation { diff --git a/packages/core/src/core/coreToolScheduler.test.ts b/packages/core/src/core/coreToolScheduler.test.ts index 5cf49350..e0d7f65a 100644 --- a/packages/core/src/core/coreToolScheduler.test.ts +++ b/packages/core/src/core/coreToolScheduler.test.ts @@ -53,6 +53,10 @@ describe('CoreToolScheduler', () => { getUsageStatisticsEnabled: () => true, getDebugMode: () => false, getApprovalMode: () => ApprovalMode.DEFAULT, + getContentGeneratorConfig: () => ({ + model: 'test-model', + authType: 'oauth-personal', + }), } as unknown as Config; const scheduler = new CoreToolScheduler({ @@ -109,6 +113,10 @@ describe('CoreToolScheduler with payload', () => { getUsageStatisticsEnabled: () => true, getDebugMode: () => false, getApprovalMode: () => ApprovalMode.DEFAULT, + getContentGeneratorConfig: () => ({ + model: 'test-model', + authType: 'oauth-personal', + }), } as unknown as Config; const scheduler = new CoreToolScheduler({ @@ -405,6 +413,10 @@ describe('CoreToolScheduler edit cancellation', () => { getUsageStatisticsEnabled: () => true, getDebugMode: () => false, getApprovalMode: () => ApprovalMode.DEFAULT, + getContentGeneratorConfig: () => ({ + model: 'test-model', + authType: 'oauth-personal', + }), } as unknown as Config; const scheduler = new CoreToolScheduler({ @@ -493,6 +505,10 @@ describe('CoreToolScheduler YOLO mode', () => { getUsageStatisticsEnabled: () => true, getDebugMode: () => false, getApprovalMode: () => ApprovalMode.YOLO, + getContentGeneratorConfig: () => ({ + model: 'test-model', + authType: 'oauth-personal', + }), } as unknown as Config; const scheduler = new CoreToolScheduler({ @@ -578,6 +594,10 @@ describe('CoreToolScheduler request queueing', () => { getUsageStatisticsEnabled: () => true, getDebugMode: () => false, getApprovalMode: () => ApprovalMode.YOLO, // Use YOLO to avoid confirmation prompts + getContentGeneratorConfig: () => ({ + model: 'test-model', + authType: 'oauth-personal', + }), } as unknown as Config; const scheduler = new CoreToolScheduler({ @@ -687,6 +707,10 @@ describe('CoreToolScheduler request queueing', () => { getUsageStatisticsEnabled: () => true, getDebugMode: () => false, getApprovalMode: () => ApprovalMode.YOLO, + getContentGeneratorConfig: () => ({ + model: 'test-model', + authType: 'oauth-personal', + }), } as unknown as Config; const scheduler = new CoreToolScheduler({ diff --git a/packages/core/src/core/nonInteractiveToolExecutor.test.ts b/packages/core/src/core/nonInteractiveToolExecutor.test.ts index 484d2e45..0c1164ea 100644 --- a/packages/core/src/core/nonInteractiveToolExecutor.test.ts +++ b/packages/core/src/core/nonInteractiveToolExecutor.test.ts @@ -20,6 +20,10 @@ const mockConfig = { getSessionId: () => 'test-session-id', getUsageStatisticsEnabled: () => true, getDebugMode: () => false, + getContentGeneratorConfig: () => ({ + model: 'test-model', + authType: 'oauth-personal', + }), } as unknown as Config; describe('executeToolCall', () => { diff --git a/packages/core/src/telemetry/clearcut-logger/clearcut-logger.test.ts b/packages/core/src/telemetry/clearcut-logger/clearcut-logger.test.ts index 39596616..2777f196 100644 --- a/packages/core/src/telemetry/clearcut-logger/clearcut-logger.test.ts +++ b/packages/core/src/telemetry/clearcut-logger/clearcut-logger.test.ts @@ -181,12 +181,25 @@ describe('ClearcutLogger', () => { 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, 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', () => { const { logger } = setup({}); @@ -195,7 +208,7 @@ describe('ClearcutLogger', () => { 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, value: 'ide-1234', }); @@ -238,7 +251,7 @@ describe('ClearcutLogger', () => { expectedValue: 'cloudshell', }, ])( - 'logs the current surface for as $expectedValue, preempting vscode detection', + 'logs the current surface as $expectedValue, preempting vscode detection', ({ env, expectedValue }) => { const { logger } = setup({}); for (const [key, value] of Object.entries(env)) { @@ -246,7 +259,7 @@ describe('ClearcutLogger', () => { } vi.stubEnv('TERM_PROGRAM', 'vscode'); 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, value: expectedValue, }); diff --git a/packages/core/src/telemetry/clearcut-logger/clearcut-logger.ts b/packages/core/src/telemetry/clearcut-logger/clearcut-logger.ts index 7777fa24..7369bc1b 100644 --- a/packages/core/src/telemetry/clearcut-logger/clearcut-logger.ts +++ b/packages/core/src/telemetry/clearcut-logger/clearcut-logger.ts @@ -7,13 +7,11 @@ import { HttpsProxyAgent } from 'https-proxy-agent'; import { StartSessionEvent, - EndSessionEvent, UserPromptEvent, ToolCallEvent, ApiRequestEvent, ApiResponseEvent, ApiErrorEvent, - FlashFallbackEvent, LoopDetectedEvent, NextSpeakerCheckEvent, SlashCommandEvent, @@ -129,6 +127,8 @@ const MAX_RETRY_EVENTS = 100; export class ClearcutLogger { private static instance: ClearcutLogger; private config?: Config; + private sessionData: EventValue[] = []; + private promptId: string = ''; /** * 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) { this.config = config; this.events = new FixedDeque(Array, MAX_EVENTS); + this.promptId = config?.getSessionId() ?? ''; } static getInstance(config?: Config): ClearcutLogger | undefined { @@ -203,7 +204,10 @@ export class ClearcutLogger { createLogEvent(eventName: EventNames, data: EventValue[] = []): LogEvent { const email = getCachedGoogleAccount(); - data = addDefaultFields(data); + if (eventName !== EventNames.START_SESSION) { + data.push(...this.sessionData); + } + data = this.addDefaultFields(data); const logEvent: LogEvent = { console_type: 'GEMINI_CLI', @@ -319,10 +323,6 @@ export class ClearcutLogger { gemini_cli_key: EventMetadataKey.GEMINI_CLI_START_SESSION_MODEL, value: event.model, }, - { - gemini_cli_key: EventMetadataKey.GEMINI_CLI_SESSION_ID, - value: this.config?.getSessionId() ?? '', - }, { gemini_cli_key: EventMetadataKey.GEMINI_CLI_START_SESSION_EMBEDDING_MODEL, @@ -379,15 +379,8 @@ export class ClearcutLogger { EventMetadataKey.GEMINI_CLI_START_SESSION_TELEMETRY_LOG_USER_PROMPTS_ENABLED, 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 this.enqueueLogEvent(this.createLogEvent(EventNames.START_SESSION, data)); @@ -397,23 +390,12 @@ export class ClearcutLogger { } logNewPromptEvent(event: UserPromptEvent): void { + this.promptId = event.prompt_id; const data: EventValue[] = [ { gemini_cli_key: EventMetadataKey.GEMINI_CLI_USER_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)); @@ -426,10 +408,6 @@ export class ClearcutLogger { gemini_cli_key: EventMetadataKey.GEMINI_CLI_TOOL_CALL_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, value: JSON.stringify(event.decision), @@ -485,10 +463,6 @@ export class ClearcutLogger { gemini_cli_key: EventMetadataKey.GEMINI_CLI_API_REQUEST_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)); @@ -501,10 +475,6 @@ export class ClearcutLogger { gemini_cli_key: EventMetadataKey.GEMINI_CLI_API_RESPONSE_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, value: JSON.stringify(event.status_code), @@ -542,10 +512,6 @@ export class ClearcutLogger { EventMetadataKey.GEMINI_CLI_API_RESPONSE_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)); @@ -558,10 +524,6 @@ export class ClearcutLogger { gemini_cli_key: EventMetadataKey.GEMINI_CLI_API_ERROR_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, value: JSON.stringify(event.error_type), @@ -574,10 +536,6 @@ export class ClearcutLogger { gemini_cli_key: EventMetadataKey.GEMINI_CLI_API_ERROR_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)); @@ -601,19 +559,8 @@ export class ClearcutLogger { ); } - logFlashFallbackEvent(event: FlashFallbackEvent): void { - const data: EventValue[] = [ - { - 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)); + logFlashFallbackEvent(): void { + this.enqueueLogEvent(this.createLogEvent(EventNames.FLASH_FALLBACK, [])); this.flushToClearcut().catch((error) => { console.debug('Error flushing to Clearcut:', error); }); @@ -621,10 +568,6 @@ export class ClearcutLogger { logLoopDetectedEvent(event: LoopDetectedEvent): void { 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, value: JSON.stringify(event.loop_type), @@ -637,10 +580,6 @@ export class ClearcutLogger { logNextSpeakerCheck(event: NextSpeakerCheckEvent): void { 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, value: JSON.stringify(event.finish_reason), @@ -649,10 +588,6 @@ export class ClearcutLogger { gemini_cli_key: EventMetadataKey.GEMINI_CLI_NEXT_SPEAKER_CHECK_RESULT, value: JSON.stringify(event.result), }, - { - gemini_cli_key: EventMetadataKey.GEMINI_CLI_SESSION_ID, - value: this.config?.getSessionId() ?? '', - }, ]; this.enqueueLogEvent( @@ -732,21 +667,57 @@ export class ClearcutLogger { this.flushIfNeeded(); } - logEndSessionEvent(event: EndSessionEvent): void { - const data: EventValue[] = [ - { - gemini_cli_key: EventMetadataKey.GEMINI_CLI_SESSION_ID, - value: event?.session_id?.toString() ?? '', - }, - ]; - + logEndSessionEvent(): void { // Flush immediately on session end. - this.enqueueLogEvent(this.createLogEvent(EventNames.END_SESSION, data)); + this.enqueueLogEvent(this.createLogEvent(EventNames.END_SESSION, [])); this.flushToClearcut().catch((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() { const proxyUrl = this.config?.getProxy(); if (!proxyUrl) return undefined; @@ -760,8 +731,7 @@ export class ClearcutLogger { } shutdown() { - const event = new EndSessionEvent(this.config); - this.logEndSessionEvent(event); + this.logEndSessionEvent(); } 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 = { MAX_RETRY_EVENTS, MAX_EVENTS, diff --git a/packages/core/src/telemetry/loggers.ts b/packages/core/src/telemetry/loggers.ts index 6504c0e7..9b9faf79 100644 --- a/packages/core/src/telemetry/loggers.ts +++ b/packages/core/src/telemetry/loggers.ts @@ -175,7 +175,7 @@ export function logFlashFallback( config: Config, event: FlashFallbackEvent, ): void { - ClearcutLogger.getInstance(config)?.logFlashFallbackEvent(event); + ClearcutLogger.getInstance(config)?.logFlashFallbackEvent(); if (!isTelemetrySdkInitialized()) return; const attributes: LogAttributes = {