From 8174e1d5b82610d5b05f4dc6ce64ed9a2a1814f8 Mon Sep 17 00:00:00 2001 From: Victor May Date: Wed, 1 Oct 2025 17:53:53 -0400 Subject: [PATCH] Smart Edit Strategy Logging (#10345) Co-authored-by: gemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com> --- .../clearcut-logger/clearcut-logger.ts | 16 +++++++++++++ .../clearcut-logger/event-metadata-key.ts | 3 +++ packages/core/src/telemetry/constants.ts | 1 + packages/core/src/telemetry/loggers.ts | 23 +++++++++++++++++++ packages/core/src/telemetry/types.ts | 12 ++++++++++ packages/core/src/tools/smart-edit.test.ts | 14 +++++++---- packages/core/src/tools/smart-edit.ts | 13 +++++++++-- 7 files changed, 76 insertions(+), 6 deletions(-) diff --git a/packages/core/src/telemetry/clearcut-logger/clearcut-logger.ts b/packages/core/src/telemetry/clearcut-logger/clearcut-logger.ts index 2489ac9962..faf2358ed4 100644 --- a/packages/core/src/telemetry/clearcut-logger/clearcut-logger.ts +++ b/packages/core/src/telemetry/clearcut-logger/clearcut-logger.ts @@ -31,6 +31,7 @@ import type { ExtensionEnableEvent, ModelSlashCommandEvent, ExtensionDisableEvent, + SmartEditStrategyEvent, } from '../types.js'; import { EventMetadataKey } from './event-metadata-key.js'; import type { Config } from '../../config/config.js'; @@ -75,6 +76,7 @@ export enum EventNames { TOOL_OUTPUT_TRUNCATED = 'tool_output_truncated', MODEL_ROUTING = 'model_routing', MODEL_SLASH_COMMAND = 'model_slash_command', + SMART_EDIT_STRATEGY = 'smart_edit_strategy', } export interface LogResponse { @@ -1039,6 +1041,20 @@ export class ClearcutLogger { this.flushIfNeeded(); } + logSmartEditStrategyEvent(event: SmartEditStrategyEvent): void { + const data: EventValue[] = [ + { + gemini_cli_key: EventMetadataKey.GEMINI_CLI_SMART_EDIT_STRATEGY, + value: event.strategy, + }, + ]; + + this.enqueueLogEvent( + this.createLogEvent(EventNames.SMART_EDIT_STRATEGY, data), + ); + this.flushIfNeeded(); + } + /** * Adds default fields to data, and returns a new data array. This fields * should exist on all log events. diff --git a/packages/core/src/telemetry/clearcut-logger/event-metadata-key.ts b/packages/core/src/telemetry/clearcut-logger/event-metadata-key.ts index 06f0f82a6d..6cf9f6cab0 100644 --- a/packages/core/src/telemetry/clearcut-logger/event-metadata-key.ts +++ b/packages/core/src/telemetry/clearcut-logger/event-metadata-key.ts @@ -416,4 +416,7 @@ export enum EventMetadataKey { // Logs an event when the user uses the /model command. GEMINI_CLI_MODEL_SLASH_COMMAND = 108, + + // Logs a smart edit tool strategy choice. + GEMINI_CLI_SMART_EDIT_STRATEGY = 109, } diff --git a/packages/core/src/telemetry/constants.ts b/packages/core/src/telemetry/constants.ts index 4815f8b6dc..bc5f68ef11 100644 --- a/packages/core/src/telemetry/constants.ts +++ b/packages/core/src/telemetry/constants.ts @@ -31,6 +31,7 @@ export const EVENT_CONTENT_RETRY_FAILURE = 'gemini_cli.chat.content_retry_failure'; export const EVENT_FILE_OPERATION = 'gemini_cli.file_operation'; export const EVENT_MODEL_SLASH_COMMAND = 'gemini_cli.slash_command.model'; +export const EVENT_SMART_EDIT_STRATEGY = 'gemini_cli.smart_edit.strategy'; export const EVENT_MODEL_ROUTING = 'gemini_cli.model_routing'; // Performance Events diff --git a/packages/core/src/telemetry/loggers.ts b/packages/core/src/telemetry/loggers.ts index d1a466ad4e..0efc394ced 100644 --- a/packages/core/src/telemetry/loggers.ts +++ b/packages/core/src/telemetry/loggers.ts @@ -34,6 +34,7 @@ import { EVENT_EXTENSION_INSTALL, EVENT_MODEL_SLASH_COMMAND, EVENT_EXTENSION_DISABLE, + EVENT_SMART_EDIT_STRATEGY, } from './constants.js'; import type { ApiErrorEvent, @@ -64,6 +65,7 @@ import type { ExtensionUninstallEvent, ExtensionInstallEvent, ModelSlashCommandEvent, + SmartEditStrategyEvent, } from './types.js'; import { recordApiErrorMetrics, @@ -816,3 +818,24 @@ export function logExtensionDisable( }; logger.emit(logRecord); } + +export function logSmartEditStrategy( + config: Config, + event: SmartEditStrategyEvent, +): void { + ClearcutLogger.getInstance(config)?.logSmartEditStrategyEvent(event); + if (!isTelemetrySdkInitialized()) return; + + const attributes: LogAttributes = { + ...getCommonAttributes(config), + ...event, + 'event.name': EVENT_SMART_EDIT_STRATEGY, + }; + + const logger = logs.getLogger(SERVICE_NAME); + const logRecord: LogRecord = { + body: `Smart Edit Tool Strategy: ${event.strategy}`, + attributes, + }; + logger.emit(logRecord); +} diff --git a/packages/core/src/telemetry/types.ts b/packages/core/src/telemetry/types.ts index e2012f3628..b83a218b4d 100644 --- a/packages/core/src/telemetry/types.ts +++ b/packages/core/src/telemetry/types.ts @@ -702,3 +702,15 @@ export class ExtensionDisableEvent implements BaseTelemetryEvent { this.setting_scope = settingScope; } } + +export class SmartEditStrategyEvent implements BaseTelemetryEvent { + 'event.name': 'smart_edit_strategy'; + 'event.timestamp': string; + strategy: string; + + constructor(strategy: string) { + this['event.name'] = 'smart_edit_strategy'; + this['event.timestamp'] = new Date().toISOString(); + this.strategy = strategy; + } +} diff --git a/packages/core/src/tools/smart-edit.test.ts b/packages/core/src/tools/smart-edit.test.ts index 9462ded913..d3cad90ad2 100644 --- a/packages/core/src/tools/smart-edit.test.ts +++ b/packages/core/src/tools/smart-edit.test.ts @@ -81,6 +81,12 @@ describe('SmartEditTool', () => { } as unknown as BaseLlmClient; mockConfig = { + getUsageStatisticsEnabled: vi.fn(() => true), + getSessionId: vi.fn(() => 'mock-session-id'), + getContentGeneratorConfig: vi.fn(() => ({ authType: 'mock' })), + getUseSmartEdit: vi.fn(() => false), + getUseModelRouter: vi.fn(() => false), + getProxy: vi.fn(() => undefined), getGeminiClient: vi.fn().mockReturnValue(geminiClient), getBaseLlmClient: vi.fn().mockReturnValue(baseLlmClient), getTargetDir: () => rootDir, @@ -195,7 +201,7 @@ describe('SmartEditTool', () => { it('should perform an exact replacement', async () => { const content = 'hello world'; - const result = await calculateReplacement({ + const result = await calculateReplacement(mockConfig, { params: { file_path: 'test.txt', instruction: 'test', @@ -211,7 +217,7 @@ describe('SmartEditTool', () => { it('should perform a flexible, whitespace-insensitive replacement', async () => { const content = ' hello\n world\n'; - const result = await calculateReplacement({ + const result = await calculateReplacement(mockConfig, { params: { file_path: 'test.txt', instruction: 'test', @@ -227,7 +233,7 @@ describe('SmartEditTool', () => { it('should return 0 occurrences if no match is found', async () => { const content = 'hello world'; - const result = await calculateReplacement({ + const result = await calculateReplacement(mockConfig, { params: { file_path: 'test.txt', instruction: 'test', @@ -245,7 +251,7 @@ describe('SmartEditTool', () => { // This case would fail with the previous exact and line-trimming flexible logic // because the whitespace *within* the line is different. const content = ' function myFunc( a, b ) {\n return a + b;\n }'; - const result = await calculateReplacement({ + const result = await calculateReplacement(mockConfig, { params: { file_path: 'test.js', instruction: 'test', diff --git a/packages/core/src/tools/smart-edit.ts b/packages/core/src/tools/smart-edit.ts index ea93e9023b..5efdec620e 100644 --- a/packages/core/src/tools/smart-edit.ts +++ b/packages/core/src/tools/smart-edit.ts @@ -32,6 +32,8 @@ import { IdeClient } from '../ide/ide-client.js'; import { FixLLMEditWithInstruction } from '../utils/llm-edit-fixer.js'; import { applyReplacement } from './edit.js'; import { safeLiteralReplace } from '../utils/textUtils.js'; +import { SmartEditStrategyEvent } from '../telemetry/types.js'; +import { logSmartEditStrategy } from '../telemetry/loggers.js'; interface ReplacementContext { params: EditToolParams; @@ -229,6 +231,7 @@ function detectLineEnding(content: string): '\r\n' | '\n' { } export async function calculateReplacement( + config: Config, context: ReplacementContext, ): Promise { const { currentContent, params } = context; @@ -247,16 +250,22 @@ export async function calculateReplacement( const exactResult = await calculateExactReplacement(context); if (exactResult) { + const event = new SmartEditStrategyEvent('exact'); + logSmartEditStrategy(config, event); return exactResult; } const flexibleResult = await calculateFlexibleReplacement(context); if (flexibleResult) { + const event = new SmartEditStrategyEvent('flexible'); + logSmartEditStrategy(config, event); return flexibleResult; } const regexResult = await calculateRegexReplacement(context); if (regexResult) { + const event = new SmartEditStrategyEvent('regex'); + logSmartEditStrategy(config, event); return regexResult; } @@ -388,7 +397,7 @@ class EditToolInvocation implements ToolInvocation { }; } - const secondAttemptResult = await calculateReplacement({ + const secondAttemptResult = await calculateReplacement(this.config, { params: { ...params, old_string: fixedEdit.search, @@ -516,7 +525,7 @@ class EditToolInvocation implements ToolInvocation { }; } - const replacementResult = await calculateReplacement({ + const replacementResult = await calculateReplacement(this.config, { params, currentContent, abortSignal,