mirror of
https://github.com/google-gemini/gemini-cli.git
synced 2026-03-10 14:10:37 -07:00
1914 lines
57 KiB
TypeScript
1914 lines
57 KiB
TypeScript
/**
|
|
* @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<void> {
|
|
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<string> {
|
|
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<LogEventEntry[]>;
|
|
|
|
/**
|
|
* 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<LogEventEntry[]>(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<void> {
|
|
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<LogResponse> {
|
|
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<void> {
|
|
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<void> {
|
|
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<void> {
|
|
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<void> {
|
|
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<void> {
|
|
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<void> {
|
|
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;
|
|
},
|
|
};
|