/** * @license * Copyright 2025 Google LLC * SPDX-License-Identifier: Apache-2.0 */ import { createHash } from 'node:crypto'; import * as os from 'node:os'; import si from 'systeminformation'; import { HttpsProxyAgent } from 'https-proxy-agent'; import type { StartSessionEvent, UserPromptEvent, ToolCallEvent, ApiRequestEvent, ApiResponseEvent, ApiErrorEvent, LoopDetectedEvent, NextSpeakerCheckEvent, SlashCommandEvent, RewindEvent, MalformedJsonResponseEvent, IdeConnectionEvent, ConversationFinishedEvent, ChatCompressionEvent, FileOperationEvent, InvalidChunkEvent, ContentRetryEvent, ContentRetryFailureEvent, ExtensionInstallEvent, ToolOutputTruncatedEvent, ExtensionUninstallEvent, ModelRoutingEvent, ExtensionEnableEvent, ModelSlashCommandEvent, ExtensionDisableEvent, EditStrategyEvent, EditCorrectionEvent, AgentStartEvent, AgentFinishEvent, RecoveryAttemptEvent, WebFetchFallbackAttemptEvent, ExtensionUpdateEvent, LlmLoopCheckEvent, HookCallEvent, ApprovalModeSwitchEvent, ApprovalModeDurationEvent, PlanExecutionEvent, ToolOutputMaskingEvent, KeychainAvailabilityEvent, TokenStorageInitializationEvent, StartupStatsEvent, } from '../types.js'; import { EventMetadataKey } from './event-metadata-key.js'; import type { Config } from '../../config/config.js'; import { InstallationManager } from '../../utils/installationManager.js'; import { UserAccountManager } from '../../utils/userAccountManager.js'; import { safeJsonStringify, safeJsonStringifyBooleanValuesOnly, } from '../../utils/safeJsonStringify.js'; import { ASK_USER_TOOL_NAME } from '../../tools/tool-names.js'; import { FixedDeque } from 'mnemonist'; import { GIT_COMMIT_INFO, CLI_VERSION } from '../../generated/git-commit.js'; import { IDE_DEFINITIONS, detectIdeFromEnv, isCloudShell, } from '../../ide/detect-ide.js'; import { debugLogger } from '../../utils/debugLogger.js'; import { getErrorMessage } from '../../utils/errors.js'; export enum EventNames { START_SESSION = 'start_session', NEW_PROMPT = 'new_prompt', TOOL_CALL = 'tool_call', FILE_OPERATION = 'file_operation', API_REQUEST = 'api_request', API_RESPONSE = 'api_response', API_ERROR = 'api_error', END_SESSION = 'end_session', FLASH_FALLBACK = 'flash_fallback', RIPGREP_FALLBACK = 'ripgrep_fallback', LOOP_DETECTED = 'loop_detected', LOOP_DETECTION_DISABLED = 'loop_detection_disabled', NEXT_SPEAKER_CHECK = 'next_speaker_check', SLASH_COMMAND = 'slash_command', REWIND = 'rewind', MALFORMED_JSON_RESPONSE = 'malformed_json_response', IDE_CONNECTION = 'ide_connection', KITTY_SEQUENCE_OVERFLOW = 'kitty_sequence_overflow', CHAT_COMPRESSION = 'chat_compression', CONVERSATION_FINISHED = 'conversation_finished', INVALID_CHUNK = 'invalid_chunk', CONTENT_RETRY = 'content_retry', CONTENT_RETRY_FAILURE = 'content_retry_failure', EXTENSION_ENABLE = 'extension_enable', EXTENSION_DISABLE = 'extension_disable', EXTENSION_INSTALL = 'extension_install', EXTENSION_UNINSTALL = 'extension_uninstall', EXTENSION_UPDATE = 'extension_update', TOOL_OUTPUT_TRUNCATED = 'tool_output_truncated', MODEL_ROUTING = 'model_routing', MODEL_SLASH_COMMAND = 'model_slash_command', EDIT_STRATEGY = 'edit_strategy', EDIT_CORRECTION = 'edit_correction', AGENT_START = 'agent_start', AGENT_FINISH = 'agent_finish', RECOVERY_ATTEMPT = 'recovery_attempt', WEB_FETCH_FALLBACK_ATTEMPT = 'web_fetch_fallback_attempt', LLM_LOOP_CHECK = 'llm_loop_check', HOOK_CALL = 'hook_call', APPROVAL_MODE_SWITCH = 'approval_mode_switch', APPROVAL_MODE_DURATION = 'approval_mode_duration', PLAN_EXECUTION = 'plan_execution', TOOL_OUTPUT_MASKING = 'tool_output_masking', KEYCHAIN_AVAILABILITY = 'keychain_availability', TOKEN_STORAGE_INITIALIZATION = 'token_storage_initialization', CONSECA_POLICY_GENERATION = 'conseca_policy_generation', CONSECA_VERDICT = 'conseca_verdict', STARTUP_STATS = 'startup_stats', } export interface LogResponse { nextRequestWaitMs?: number; } export interface LogEventEntry { event_time_ms: number; source_extension_json: string; exp?: { gws_experiment: number[]; }; } export interface EventValue { gemini_cli_key: EventMetadataKey; value: string; } export interface LogEvent { console_type: 'GEMINI_CLI'; application: number; event_name: string; event_metadata: EventValue[][]; client_email?: string; client_install_id?: string; } export interface LogRequest { log_source_name: 'CONCORD'; request_time_ms: number; log_event: LogEventEntry[][]; } /** * Determine the surface that the user is currently using. Surface is effectively the * distribution channel in which the user is using Gemini CLI. Gemini CLI comes bundled * w/ Firebase Studio and Cloud Shell. Users that manually download themselves will * likely be "SURFACE_NOT_SET". * * This is computed based upon a series of environment variables these distribution * methods might have in their runtimes. */ function determineSurface(): string { if (process.env['SURFACE']) { return process.env['SURFACE']; } else if (isCloudShell()) { return IDE_DEFINITIONS.cloudshell.name; } else if (process.env['GITHUB_SHA']) { return 'GitHub'; } else if (process.env['TERM_PROGRAM'] === 'vscode') { return detectIdeFromEnv().name || IDE_DEFINITIONS.vscode.name; } else { return 'SURFACE_NOT_SET'; } } /** * Determines the GitHub Actions workflow name if the CLI is running in a GitHub Actions environment. */ function determineGHWorkflowName(): string | undefined { return process.env['GH_WORKFLOW_NAME']; } /** * Determines the GitHub repository name if the CLI is running in a GitHub Actions environment. */ function determineGHRepositoryName(): string | undefined { return process.env['GITHUB_REPOSITORY']; } /** * Determines the GitHub event name if the CLI is running in a GitHub Actions environment. */ function determineGHEventName(): string | undefined { return process.env['GITHUB_EVENT_NAME']; } /** * Determines the GitHub Pull Request number if the CLI is running in a GitHub Actions environment. */ function determineGHPRNumber(): string | undefined { return process.env['GH_PR_NUMBER']; } /** * Determines the GitHub Issue number if the CLI is running in a GitHub Actions environment. */ function determineGHIssueNumber(): string | undefined { return process.env['GH_ISSUE_NUMBER']; } /** * Determines the GitHub custom tracking ID if the CLI is running in a GitHub Actions environment. */ function determineGHCustomTrackingId(): string | undefined { return process.env['GH_CUSTOM_TRACKING_ID']; } /** * Clearcut URL to send logging events to. */ const CLEARCUT_URL = 'https://play.googleapis.com/log?format=json&hasfast=true'; /** * Interval in which buffered events are sent to clearcut. */ const FLUSH_INTERVAL_MS = 1000 * 60; /** * Maximum amount of events to keep in memory. Events added after this amount * are dropped until the next flush to clearcut, which happens periodically as * defined by {@link FLUSH_INTERVAL_MS}. */ const MAX_EVENTS = 1000; /** * Maximum events to retry after a failed clearcut flush */ const MAX_RETRY_EVENTS = 100; const NO_GPU = 'NA'; let cachedGpuInfo: string | undefined; async function refreshGpuInfo(): Promise { try { const graphics = await si.graphics(); if (graphics.controllers && graphics.controllers.length > 0) { cachedGpuInfo = graphics.controllers.map((c) => c.model).join(', '); } else { cachedGpuInfo = NO_GPU; } } catch (error) { cachedGpuInfo = 'FAILED'; debugLogger.error( 'Failed to get GPU information for telemetry', getErrorMessage(error), ); } } async function getGpuInfo(): Promise { if (!cachedGpuInfo) { await refreshGpuInfo(); } return cachedGpuInfo ?? NO_GPU; } // Singleton class for batch posting log events to Clearcut. When a new event comes in, the elapsed time // is checked and events are flushed to Clearcut if at least a minute has passed since the last flush. export class ClearcutLogger { private static instance: ClearcutLogger; private config?: Config; private sessionData: EventValue[] = []; private promptId: string = ''; private readonly installationManager: InstallationManager; private readonly userAccountManager: UserAccountManager; private readonly hashedGHRepositoryName?: string; /** * Queue of pending events that need to be flushed to the server. New events * are added to this queue and then flushed on demand (via `flushToClearcut`) */ private readonly events: FixedDeque; /** * The last time that the events were successfully flushed to the server. */ private lastFlushTime: number = Date.now(); /** * the value is true when there is a pending flush happening. This prevents * concurrent flush operations. */ private flushing: boolean = false; /** * This value is true when a flush was requested during an ongoing flush. */ private pendingFlush: boolean = false; private constructor(config: Config) { this.config = config; this.events = new FixedDeque(Array, MAX_EVENTS); this.promptId = config?.getSessionId() ?? ''; this.installationManager = new InstallationManager(); this.userAccountManager = new UserAccountManager(); const ghRepositoryName = determineGHRepositoryName(); if (ghRepositoryName) { this.hashedGHRepositoryName = createHash('sha256') .update(ghRepositoryName) .digest('hex'); } } static getInstance(config?: Config): ClearcutLogger | undefined { if (config === undefined || !config?.getUsageStatisticsEnabled()) return undefined; if (!ClearcutLogger.instance) { ClearcutLogger.instance = new ClearcutLogger(config); } return ClearcutLogger.instance; } /** For testing purposes only. */ static clearInstance(): void { // @ts-expect-error - ClearcutLogger is a singleton, but we need to clear it for tests. ClearcutLogger.instance = undefined; } enqueueHelper(event: LogEvent, experimentIds?: number[]): void { // Manually handle overflow for FixedDeque, which throws when full. const wasAtCapacity = this.events.size >= MAX_EVENTS; if (wasAtCapacity) { this.events.shift(); // Evict oldest element to make space. } const logEventEntry: LogEventEntry = { event_time_ms: Date.now(), source_extension_json: safeJsonStringify(event), }; if (experimentIds !== undefined) { logEventEntry.exp = { gws_experiment: experimentIds, }; } this.events.push([logEventEntry]); if (wasAtCapacity && this.config?.getDebugMode()) { debugLogger.debug( `ClearcutLogger: Dropped old event to prevent memory leak (queue size: ${this.events.size})`, ); } } enqueueLogEvent(event: LogEvent): void { try { this.enqueueHelper(event); } catch (error) { if (this.config?.getDebugMode()) { debugLogger.warn('ClearcutLogger: Failed to enqueue log event.', error); } } } async enqueueLogEventAfterExperimentsLoadAsync( event: LogEvent, ): Promise { try { // eslint-disable-next-line @typescript-eslint/no-floating-promises this.config?.getExperimentsAsync().then((experiments) => { if (experiments) { const exp_id_data: EventValue[] = [ { gemini_cli_key: EventMetadataKey.GEMINI_CLI_EXPERIMENT_IDS, value: experiments.experimentIds.toString() ?? 'NA', }, ]; event.event_metadata = [[...event.event_metadata[0], ...exp_id_data]]; } this.enqueueHelper(event, experiments?.experimentIds); }); } catch (error) { debugLogger.warn('ClearcutLogger: Failed to enqueue log event.', error); } } createBasicLogEvent( eventName: EventNames, data: EventValue[] = [], ): LogEvent { const email = this.userAccountManager.getCachedGoogleAccount(); const surface = determineSurface(); const ghWorkflowName = determineGHWorkflowName(); const ghEventName = determineGHEventName(); const ghPRNumber = determineGHPRNumber(); const ghIssueNumber = determineGHIssueNumber(); const ghCustomTrackingId = determineGHCustomTrackingId(); const baseMetadata: EventValue[] = [ ...data, { 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_OS, value: process.platform, }, ]; if (ghWorkflowName) { baseMetadata.push({ gemini_cli_key: EventMetadataKey.GEMINI_CLI_GH_WORKFLOW_NAME, value: ghWorkflowName, }); } if (this.hashedGHRepositoryName) { baseMetadata.push({ gemini_cli_key: EventMetadataKey.GEMINI_CLI_GH_REPOSITORY_NAME_HASH, value: this.hashedGHRepositoryName, }); } if (ghEventName) { baseMetadata.push({ gemini_cli_key: EventMetadataKey.GEMINI_CLI_GH_EVENT_NAME, value: ghEventName, }); } if (ghPRNumber) { baseMetadata.push({ gemini_cli_key: EventMetadataKey.GEMINI_CLI_GH_PR_NUMBER, value: ghPRNumber, }); } if (ghIssueNumber) { baseMetadata.push({ gemini_cli_key: EventMetadataKey.GEMINI_CLI_GH_ISSUE_NUMBER, value: ghIssueNumber, }); } if (ghCustomTrackingId) { baseMetadata.push({ gemini_cli_key: EventMetadataKey.GEMINI_CLI_GH_CUSTOM_TRACKING_ID, value: ghCustomTrackingId, }); } const logEvent: LogEvent = { console_type: 'GEMINI_CLI', application: 102, // GEMINI_CLI event_name: eventName as string, event_metadata: [baseMetadata], }; // Should log either email or install ID, not both. See go/cloudmill-1p-oss-instrumentation#define-sessionable-id if (email) { logEvent.client_email = email; } else { logEvent.client_install_id = this.installationManager.getInstallationId(); } return logEvent; } createLogEvent(eventName: EventNames, data: EventValue[] = []): LogEvent { if (eventName !== EventNames.START_SESSION) { data.push(...this.sessionData); } const totalAccounts = this.userAccountManager.getLifetimeGoogleAccounts(); data = this.addDefaultFields(data, totalAccounts); return this.createBasicLogEvent(eventName, data); } flushIfNeeded(): void { if (Date.now() - this.lastFlushTime < FLUSH_INTERVAL_MS) { return; } this.flushToClearcut().catch((error) => { debugLogger.debug('Error flushing to Clearcut:', error); }); } async flushToClearcut(): Promise { if (this.flushing) { if (this.config?.getDebugMode()) { debugLogger.debug( 'ClearcutLogger: Flush already in progress, marking pending flush.', ); } this.pendingFlush = true; return Promise.resolve({}); } this.flushing = true; if (this.config?.getDebugMode()) { debugLogger.log('Flushing log events to Clearcut.'); } // eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion const eventsToSend = this.events.toArray() as LogEventEntry[][]; this.events.clear(); const request: LogRequest[] = [ { log_source_name: 'CONCORD', request_time_ms: Date.now(), log_event: eventsToSend, }, ]; let result: LogResponse = {}; try { // eslint-disable-next-line no-restricted-syntax -- TODO: Migrate to safeFetch for SSRF protection const response = await fetch(CLEARCUT_URL, { method: 'POST', body: safeJsonStringify(request), headers: { 'Content-Type': 'application/json', }, }); const responseBody = await response.text(); if (response.status >= 200 && response.status < 300) { this.lastFlushTime = Date.now(); const nextRequestWaitMs = Number(JSON.parse(responseBody)[0]); result = { ...result, nextRequestWaitMs, }; } else { if (this.config?.getDebugMode()) { debugLogger.warn( `Error flushing log events: HTTP ${response.status}: ${response.statusText}`, ); } // Re-queue failed events for retry this.requeueFailedEvents(eventsToSend); } } catch (e: unknown) { if (this.config?.getDebugMode()) { // eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion debugLogger.warn('Error flushing log events:', e as Error); } // Re-queue failed events for retry this.requeueFailedEvents(eventsToSend); } this.flushing = false; // If a flush was requested while we were flushing, flush again if (this.pendingFlush) { this.pendingFlush = false; // Fire and forget the pending flush this.flushToClearcut().catch((error) => { if (this.config?.getDebugMode()) { debugLogger.debug('Error in pending flush to Clearcut:', error); } }); } return result; } async logStartSessionEvent(event: StartSessionEvent): Promise { const data: EventValue[] = [ { gemini_cli_key: EventMetadataKey.GEMINI_CLI_START_SESSION_MODEL, value: event.model, }, { gemini_cli_key: EventMetadataKey.GEMINI_CLI_START_SESSION_EMBEDDING_MODEL, value: event.embedding_model, }, { gemini_cli_key: EventMetadataKey.GEMINI_CLI_START_SESSION_SANDBOX, value: event.sandbox_enabled.toString(), }, { gemini_cli_key: EventMetadataKey.GEMINI_CLI_START_SESSION_CORE_TOOLS, value: event.core_tools_enabled, }, { gemini_cli_key: EventMetadataKey.GEMINI_CLI_START_SESSION_APPROVAL_MODE, value: event.approval_mode, }, { gemini_cli_key: EventMetadataKey.GEMINI_CLI_START_SESSION_API_KEY_ENABLED, value: event.api_key_enabled.toString(), }, { gemini_cli_key: EventMetadataKey.GEMINI_CLI_START_SESSION_VERTEX_API_ENABLED, value: event.vertex_ai_enabled.toString(), }, { gemini_cli_key: EventMetadataKey.GEMINI_CLI_START_SESSION_DEBUG_MODE_ENABLED, value: event.debug_enabled.toString(), }, { gemini_cli_key: EventMetadataKey.GEMINI_CLI_START_SESSION_VERTEX_API_ENABLED, value: event.vertex_ai_enabled.toString(), }, { gemini_cli_key: EventMetadataKey.GEMINI_CLI_START_SESSION_MCP_SERVERS, value: event.mcp_servers, }, { gemini_cli_key: EventMetadataKey.GEMINI_CLI_START_SESSION_VERTEX_API_ENABLED, value: event.vertex_ai_enabled.toString(), }, { gemini_cli_key: EventMetadataKey.GEMINI_CLI_START_SESSION_TELEMETRY_ENABLED, value: event.telemetry_enabled.toString(), }, { gemini_cli_key: EventMetadataKey.GEMINI_CLI_START_SESSION_TELEMETRY_LOG_USER_PROMPTS_ENABLED, value: event.telemetry_log_user_prompts_enabled.toString(), }, { gemini_cli_key: EventMetadataKey.GEMINI_CLI_START_SESSION_MCP_SERVERS_COUNT, value: event.mcp_servers_count ? event.mcp_servers_count.toString() : '', }, { gemini_cli_key: EventMetadataKey.GEMINI_CLI_START_SESSION_MCP_TOOLS_COUNT, value: event.mcp_tools_count?.toString() ?? '', }, { gemini_cli_key: EventMetadataKey.GEMINI_CLI_START_SESSION_MCP_TOOLS, value: event.mcp_tools ? event.mcp_tools : '', }, { gemini_cli_key: EventMetadataKey.GEMINI_CLI_START_SESSION_EXTENSIONS_COUNT, value: event.extensions_count.toString(), }, // We deliberately do not log the names of extensions here, to be safe. { gemini_cli_key: EventMetadataKey.GEMINI_CLI_START_SESSION_EXTENSION_IDS, value: event.extension_ids.toString(), }, ]; // Add hardware information only to the start session event const cpus = os.cpus(); if (cpus && cpus.length > 0) { data.push({ gemini_cli_key: EventMetadataKey.GEMINI_CLI_CPU_INFO, value: cpus[0].model, }); } data.push( { gemini_cli_key: EventMetadataKey.GEMINI_CLI_CPU_CORES, value: os.availableParallelism().toString(), }, { gemini_cli_key: EventMetadataKey.GEMINI_CLI_RAM_TOTAL_GB, value: (os.totalmem() / 1024 ** 3).toFixed(2).toString(), }, ); const gpuInfo = await getGpuInfo(); data.push({ gemini_cli_key: EventMetadataKey.GEMINI_CLI_GPU_INFO, value: gpuInfo, }); this.sessionData = data; // Flush after experiments finish loading from CCPA server // eslint-disable-next-line @typescript-eslint/no-floating-promises this.enqueueLogEventAfterExperimentsLoadAsync( this.createLogEvent(EventNames.START_SESSION, data), ).then(() => { this.flushToClearcut().catch((error) => { debugLogger.debug('Error flushing to Clearcut:', error); }); }); } 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), }, ]; this.enqueueLogEvent(this.createLogEvent(EventNames.NEW_PROMPT, data)); this.flushIfNeeded(); } logToolCallEvent(event: ToolCallEvent): void { const data: EventValue[] = [ { gemini_cli_key: EventMetadataKey.GEMINI_CLI_TOOL_CALL_NAME, value: JSON.stringify(event.function_name), }, { gemini_cli_key: EventMetadataKey.GEMINI_CLI_TOOL_CALL_DECISION, value: JSON.stringify(event.decision), }, { gemini_cli_key: EventMetadataKey.GEMINI_CLI_TOOL_CALL_SUCCESS, value: JSON.stringify(event.success), }, { gemini_cli_key: EventMetadataKey.GEMINI_CLI_TOOL_CALL_DURATION_MS, value: JSON.stringify(event.duration_ms), }, { gemini_cli_key: EventMetadataKey.GEMINI_CLI_TOOL_CALL_ERROR_TYPE, value: JSON.stringify(event.error_type), }, { gemini_cli_key: EventMetadataKey.GEMINI_CLI_TOOL_TYPE, value: JSON.stringify(event.tool_type), }, { gemini_cli_key: EventMetadataKey.GEMINI_CLI_TOOL_CALL_CONTENT_LENGTH, value: JSON.stringify(event.content_length), }, { gemini_cli_key: EventMetadataKey.GEMINI_CLI_TOOL_CALL_MCP_SERVER_NAME, value: JSON.stringify(event.mcp_server_name), }, ]; if (event.metadata) { const metadataMapping: { [key: string]: EventMetadataKey } = { model_added_lines: EventMetadataKey.GEMINI_CLI_AI_ADDED_LINES, model_removed_lines: EventMetadataKey.GEMINI_CLI_AI_REMOVED_LINES, model_added_chars: EventMetadataKey.GEMINI_CLI_AI_ADDED_CHARS, model_removed_chars: EventMetadataKey.GEMINI_CLI_AI_REMOVED_CHARS, user_added_lines: EventMetadataKey.GEMINI_CLI_USER_ADDED_LINES, user_removed_lines: EventMetadataKey.GEMINI_CLI_USER_REMOVED_LINES, user_added_chars: EventMetadataKey.GEMINI_CLI_USER_ADDED_CHARS, user_removed_chars: EventMetadataKey.GEMINI_CLI_USER_REMOVED_CHARS, }; if ( event.function_name === ASK_USER_TOOL_NAME && event.metadata['ask_user'] ) { // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment const askUser = event.metadata['ask_user']; const askUserMapping: { [key: string]: EventMetadataKey } = { question_types: EventMetadataKey.GEMINI_CLI_ASK_USER_QUESTION_TYPES, dismissed: EventMetadataKey.GEMINI_CLI_ASK_USER_DISMISSED, empty_submission: EventMetadataKey.GEMINI_CLI_ASK_USER_EMPTY_SUBMISSION, answer_count: EventMetadataKey.GEMINI_CLI_ASK_USER_ANSWER_COUNT, }; for (const [key, gemini_cli_key] of Object.entries(askUserMapping)) { if (askUser[key] !== undefined) { data.push({ gemini_cli_key, value: JSON.stringify(askUser[key]), }); } } } for (const [key, gemini_cli_key] of Object.entries(metadataMapping)) { if (event.metadata[key] !== undefined) { data.push({ gemini_cli_key, value: JSON.stringify(event.metadata[key]), }); } } } if (event.extension_id) { data.push({ gemini_cli_key: EventMetadataKey.GEMINI_CLI_EXTENSION_ID, value: event.extension_id, }); } const logEvent = this.createLogEvent(EventNames.TOOL_CALL, data); this.enqueueLogEvent(logEvent); this.flushIfNeeded(); } logFileOperationEvent(event: FileOperationEvent): void { const data: EventValue[] = [ { gemini_cli_key: EventMetadataKey.GEMINI_CLI_TOOL_CALL_NAME, value: JSON.stringify(event.tool_name), }, { gemini_cli_key: EventMetadataKey.GEMINI_CLI_FILE_OPERATION_TYPE, value: JSON.stringify(event.operation), }, { gemini_cli_key: EventMetadataKey.GEMINI_CLI_FILE_OPERATION_LINES, value: JSON.stringify(event.lines), }, { gemini_cli_key: EventMetadataKey.GEMINI_CLI_FILE_OPERATION_MIMETYPE, value: JSON.stringify(event.mimetype), }, { gemini_cli_key: EventMetadataKey.GEMINI_CLI_FILE_OPERATION_EXTENSION, value: JSON.stringify(event.extension), }, ]; if (event.programming_language) { data.push({ gemini_cli_key: EventMetadataKey.GEMINI_CLI_PROGRAMMING_LANGUAGE, value: event.programming_language, }); } const logEvent = this.createLogEvent(EventNames.FILE_OPERATION, data); this.enqueueLogEvent(logEvent); this.flushIfNeeded(); } logApiRequestEvent(event: ApiRequestEvent): void { const data: EventValue[] = [ { gemini_cli_key: EventMetadataKey.GEMINI_CLI_API_REQUEST_MODEL, value: JSON.stringify(event.model), }, ]; this.enqueueLogEvent(this.createLogEvent(EventNames.API_REQUEST, data)); this.flushIfNeeded(); } logApiResponseEvent(event: ApiResponseEvent): void { const data: EventValue[] = [ { gemini_cli_key: EventMetadataKey.GEMINI_CLI_API_RESPONSE_MODEL, value: JSON.stringify(event.model), }, { gemini_cli_key: EventMetadataKey.GEMINI_CLI_API_RESPONSE_STATUS_CODE, value: JSON.stringify(event.status_code), }, { gemini_cli_key: EventMetadataKey.GEMINI_CLI_API_RESPONSE_DURATION_MS, value: JSON.stringify(event.duration_ms), }, { gemini_cli_key: EventMetadataKey.GEMINI_CLI_API_RESPONSE_INPUT_TOKEN_COUNT, value: JSON.stringify(event.usage.input_token_count), }, { gemini_cli_key: EventMetadataKey.GEMINI_CLI_API_RESPONSE_OUTPUT_TOKEN_COUNT, value: JSON.stringify(event.usage.output_token_count), }, { gemini_cli_key: EventMetadataKey.GEMINI_CLI_API_RESPONSE_CACHED_TOKEN_COUNT, value: JSON.stringify(event.usage.cached_content_token_count), }, { gemini_cli_key: EventMetadataKey.GEMINI_CLI_API_RESPONSE_THINKING_TOKEN_COUNT, value: JSON.stringify(event.usage.thoughts_token_count), }, { gemini_cli_key: EventMetadataKey.GEMINI_CLI_API_RESPONSE_TOOL_TOKEN_COUNT, value: JSON.stringify(event.usage.tool_token_count), }, // Context breakdown fields are only populated on turn-ending responses // (when the user gets back control), not during intermediate tool-use // loops. Values still grow across turns as conversation history // accumulates, so downstream consumers should use the last event per // session (MAX) rather than summing across events. { gemini_cli_key: EventMetadataKey.GEMINI_CLI_API_RESPONSE_CONTEXT_BREAKDOWN_SYSTEM_INSTRUCTIONS, value: JSON.stringify( event.usage.context_breakdown?.system_instructions ?? 0, ), }, { gemini_cli_key: EventMetadataKey.GEMINI_CLI_API_RESPONSE_CONTEXT_BREAKDOWN_TOOL_DEFINITIONS, value: JSON.stringify( event.usage.context_breakdown?.tool_definitions ?? 0, ), }, { gemini_cli_key: EventMetadataKey.GEMINI_CLI_API_RESPONSE_CONTEXT_BREAKDOWN_HISTORY, value: JSON.stringify(event.usage.context_breakdown?.history ?? 0), }, { gemini_cli_key: EventMetadataKey.GEMINI_CLI_API_RESPONSE_CONTEXT_BREAKDOWN_TOOL_CALLS, value: JSON.stringify(event.usage.context_breakdown?.tool_calls ?? {}), }, { gemini_cli_key: EventMetadataKey.GEMINI_CLI_API_RESPONSE_CONTEXT_BREAKDOWN_MCP_SERVERS, value: JSON.stringify(event.usage.context_breakdown?.mcp_servers ?? 0), }, ]; this.enqueueLogEvent(this.createLogEvent(EventNames.API_RESPONSE, data)); this.flushIfNeeded(); } logApiErrorEvent(event: ApiErrorEvent): void { const data: EventValue[] = [ { gemini_cli_key: EventMetadataKey.GEMINI_CLI_API_ERROR_MODEL, value: JSON.stringify(event.model), }, { gemini_cli_key: EventMetadataKey.GEMINI_CLI_API_ERROR_TYPE, value: JSON.stringify(event.error_type), }, { gemini_cli_key: EventMetadataKey.GEMINI_CLI_API_ERROR_STATUS_CODE, value: JSON.stringify(event.status_code), }, { gemini_cli_key: EventMetadataKey.GEMINI_CLI_API_ERROR_DURATION_MS, value: JSON.stringify(event.duration_ms), }, ]; this.enqueueLogEvent(this.createLogEvent(EventNames.API_ERROR, data)); this.flushIfNeeded(); } logChatCompressionEvent(event: ChatCompressionEvent): void { const data: EventValue[] = [ { gemini_cli_key: EventMetadataKey.GEMINI_CLI_COMPRESSION_TOKENS_BEFORE, value: `${event.tokens_before}`, }, { gemini_cli_key: EventMetadataKey.GEMINI_CLI_COMPRESSION_TOKENS_AFTER, value: `${event.tokens_after}`, }, ]; this.enqueueLogEvent( this.createLogEvent(EventNames.CHAT_COMPRESSION, data), ); } logFlashFallbackEvent(): void { this.enqueueLogEvent(this.createLogEvent(EventNames.FLASH_FALLBACK, [])); this.flushToClearcut().catch((error) => { debugLogger.debug('Error flushing to Clearcut:', error); }); } logRipgrepFallbackEvent(): void { this.enqueueLogEvent(this.createLogEvent(EventNames.RIPGREP_FALLBACK, [])); this.flushToClearcut().catch((error) => { debugLogger.debug('Error flushing to Clearcut:', error); }); } logLoopDetectedEvent(event: LoopDetectedEvent): void { const data: EventValue[] = [ { gemini_cli_key: EventMetadataKey.GEMINI_CLI_LOOP_DETECTED_TYPE, value: JSON.stringify(event.loop_type), }, ]; if (event.confirmed_by_model) { data.push({ gemini_cli_key: EventMetadataKey.GEMINI_CLI_LOOP_DETECTED_CONFIRMED_BY_MODEL, value: event.confirmed_by_model, }); } this.enqueueLogEvent(this.createLogEvent(EventNames.LOOP_DETECTED, data)); this.flushIfNeeded(); } logLoopDetectionDisabledEvent(): void { const data: EventValue[] = []; this.enqueueLogEvent( this.createLogEvent(EventNames.LOOP_DETECTION_DISABLED, data), ); this.flushIfNeeded(); } logNextSpeakerCheck(event: NextSpeakerCheckEvent): void { const data: EventValue[] = [ { gemini_cli_key: EventMetadataKey.GEMINI_CLI_RESPONSE_FINISH_REASON, value: JSON.stringify(event.finish_reason), }, { gemini_cli_key: EventMetadataKey.GEMINI_CLI_NEXT_SPEAKER_CHECK_RESULT, value: JSON.stringify(event.result), }, ]; this.enqueueLogEvent( this.createLogEvent(EventNames.NEXT_SPEAKER_CHECK, data), ); this.flushIfNeeded(); } logSlashCommandEvent(event: SlashCommandEvent): void { const data: EventValue[] = [ { gemini_cli_key: EventMetadataKey.GEMINI_CLI_SLASH_COMMAND_NAME, value: JSON.stringify(event.command), }, ]; if (event.subcommand) { data.push({ gemini_cli_key: EventMetadataKey.GEMINI_CLI_SLASH_COMMAND_SUBCOMMAND, value: JSON.stringify(event.subcommand), }); } if (event.status) { data.push({ gemini_cli_key: EventMetadataKey.GEMINI_CLI_SLASH_COMMAND_STATUS, value: JSON.stringify(event.status), }); } if (event.extension_id) { data.push({ gemini_cli_key: EventMetadataKey.GEMINI_CLI_EXTENSION_ID, value: event.extension_id, }); } this.enqueueLogEvent(this.createLogEvent(EventNames.SLASH_COMMAND, data)); this.flushIfNeeded(); } logRewindEvent(event: RewindEvent): void { const data: EventValue[] = [ { gemini_cli_key: EventMetadataKey.GEMINI_CLI_REWIND_OUTCOME, value: event.outcome, }, ]; this.enqueueLogEvent(this.createLogEvent(EventNames.REWIND, data)); this.flushIfNeeded(); } logMalformedJsonResponseEvent(event: MalformedJsonResponseEvent): void { const data: EventValue[] = [ { gemini_cli_key: EventMetadataKey.GEMINI_CLI_MALFORMED_JSON_RESPONSE_MODEL, value: JSON.stringify(event.model), }, ]; this.enqueueLogEvent( this.createLogEvent(EventNames.MALFORMED_JSON_RESPONSE, data), ); this.flushIfNeeded(); } logIdeConnectionEvent(event: IdeConnectionEvent): void { const data: EventValue[] = [ { gemini_cli_key: EventMetadataKey.GEMINI_CLI_IDE_CONNECTION_TYPE, value: JSON.stringify(event.connection_type), }, ]; // Flush after experiments finish loading from CCPA server // eslint-disable-next-line @typescript-eslint/no-floating-promises this.enqueueLogEventAfterExperimentsLoadAsync( this.createLogEvent(EventNames.START_SESSION, data), ).then(() => { this.flushToClearcut().catch((error) => { debugLogger.debug('Error flushing to Clearcut:', error); }); }); } logConversationFinishedEvent(event: ConversationFinishedEvent): void { const data: EventValue[] = [ { gemini_cli_key: EventMetadataKey.GEMINI_CLI_SESSION_ID, value: this.config?.getSessionId() ?? '', }, { gemini_cli_key: EventMetadataKey.GEMINI_CLI_CONVERSATION_TURN_COUNT, value: JSON.stringify(event.turnCount), }, { gemini_cli_key: EventMetadataKey.GEMINI_CLI_APPROVAL_MODE, value: event.approvalMode, }, ]; this.enqueueLogEvent( this.createLogEvent(EventNames.CONVERSATION_FINISHED, data), ); this.flushIfNeeded(); } logEndSessionEvent(): void { // Flush immediately on session end. this.enqueueLogEvent(this.createLogEvent(EventNames.END_SESSION, [])); this.flushToClearcut().catch((error) => { debugLogger.debug('Error flushing to Clearcut:', error); }); } logInvalidChunkEvent(event: InvalidChunkEvent): void { const data: EventValue[] = []; if (event.error_message) { data.push({ gemini_cli_key: EventMetadataKey.GEMINI_CLI_INVALID_CHUNK_ERROR_MESSAGE, value: event.error_message, }); } this.enqueueLogEvent(this.createLogEvent(EventNames.INVALID_CHUNK, data)); this.flushIfNeeded(); } logContentRetryEvent(event: ContentRetryEvent): void { const data: EventValue[] = [ { gemini_cli_key: EventMetadataKey.GEMINI_CLI_CONTENT_RETRY_ATTEMPT_NUMBER, value: String(event.attempt_number), }, { gemini_cli_key: EventMetadataKey.GEMINI_CLI_CONTENT_RETRY_ERROR_TYPE, value: event.error_type, }, { gemini_cli_key: EventMetadataKey.GEMINI_CLI_CONTENT_RETRY_DELAY_MS, value: String(event.retry_delay_ms), }, { gemini_cli_key: EventMetadataKey.GEMINI_CLI_API_REQUEST_MODEL, value: event.model, }, ]; this.enqueueLogEvent(this.createLogEvent(EventNames.CONTENT_RETRY, data)); this.flushIfNeeded(); } logContentRetryFailureEvent(event: ContentRetryFailureEvent): void { const data: EventValue[] = [ { gemini_cli_key: EventMetadataKey.GEMINI_CLI_CONTENT_RETRY_FAILURE_TOTAL_ATTEMPTS, value: String(event.total_attempts), }, { gemini_cli_key: EventMetadataKey.GEMINI_CLI_CONTENT_RETRY_FAILURE_FINAL_ERROR_TYPE, value: event.final_error_type, }, { gemini_cli_key: EventMetadataKey.GEMINI_CLI_API_REQUEST_MODEL, value: event.model, }, ]; if (event.total_duration_ms) { data.push({ gemini_cli_key: EventMetadataKey.GEMINI_CLI_CONTENT_RETRY_FAILURE_TOTAL_DURATION_MS, value: String(event.total_duration_ms), }); } this.enqueueLogEvent( this.createLogEvent(EventNames.CONTENT_RETRY_FAILURE, data), ); this.flushIfNeeded(); } async logExtensionInstallEvent(event: ExtensionInstallEvent): Promise { const data: EventValue[] = [ { gemini_cli_key: EventMetadataKey.GEMINI_CLI_EXTENSION_NAME, value: event.hashed_extension_name, }, { gemini_cli_key: EventMetadataKey.GEMINI_CLI_EXTENSION_ID, value: event.extension_id, }, { gemini_cli_key: EventMetadataKey.GEMINI_CLI_EXTENSION_VERSION, value: event.extension_version, }, { gemini_cli_key: EventMetadataKey.GEMINI_CLI_EXTENSION_SOURCE, value: event.extension_source, }, { gemini_cli_key: EventMetadataKey.GEMINI_CLI_EXTENSION_INSTALL_STATUS, value: event.status, }, ]; this.enqueueLogEvent( this.createBasicLogEvent(EventNames.EXTENSION_INSTALL, data), ); await this.flushToClearcut().catch((error) => { debugLogger.debug('Error flushing to Clearcut:', error); }); } async logExtensionUninstallEvent( event: ExtensionUninstallEvent, ): Promise { const data: EventValue[] = [ { gemini_cli_key: EventMetadataKey.GEMINI_CLI_EXTENSION_NAME, value: event.hashed_extension_name, }, { gemini_cli_key: EventMetadataKey.GEMINI_CLI_EXTENSION_ID, value: event.extension_id, }, { gemini_cli_key: EventMetadataKey.GEMINI_CLI_EXTENSION_UNINSTALL_STATUS, value: event.status, }, ]; this.enqueueLogEvent( this.createBasicLogEvent(EventNames.EXTENSION_UNINSTALL, data), ); await this.flushToClearcut().catch((error) => { debugLogger.debug('Error flushing to Clearcut:', error); }); } async logExtensionUpdateEvent(event: ExtensionUpdateEvent): Promise { const data: EventValue[] = [ { gemini_cli_key: EventMetadataKey.GEMINI_CLI_EXTENSION_NAME, value: event.hashed_extension_name, }, { gemini_cli_key: EventMetadataKey.GEMINI_CLI_EXTENSION_ID, value: event.extension_id, }, { gemini_cli_key: EventMetadataKey.GEMINI_CLI_EXTENSION_VERSION, value: event.extension_version, }, { gemini_cli_key: EventMetadataKey.GEMINI_CLI_EXTENSION_PREVIOUS_VERSION, value: event.extension_previous_version, }, { gemini_cli_key: EventMetadataKey.GEMINI_CLI_EXTENSION_SOURCE, value: event.extension_source, }, { gemini_cli_key: EventMetadataKey.GEMINI_CLI_EXTENSION_UPDATE_STATUS, value: event.status, }, ]; this.enqueueLogEvent( this.createBasicLogEvent(EventNames.EXTENSION_UPDATE, data), ); await this.flushToClearcut().catch((error) => { debugLogger.debug('Error flushing to Clearcut:', error); }); } logToolOutputTruncatedEvent(event: ToolOutputTruncatedEvent): void { const data: EventValue[] = [ { gemini_cli_key: EventMetadataKey.GEMINI_CLI_TOOL_CALL_NAME, value: JSON.stringify(event.tool_name), }, { gemini_cli_key: EventMetadataKey.GEMINI_CLI_TOOL_OUTPUT_TRUNCATED_ORIGINAL_LENGTH, value: JSON.stringify(event.original_content_length), }, { gemini_cli_key: EventMetadataKey.GEMINI_CLI_TOOL_OUTPUT_TRUNCATED_TRUNCATED_LENGTH, value: JSON.stringify(event.truncated_content_length), }, { gemini_cli_key: EventMetadataKey.GEMINI_CLI_TOOL_OUTPUT_TRUNCATED_THRESHOLD, value: JSON.stringify(event.threshold), }, ]; const logEvent = this.createLogEvent( EventNames.TOOL_OUTPUT_TRUNCATED, data, ); this.enqueueLogEvent(logEvent); this.flushIfNeeded(); } logToolOutputMaskingEvent(event: ToolOutputMaskingEvent): void { const data: EventValue[] = [ { gemini_cli_key: EventMetadataKey.GEMINI_CLI_TOOL_OUTPUT_MASKING_TOKENS_BEFORE, value: event.tokens_before.toString(), }, { gemini_cli_key: EventMetadataKey.GEMINI_CLI_TOOL_OUTPUT_MASKING_TOKENS_AFTER, value: event.tokens_after.toString(), }, { gemini_cli_key: EventMetadataKey.GEMINI_CLI_TOOL_OUTPUT_MASKING_MASKED_COUNT, value: event.masked_count.toString(), }, { gemini_cli_key: EventMetadataKey.GEMINI_CLI_TOOL_OUTPUT_MASKING_TOTAL_PRUNABLE_TOKENS, value: event.total_prunable_tokens.toString(), }, ]; this.enqueueLogEvent( this.createLogEvent(EventNames.TOOL_OUTPUT_MASKING, data), ); this.flushIfNeeded(); } logModelRoutingEvent(event: ModelRoutingEvent): void { const data: EventValue[] = [ { gemini_cli_key: EventMetadataKey.GEMINI_CLI_ROUTING_DECISION, value: event.decision_model, }, { gemini_cli_key: EventMetadataKey.GEMINI_CLI_ROUTING_DECISION_SOURCE, value: event.decision_source, }, { gemini_cli_key: EventMetadataKey.GEMINI_CLI_ROUTING_LATENCY_MS, value: event.routing_latency_ms.toString(), }, { gemini_cli_key: EventMetadataKey.GEMINI_CLI_ROUTING_FAILURE, value: event.failed.toString(), }, ]; if (event.error_message) { data.push({ gemini_cli_key: EventMetadataKey.GEMINI_CLI_ROUTING_FAILURE_REASON, value: event.error_message, }); } if (event.reasoning && this.config?.getTelemetryLogPromptsEnabled()) { data.push({ gemini_cli_key: EventMetadataKey.GEMINI_CLI_ROUTING_REASONING, value: event.reasoning, }); } if (event.enable_numerical_routing !== undefined) { data.push({ gemini_cli_key: EventMetadataKey.GEMINI_CLI_ROUTING_NUMERICAL_ENABLED, value: event.enable_numerical_routing.toString(), }); } if (event.classifier_threshold) { data.push({ gemini_cli_key: EventMetadataKey.GEMINI_CLI_ROUTING_CLASSIFIER_THRESHOLD, value: event.classifier_threshold, }); } this.enqueueLogEvent(this.createLogEvent(EventNames.MODEL_ROUTING, data)); this.flushIfNeeded(); } async logExtensionEnableEvent(event: ExtensionEnableEvent): Promise { const data: EventValue[] = [ { gemini_cli_key: EventMetadataKey.GEMINI_CLI_EXTENSION_NAME, value: event.hashed_extension_name, }, { gemini_cli_key: EventMetadataKey.GEMINI_CLI_EXTENSION_ID, value: event.extension_id, }, { gemini_cli_key: EventMetadataKey.GEMINI_CLI_EXTENSION_ENABLE_SETTING_SCOPE, value: event.setting_scope, }, ]; this.enqueueLogEvent( this.createBasicLogEvent(EventNames.EXTENSION_ENABLE, data), ); await this.flushToClearcut().catch((error) => { debugLogger.debug('Error flushing to Clearcut:', error); }); } logModelSlashCommandEvent(event: ModelSlashCommandEvent): void { const data: EventValue[] = [ { gemini_cli_key: EventMetadataKey.GEMINI_CLI_MODEL_SLASH_COMMAND, value: event.model_name, }, ]; this.enqueueLogEvent( this.createLogEvent(EventNames.MODEL_SLASH_COMMAND, data), ); this.flushIfNeeded(); } async logExtensionDisableEvent(event: ExtensionDisableEvent): Promise { const data: EventValue[] = [ { gemini_cli_key: EventMetadataKey.GEMINI_CLI_EXTENSION_NAME, value: event.hashed_extension_name, }, { gemini_cli_key: EventMetadataKey.GEMINI_CLI_EXTENSION_ID, value: event.extension_id, }, { gemini_cli_key: EventMetadataKey.GEMINI_CLI_EXTENSION_DISABLE_SETTING_SCOPE, value: event.setting_scope, }, ]; this.enqueueLogEvent( this.createBasicLogEvent(EventNames.EXTENSION_DISABLE, data), ); await this.flushToClearcut().catch((error) => { debugLogger.debug('Error flushing to Clearcut:', error); }); } logEditStrategyEvent(event: EditStrategyEvent): void { const data: EventValue[] = [ { gemini_cli_key: EventMetadataKey.GEMINI_CLI_EDIT_STRATEGY, value: event.strategy, }, ]; this.enqueueLogEvent(this.createLogEvent(EventNames.EDIT_STRATEGY, data)); this.flushIfNeeded(); } logEditCorrectionEvent(event: EditCorrectionEvent): void { const data: EventValue[] = [ { gemini_cli_key: EventMetadataKey.GEMINI_CLI_EDIT_CORRECTION, value: event.correction, }, ]; this.enqueueLogEvent(this.createLogEvent(EventNames.EDIT_CORRECTION, data)); this.flushIfNeeded(); } logAgentStartEvent(event: AgentStartEvent): void { const data: EventValue[] = [ { gemini_cli_key: EventMetadataKey.GEMINI_CLI_AGENT_ID, value: event.agent_id, }, { gemini_cli_key: EventMetadataKey.GEMINI_CLI_AGENT_NAME, value: event.agent_name, }, ]; this.enqueueLogEvent(this.createLogEvent(EventNames.AGENT_START, data)); this.flushIfNeeded(); } logAgentFinishEvent(event: AgentFinishEvent): void { const data: EventValue[] = [ { gemini_cli_key: EventMetadataKey.GEMINI_CLI_AGENT_ID, value: event.agent_id, }, { gemini_cli_key: EventMetadataKey.GEMINI_CLI_AGENT_NAME, value: event.agent_name, }, { gemini_cli_key: EventMetadataKey.GEMINI_CLI_AGENT_DURATION_MS, value: event.duration_ms.toString(), }, { gemini_cli_key: EventMetadataKey.GEMINI_CLI_AGENT_TURN_COUNT, value: event.turn_count.toString(), }, { gemini_cli_key: EventMetadataKey.GEMINI_CLI_AGENT_TERMINATE_REASON, value: event.terminate_reason, }, ]; this.enqueueLogEvent(this.createLogEvent(EventNames.AGENT_FINISH, data)); this.flushIfNeeded(); } logRecoveryAttemptEvent(event: RecoveryAttemptEvent): void { const data: EventValue[] = [ { gemini_cli_key: EventMetadataKey.GEMINI_CLI_AGENT_ID, value: event.agent_id, }, { gemini_cli_key: EventMetadataKey.GEMINI_CLI_AGENT_NAME, value: event.agent_name, }, { gemini_cli_key: EventMetadataKey.GEMINI_CLI_AGENT_RECOVERY_REASON, value: event.reason, }, { gemini_cli_key: EventMetadataKey.GEMINI_CLI_AGENT_RECOVERY_DURATION_MS, value: event.duration_ms.toString(), }, { gemini_cli_key: EventMetadataKey.GEMINI_CLI_AGENT_RECOVERY_SUCCESS, value: event.success.toString(), }, { gemini_cli_key: EventMetadataKey.GEMINI_CLI_AGENT_TURN_COUNT, value: event.turn_count.toString(), }, ]; this.enqueueLogEvent( this.createLogEvent(EventNames.RECOVERY_ATTEMPT, data), ); this.flushIfNeeded(); } logWebFetchFallbackAttemptEvent(event: WebFetchFallbackAttemptEvent): void { const data: EventValue[] = [ { gemini_cli_key: EventMetadataKey.GEMINI_CLI_WEB_FETCH_FALLBACK_REASON, value: event.reason, }, ]; this.enqueueLogEvent( this.createLogEvent(EventNames.WEB_FETCH_FALLBACK_ATTEMPT, data), ); this.flushIfNeeded(); } logLlmLoopCheckEvent(event: LlmLoopCheckEvent): void { const data: EventValue[] = [ { gemini_cli_key: EventMetadataKey.GEMINI_CLI_PROMPT_ID, value: event.prompt_id, }, { gemini_cli_key: EventMetadataKey.GEMINI_CLI_LLM_LOOP_CHECK_FLASH_CONFIDENCE, value: event.flash_confidence.toString(), }, { gemini_cli_key: EventMetadataKey.GEMINI_CLI_LLM_LOOP_CHECK_MAIN_MODEL, value: event.main_model, }, { gemini_cli_key: EventMetadataKey.GEMINI_CLI_LLM_LOOP_CHECK_MAIN_MODEL_CONFIDENCE, value: event.main_model_confidence.toString(), }, ]; this.enqueueLogEvent(this.createLogEvent(EventNames.LLM_LOOP_CHECK, data)); this.flushIfNeeded(); } logHookCallEvent(event: HookCallEvent): void { const data: EventValue[] = [ { gemini_cli_key: EventMetadataKey.GEMINI_CLI_HOOK_EVENT_NAME, value: event.hook_event_name, }, { gemini_cli_key: EventMetadataKey.GEMINI_CLI_HOOK_DURATION_MS, value: event.duration_ms.toString(), }, { gemini_cli_key: EventMetadataKey.GEMINI_CLI_HOOK_SUCCESS, value: event.success.toString(), }, ]; if (event.exit_code !== undefined) { data.push({ gemini_cli_key: EventMetadataKey.GEMINI_CLI_HOOK_EXIT_CODE, value: event.exit_code.toString(), }); } this.enqueueLogEvent(this.createLogEvent(EventNames.HOOK_CALL, data)); this.flushIfNeeded(); } logApprovalModeSwitchEvent(event: ApprovalModeSwitchEvent): void { const data: EventValue[] = [ { gemini_cli_key: EventMetadataKey.GEMINI_CLI_ACTIVE_APPROVAL_MODE, value: event.from_mode, }, { gemini_cli_key: EventMetadataKey.GEMINI_CLI_APPROVAL_MODE_TO, value: event.to_mode, }, ]; this.enqueueLogEvent( this.createLogEvent(EventNames.APPROVAL_MODE_SWITCH, data), ); this.flushIfNeeded(); } logApprovalModeDurationEvent(event: ApprovalModeDurationEvent): void { const data: EventValue[] = [ { gemini_cli_key: EventMetadataKey.GEMINI_CLI_ACTIVE_APPROVAL_MODE, value: event.mode, }, { gemini_cli_key: EventMetadataKey.GEMINI_CLI_APPROVAL_MODE_DURATION_MS, value: event.duration_ms.toString(), }, ]; this.enqueueLogEvent( this.createLogEvent(EventNames.APPROVAL_MODE_DURATION, data), ); this.flushIfNeeded(); } logPlanExecutionEvent(event: PlanExecutionEvent): void { const data: EventValue[] = [ { gemini_cli_key: EventMetadataKey.GEMINI_CLI_APPROVAL_MODE, value: event.approval_mode, }, ]; this.enqueueLogEvent(this.createLogEvent(EventNames.PLAN_EXECUTION, data)); this.flushIfNeeded(); } logKeychainAvailabilityEvent(event: KeychainAvailabilityEvent): void { const data: EventValue[] = [ { gemini_cli_key: EventMetadataKey.GEMINI_CLI_KEYCHAIN_AVAILABLE, value: JSON.stringify(event.available), }, ]; this.enqueueLogEvent( this.createLogEvent(EventNames.KEYCHAIN_AVAILABILITY, data), ); this.flushIfNeeded(); } logTokenStorageInitializationEvent( event: TokenStorageInitializationEvent, ): void { const data: EventValue[] = [ { gemini_cli_key: EventMetadataKey.GEMINI_CLI_TOKEN_STORAGE_TYPE, value: event.type, }, { gemini_cli_key: EventMetadataKey.GEMINI_CLI_TOKEN_STORAGE_FORCED, value: JSON.stringify(event.forced), }, ]; this.enqueueLogEvent( this.createLogEvent(EventNames.TOKEN_STORAGE_INITIALIZATION, data), ); this.flushIfNeeded(); } logStartupStatsEvent(event: StartupStatsEvent): void { const data: EventValue[] = [ { gemini_cli_key: EventMetadataKey.GEMINI_CLI_STARTUP_PHASES, value: JSON.stringify(event.phases), }, { gemini_cli_key: EventMetadataKey.GEMINI_CLI_STARTUP_OS_PLATFORM, value: event.os_platform, }, { gemini_cli_key: EventMetadataKey.GEMINI_CLI_STARTUP_OS_RELEASE, value: event.os_release, }, { gemini_cli_key: EventMetadataKey.GEMINI_CLI_STARTUP_IS_DOCKER, value: JSON.stringify(event.is_docker), }, ]; this.enqueueLogEvent(this.createLogEvent(EventNames.STARTUP_STATS, data)); this.flushIfNeeded(); } /** * Adds default fields to data, and returns a new data array. This fields * should exist on all log events. */ addDefaultFields(data: EventValue[], totalAccounts: number): EventValue[] { 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_PROMPT_ID, value: this.promptId, }, { gemini_cli_key: EventMetadataKey.GEMINI_CLI_NODE_VERSION, value: process.versions.node, }, { gemini_cli_key: EventMetadataKey.GEMINI_CLI_USER_SETTINGS, value: this.getConfigJson(), }, { gemini_cli_key: EventMetadataKey.GEMINI_CLI_INTERACTIVE, value: this.config?.isInteractive().toString() ?? 'false', }, { gemini_cli_key: EventMetadataKey.GEMINI_CLI_ACTIVE_APPROVAL_MODE, value: typeof this.config?.getPolicyEngine === 'function' && typeof this.config.getPolicyEngine()?.getApprovalMode === 'function' ? this.config.getPolicyEngine().getApprovalMode() : '', }, ]; if (this.config?.getExperiments()) { defaultLogMetadata.push({ gemini_cli_key: EventMetadataKey.GEMINI_CLI_EXPERIMENT_IDS, value: this.config?.getExperiments()?.experimentIds.toString() ?? 'NA', }); } return [...data, ...defaultLogMetadata]; } getProxyAgent() { const proxyUrl = this.config?.getProxy(); if (!proxyUrl) return undefined; // undici which is widely used in the repo can only support http & https proxy protocol, // https://github.com/nodejs/undici/issues/2224 if (proxyUrl.startsWith('http')) { return new HttpsProxyAgent(proxyUrl); } else { throw new Error('Unsupported proxy type'); } } getConfigJson() { return safeJsonStringifyBooleanValuesOnly(this.config); } shutdown() { this.logEndSessionEvent(); } private requeueFailedEvents(eventsToSend: LogEventEntry[][]): void { // Add the events back to the front of the queue to be retried, but limit retry queue size const eventsToRetry = eventsToSend.slice(-MAX_RETRY_EVENTS); // Keep only the most recent events // Log a warning if we're dropping events if (eventsToSend.length > MAX_RETRY_EVENTS && this.config?.getDebugMode()) { debugLogger.warn( `ClearcutLogger: Dropping ${ eventsToSend.length - MAX_RETRY_EVENTS } events due to retry queue limit. Total events: ${ eventsToSend.length }, keeping: ${MAX_RETRY_EVENTS}`, ); } // Determine how many events can be re-queued const availableSpace = MAX_EVENTS - this.events.size; const numEventsToRequeue = Math.min(eventsToRetry.length, availableSpace); if (numEventsToRequeue === 0) { if (this.config?.getDebugMode()) { debugLogger.debug( `ClearcutLogger: No events re-queued (queue size: ${this.events.size})`, ); } return; } // Get the most recent events to re-queue const eventsToRequeue = eventsToRetry.slice( eventsToRetry.length - numEventsToRequeue, ); // Prepend events to the front of the deque to be retried first. // We iterate backwards to maintain the original order of the failed events. for (let i = eventsToRequeue.length - 1; i >= 0; i--) { this.events.unshift(eventsToRequeue[i]); } // Clear any potential overflow while (this.events.size > MAX_EVENTS) { this.events.pop(); } if (this.config?.getDebugMode()) { debugLogger.debug( `ClearcutLogger: Re-queued ${numEventsToRequeue} events for retry (queue size: ${this.events.size})`, ); } } } export const TEST_ONLY = { MAX_RETRY_EVENTS, MAX_EVENTS, refreshGpuInfo, resetCachedGpuInfoForTesting: () => { cachedGpuInfo = undefined; }, };