From 528227a0f89719d3a0cac83f386b61e9a5f8785d Mon Sep 17 00:00:00 2001 From: Nanda Kishore Date: Fri, 22 Aug 2025 17:47:32 +0530 Subject: [PATCH] feat: Add programming language to CLI events (#6071) Co-authored-by: christine betts Co-authored-by: gemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com> Co-authored-by: Adam Weidman <65992621+adamfweidman@users.noreply.github.com> Co-authored-by: JaeHo Jang Co-authored-by: Jacob Richman Co-authored-by: Victor May Co-authored-by: Gaurav <39389231+gsquared94@users.noreply.github.com> Co-authored-by: joshualitt Co-authored-by: Billy Biggs Co-authored-by: Ricardo Fabbri Co-authored-by: Arya Gummadi Co-authored-by: Tommaso Sciortino Co-authored-by: Gal Zahavi <38544478+galz10@users.noreply.github.com> Co-authored-by: Shreya Keshive Co-authored-by: Ben Guo <36952867+HunDun0Ben@users.noreply.github.com> Co-authored-by: Ben Guo Co-authored-by: mkusaka --- docs/telemetry.md | 1 + .../src/ui/hooks/atCommandProcessor.test.ts | 1 + .../clearcut-logger/clearcut-logger.ts | 60 ++++++++++ .../clearcut-logger/event-metadata-key.ts | 19 ++++ packages/core/src/telemetry/loggers.ts | 20 ++++ packages/core/src/telemetry/metrics.ts | 4 + .../src/telemetry/telemetry-utils.test.ts | 46 ++++++++ .../core/src/telemetry/telemetry-utils.ts | 17 +++ packages/core/src/telemetry/types.ts | 39 ++++++- packages/core/src/tools/edit.test.ts | 4 + packages/core/src/tools/edit.ts | 44 ++++++-- packages/core/src/tools/read-file.test.ts | 6 +- packages/core/src/tools/read-file.ts | 26 +++-- .../core/src/tools/read-many-files.test.ts | 4 + packages/core/src/tools/read-many-files.ts | 26 +++-- packages/core/src/tools/write-file.test.ts | 5 + packages/core/src/tools/write-file.ts | 41 ++++--- packages/core/src/utils/language-detection.ts | 103 ++++++++++++++++++ 18 files changed, 420 insertions(+), 46 deletions(-) create mode 100644 packages/core/src/telemetry/telemetry-utils.test.ts create mode 100644 packages/core/src/telemetry/telemetry-utils.ts create mode 100644 packages/core/src/utils/language-detection.ts diff --git a/docs/telemetry.md b/docs/telemetry.md index 205242e7..55dc8fb8 100644 --- a/docs/telemetry.md +++ b/docs/telemetry.md @@ -272,6 +272,7 @@ Metrics are numerical measurements of behavior over time. The following metrics - `ai_removed_lines` (Int, if applicable): Number of lines removed/changed by AI. - `user_added_lines` (Int, if applicable): Number of lines added/changed by user in AI proposed changes. - `user_removed_lines` (Int, if applicable): Number of lines removed/changed by user in AI proposed changes. + - `programming_language` (string, if applicable): The programming language of the file. - `gemini_cli.chat_compression` (Counter, Int): Counts chat compression operations - **Attributes**: diff --git a/packages/cli/src/ui/hooks/atCommandProcessor.test.ts b/packages/cli/src/ui/hooks/atCommandProcessor.test.ts index 837e0d32..a0f82bcb 100644 --- a/packages/cli/src/ui/hooks/atCommandProcessor.test.ts +++ b/packages/cli/src/ui/hooks/atCommandProcessor.test.ts @@ -69,6 +69,7 @@ describe('handleAtCommand', () => { getPromptsByServer: () => [], }), getDebugMode: () => false, + getUsageStatisticsEnabled: () => false, } as unknown as Config; const registry = new ToolRegistry(mockConfig); diff --git a/packages/core/src/telemetry/clearcut-logger/clearcut-logger.ts b/packages/core/src/telemetry/clearcut-logger/clearcut-logger.ts index dd506bf0..3cbb9e55 100644 --- a/packages/core/src/telemetry/clearcut-logger/clearcut-logger.ts +++ b/packages/core/src/telemetry/clearcut-logger/clearcut-logger.ts @@ -19,6 +19,7 @@ import { IdeConnectionEvent, KittySequenceOverflowEvent, ChatCompressionEvent, + FileOperationEvent, } from '../types.js'; import { EventMetadataKey } from './event-metadata-key.js'; import { Config } from '../../config/config.js'; @@ -33,6 +34,7 @@ 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', @@ -476,6 +478,64 @@ export class ClearcutLogger { 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, + }); + } + + if (event.diff_stat) { + const metadataMapping: { [key: string]: EventMetadataKey } = { + ai_added_lines: EventMetadataKey.GEMINI_CLI_AI_ADDED_LINES, + ai_removed_lines: EventMetadataKey.GEMINI_CLI_AI_REMOVED_LINES, + user_added_lines: EventMetadataKey.GEMINI_CLI_USER_ADDED_LINES, + user_removed_lines: EventMetadataKey.GEMINI_CLI_USER_REMOVED_LINES, + }; + + for (const [key, gemini_cli_key] of Object.entries(metadataMapping)) { + if ( + event.diff_stat[key as keyof typeof event.diff_stat] !== undefined + ) { + data.push({ + gemini_cli_key, + value: JSON.stringify( + event.diff_stat[key as keyof typeof event.diff_stat], + ), + }); + } + } + } + + const logEvent = this.createLogEvent(EventNames.FILE_OPERATION, data); + this.enqueueLogEvent(logEvent); + this.flushIfNeeded(); + } + logApiRequestEvent(event: ApiRequestEvent): void { const data: EventValue[] = [ { 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 a9c39758..500b472b 100644 --- a/packages/core/src/telemetry/clearcut-logger/event-metadata-key.ts +++ b/packages/core/src/telemetry/clearcut-logger/event-metadata-key.ts @@ -219,6 +219,9 @@ export enum EventMetadataKey { // Logs user removed lines in edit/write tool response. GEMINI_CLI_USER_REMOVED_LINES = 50, + // Logs the programming language of the project. + GEMINI_CLI_PROGRAMMING_LANGUAGE = 56, + // ========================================================================== // Kitty Sequence Overflow Event Keys // =========================================================================== @@ -246,4 +249,20 @@ export enum EventMetadataKey { // Logs name of MCP tools as comma seperated string GEMINI_CLI_START_SESSION_MCP_TOOLS = 65, + + // ========================================================================== + // File Operation Event Keys + // =========================================================================== + + // Logs the operation type of the file operation. + GEMINI_CLI_FILE_OPERATION_TYPE = 66, + + // Logs the number of lines in the file operation. + GEMINI_CLI_FILE_OPERATION_LINES = 67, + + // Logs the mimetype of the file in the file operation. + GEMINI_CLI_FILE_OPERATION_MIMETYPE = 68, + + // Logs the extension of the file in the file operation. + GEMINI_CLI_FILE_OPERATION_EXTENSION = 69, } diff --git a/packages/core/src/telemetry/loggers.ts b/packages/core/src/telemetry/loggers.ts index 564e23b2..29ccc226 100644 --- a/packages/core/src/telemetry/loggers.ts +++ b/packages/core/src/telemetry/loggers.ts @@ -25,6 +25,7 @@ import { ApiErrorEvent, ApiRequestEvent, ApiResponseEvent, + FileOperationEvent, IdeConnectionEvent, StartSessionEvent, ToolCallEvent, @@ -42,6 +43,7 @@ import { recordApiResponseMetrics, recordToolCallMetrics, recordChatCompressionMetrics, + recordFileOperationMetric, } from './metrics.js'; import { isTelemetrySdkInitialized } from './sdk.js'; import { uiTelemetryService, UiEvent } from './uiTelemetry.js'; @@ -155,6 +157,24 @@ export function logToolCall(config: Config, event: ToolCallEvent): void { ); } +export function logFileOperation( + config: Config, + event: FileOperationEvent, +): void { + ClearcutLogger.getInstance(config)?.logFileOperationEvent(event); + if (!isTelemetrySdkInitialized()) return; + + recordFileOperationMetric( + config, + event.operation, + event.lines, + event.mimetype, + event.extension, + event.diff_stat, + event.programming_language, + ); +} + export function logApiRequest(config: Config, event: ApiRequestEvent): void { ClearcutLogger.getInstance(config)?.logApiRequestEvent(event); if (!isTelemetrySdkInitialized()) return; diff --git a/packages/core/src/telemetry/metrics.ts b/packages/core/src/telemetry/metrics.ts index 4abcb4f0..d3c4ea9c 100644 --- a/packages/core/src/telemetry/metrics.ts +++ b/packages/core/src/telemetry/metrics.ts @@ -210,6 +210,7 @@ export function recordFileOperationMetric( mimetype?: string, extension?: string, diffStat?: DiffStat, + programming_language?: string, ): void { if (!fileOperationCounter || !isMetricsInitialized) return; const attributes: Attributes = { @@ -225,5 +226,8 @@ export function recordFileOperationMetric( attributes['user_added_lines'] = diffStat.user_added_lines; attributes['user_removed_lines'] = diffStat.user_removed_lines; } + if (programming_language !== undefined) { + attributes['programming_language'] = programming_language; + } fileOperationCounter.add(1, attributes); } diff --git a/packages/core/src/telemetry/telemetry-utils.test.ts b/packages/core/src/telemetry/telemetry-utils.test.ts new file mode 100644 index 00000000..a5758654 --- /dev/null +++ b/packages/core/src/telemetry/telemetry-utils.test.ts @@ -0,0 +1,46 @@ +/** + * @license + * Copyright 2025 Google LLC + * SPDX-License-Identifier: Apache-2.0 + */ + +import { describe, it, expect } from 'vitest'; +import { getProgrammingLanguage } from './telemetry-utils.js'; + +describe('getProgrammingLanguage', () => { + it('should return the programming language when file_path is present', () => { + const args = { file_path: 'src/test.ts' }; + const language = getProgrammingLanguage(args); + expect(language).toBe('TypeScript'); + }); + + it('should return the programming language when absolute_path is present', () => { + const args = { absolute_path: 'src/test.py' }; + const language = getProgrammingLanguage(args); + expect(language).toBe('Python'); + }); + + it('should return the programming language when path is present', () => { + const args = { path: 'src/test.go' }; + const language = getProgrammingLanguage(args); + expect(language).toBe('Go'); + }); + + it('should return undefined when no file path is present', () => { + const args = {}; + const language = getProgrammingLanguage(args); + expect(language).toBeUndefined(); + }); + + it('should handle unknown file extensions gracefully', () => { + const args = { file_path: 'src/test.unknown' }; + const language = getProgrammingLanguage(args); + expect(language).toBeUndefined(); + }); + + it('should handle files with no extension', () => { + const args = { file_path: 'src/test' }; + const language = getProgrammingLanguage(args); + expect(language).toBeUndefined(); + }); +}); diff --git a/packages/core/src/telemetry/telemetry-utils.ts b/packages/core/src/telemetry/telemetry-utils.ts new file mode 100644 index 00000000..d2a0ffe8 --- /dev/null +++ b/packages/core/src/telemetry/telemetry-utils.ts @@ -0,0 +1,17 @@ +/** + * @license + * Copyright 2025 Google LLC + * SPDX-License-Identifier: Apache-2.0 + */ + +import { getLanguageFromFilePath } from '../utils/language-detection.js'; + +export function getProgrammingLanguage( + args: Record, +): string | undefined { + const filePath = args['file_path'] || args['path'] || args['absolute_path']; + if (typeof filePath === 'string') { + return getLanguageFromFilePath(filePath); + } + return undefined; +} diff --git a/packages/core/src/telemetry/types.ts b/packages/core/src/telemetry/types.ts index 9310992c..7010e698 100644 --- a/packages/core/src/telemetry/types.ts +++ b/packages/core/src/telemetry/types.ts @@ -8,12 +8,14 @@ import { GenerateContentResponseUsageMetadata } from '@google/genai'; import { Config } from '../config/config.js'; import { CompletedToolCall } from '../core/coreToolScheduler.js'; import { DiscoveredMCPTool } from '../tools/mcp-tool.js'; -import { FileDiff } from '../tools/tools.js'; +import { DiffStat, FileDiff } from '../tools/tools.js'; import { AuthType } from '../core/contentGenerator.js'; import { getDecisionFromOutcome, ToolCallDecision, } from './tool-call-decision.js'; +import { FileOperation } from './metrics.js'; +export { ToolCallDecision }; import { ToolRegistry } from '../tools/tool-registry.js'; export interface BaseTelemetryEvent { @@ -399,6 +401,38 @@ export class KittySequenceOverflowEvent { } } +export class FileOperationEvent implements BaseTelemetryEvent { + 'event.name': 'file_operation'; + 'event.timestamp': string; + tool_name: string; + operation: FileOperation; + lines?: number; + mimetype?: string; + extension?: string; + diff_stat?: DiffStat; + programming_language?: string; + + constructor( + tool_name: string, + operation: FileOperation, + lines?: number, + mimetype?: string, + extension?: string, + diff_stat?: DiffStat, + programming_language?: string, + ) { + this['event.name'] = 'file_operation'; + this['event.timestamp'] = new Date().toISOString(); + this.tool_name = tool_name; + this.operation = operation; + this.lines = lines; + this.mimetype = mimetype; + this.extension = extension; + this.diff_stat = diff_stat; + this.programming_language = programming_language; + } +} + export type TelemetryEvent = | StartSessionEvent | EndSessionEvent @@ -413,4 +447,5 @@ export type TelemetryEvent = | KittySequenceOverflowEvent | MalformedJsonResponseEvent | IdeConnectionEvent - | SlashCommandEvent; + | SlashCommandEvent + | FileOperationEvent; diff --git a/packages/core/src/tools/edit.test.ts b/packages/core/src/tools/edit.test.ts index 10da7cd3..93b17e73 100644 --- a/packages/core/src/tools/edit.test.ts +++ b/packages/core/src/tools/edit.test.ts @@ -26,6 +26,10 @@ vi.mock('../utils/editor.js', () => ({ openDiff: mockOpenDiff, })); +vi.mock('../telemetry/loggers.js', () => ({ + logFileOperation: vi.fn(), +})); + import { describe, it, expect, beforeEach, afterEach, vi, Mock } from 'vitest'; import { applyReplacement, EditTool, EditToolParams } from './edit.js'; import { FileDiff, ToolConfirmationOutcome } from './tools.js'; diff --git a/packages/core/src/tools/edit.ts b/packages/core/src/tools/edit.ts index ca1e76ec..915097d1 100644 --- a/packages/core/src/tools/edit.ts +++ b/packages/core/src/tools/edit.ts @@ -27,6 +27,11 @@ import { DEFAULT_DIFF_OPTIONS, getDiffStat } from './diffOptions.js'; import { ReadFileTool } from './read-file.js'; import { ModifiableDeclarativeTool, ModifyContext } from './modifiable-tool.js'; import { IDEConnectionStatus } from '../ide/ide-client.js'; +import { FileOperation } from '../telemetry/metrics.js'; +import { logFileOperation } from '../telemetry/loggers.js'; +import { FileOperationEvent } from '../telemetry/types.js'; +import { getProgrammingLanguage } from '../telemetry/telemetry-utils.js'; +import { getSpecificMimeType } from '../utils/fileUtils.js'; export function applyReplacement( currentContent: string | null, @@ -345,12 +350,21 @@ class EditToolInvocation implements ToolInvocation { .writeTextFile(this.params.file_path, editData.newContent); let displayResult: ToolResultDisplay; + const fileName = path.basename(this.params.file_path); + const originallyProposedContent = + this.params.ai_proposed_string || this.params.new_string; + const diffStat = getDiffStat( + fileName, + editData.currentContent ?? '', + originallyProposedContent, + this.params.new_string, + ); + if (editData.isNewFile) { displayResult = `Created ${shortenPath(makeRelative(this.params.file_path, this.config.getTargetDir()))}`; } else { // Generate diff for display, even though core logic doesn't technically need it // The CLI wrapper will use this part of the ToolResult - const fileName = path.basename(this.params.file_path); const fileDiff = Diff.createPatch( fileName, editData.currentContent ?? '', // Should not be null here if not isNewFile @@ -359,14 +373,6 @@ class EditToolInvocation implements ToolInvocation { 'Proposed', DEFAULT_DIFF_OPTIONS, ); - const originallyProposedContent = - this.params.ai_proposed_string || this.params.new_string; - const diffStat = getDiffStat( - fileName, - editData.currentContent ?? '', - originallyProposedContent, - this.params.new_string, - ); displayResult = { fileDiff, fileName, @@ -387,6 +393,26 @@ class EditToolInvocation implements ToolInvocation { ); } + const lines = editData.newContent.split('\n').length; + const mimetype = getSpecificMimeType(this.params.file_path); + const extension = path.extname(this.params.file_path); + const programming_language = getProgrammingLanguage({ + file_path: this.params.file_path, + }); + + logFileOperation( + this.config, + new FileOperationEvent( + EditTool.Name, + editData.isNewFile ? FileOperation.CREATE : FileOperation.UPDATE, + lines, + mimetype, + extension, + diffStat, + programming_language, + ), + ); + return { llmContent: llmSuccessMessageParts.join(' '), returnDisplay: displayResult, diff --git a/packages/core/src/tools/read-file.test.ts b/packages/core/src/tools/read-file.test.ts index 7f7f5ede..d4d6836a 100644 --- a/packages/core/src/tools/read-file.test.ts +++ b/packages/core/src/tools/read-file.test.ts @@ -4,7 +4,7 @@ * SPDX-License-Identifier: Apache-2.0 */ -import { describe, it, expect, beforeEach, afterEach } from 'vitest'; +import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest'; import { ReadFileTool, ReadFileToolParams } from './read-file.js'; import { ToolErrorType } from './tool-error.js'; import path from 'path'; @@ -17,6 +17,10 @@ import { StandardFileSystemService } from '../services/fileSystemService.js'; import { createMockWorkspaceContext } from '../test-utils/mockWorkspaceContext.js'; import { ToolInvocation, ToolResult } from './tools.js'; +vi.mock('../telemetry/loggers.js', () => ({ + logFileOperation: vi.fn(), +})); + describe('ReadFileTool', () => { let tempRootDir: string; let tool: ReadFileTool; diff --git a/packages/core/src/tools/read-file.ts b/packages/core/src/tools/read-file.ts index 7e9e8535..8e171ca5 100644 --- a/packages/core/src/tools/read-file.ts +++ b/packages/core/src/tools/read-file.ts @@ -21,10 +21,10 @@ import { getSpecificMimeType, } from '../utils/fileUtils.js'; import { Config } from '../config/config.js'; -import { - recordFileOperationMetric, - FileOperation, -} from '../telemetry/metrics.js'; +import { FileOperation } from '../telemetry/metrics.js'; +import { getProgrammingLanguage } from '../telemetry/telemetry-utils.js'; +import { logFileOperation } from '../telemetry/loggers.js'; +import { FileOperationEvent } from '../telemetry/types.js'; /** * Parameters for the ReadFile tool @@ -112,12 +112,20 @@ ${result.llmContent}`; ? result.llmContent.split('\n').length : undefined; const mimetype = getSpecificMimeType(this.params.absolute_path); - recordFileOperationMetric( + const programming_language = getProgrammingLanguage({ + absolute_path: this.params.absolute_path, + }); + logFileOperation( this.config, - FileOperation.READ, - lines, - mimetype, - path.extname(this.params.absolute_path), + new FileOperationEvent( + ReadFileTool.Name, + FileOperation.READ, + lines, + mimetype, + path.extname(this.params.absolute_path), + undefined, + programming_language, + ), ); return { diff --git a/packages/core/src/tools/read-many-files.test.ts b/packages/core/src/tools/read-many-files.test.ts index 0e55f5c0..61ae8310 100644 --- a/packages/core/src/tools/read-many-files.test.ts +++ b/packages/core/src/tools/read-many-files.test.ts @@ -47,6 +47,10 @@ vi.mock('mime-types', () => { }; }); +vi.mock('../telemetry/loggers.js', () => ({ + logFileOperation: vi.fn(), +})); + describe('ReadManyFilesTool', () => { let tool: ReadManyFilesTool; let tempRootDir: string; diff --git a/packages/core/src/tools/read-many-files.ts b/packages/core/src/tools/read-many-files.ts index 06bc2212..1d48dd63 100644 --- a/packages/core/src/tools/read-many-files.ts +++ b/packages/core/src/tools/read-many-files.ts @@ -25,10 +25,10 @@ import { } from '../utils/fileUtils.js'; import { PartListUnion } from '@google/genai'; import { Config, DEFAULT_FILE_FILTERING_OPTIONS } from '../config/config.js'; -import { - recordFileOperationMetric, - FileOperation, -} from '../telemetry/metrics.js'; +import { FileOperation } from '../telemetry/metrics.js'; +import { getProgrammingLanguage } from '../telemetry/telemetry-utils.js'; +import { logFileOperation } from '../telemetry/loggers.js'; +import { FileOperationEvent } from '../telemetry/types.js'; import { ToolErrorType } from './tool-error.js'; /** @@ -468,12 +468,20 @@ ${finalExclusionPatternsForDescription ? fileReadResult.llmContent.split('\n').length : undefined; const mimetype = getSpecificMimeType(filePath); - recordFileOperationMetric( + const programming_language = getProgrammingLanguage({ + absolute_path: filePath, + }); + logFileOperation( this.config, - FileOperation.READ, - lines, - mimetype, - path.extname(filePath), + new FileOperationEvent( + ReadManyFilesTool.Name, + FileOperation.READ, + lines, + mimetype, + path.extname(filePath), + undefined, + programming_language, + ), ); } } else { diff --git a/packages/core/src/tools/write-file.test.ts b/packages/core/src/tools/write-file.test.ts index e5d5ece9..0ac93602 100644 --- a/packages/core/src/tools/write-file.test.ts +++ b/packages/core/src/tools/write-file.test.ts @@ -88,6 +88,11 @@ const mockConfigInternal = { }) as unknown as ToolRegistry, }; const mockConfig = mockConfigInternal as unknown as Config; + +vi.mock('../telemetry/loggers.js', () => ({ + logFileOperation: vi.fn(), +})); + // --- END MOCKS --- describe('WriteFileTool', () => { diff --git a/packages/core/src/tools/write-file.ts b/packages/core/src/tools/write-file.ts index 063b3613..e4aa3391 100644 --- a/packages/core/src/tools/write-file.ts +++ b/packages/core/src/tools/write-file.ts @@ -30,11 +30,11 @@ import { import { DEFAULT_DIFF_OPTIONS, getDiffStat } from './diffOptions.js'; import { ModifiableDeclarativeTool, ModifyContext } from './modifiable-tool.js'; import { getSpecificMimeType } from '../utils/fileUtils.js'; -import { - recordFileOperationMetric, - FileOperation, -} from '../telemetry/metrics.js'; +import { FileOperation } from '../telemetry/metrics.js'; import { IDEConnectionStatus } from '../ide/ide-client.js'; +import { getProgrammingLanguage } from '../telemetry/telemetry-utils.js'; +import { logFileOperation } from '../telemetry/loggers.js'; +import { FileOperationEvent } from '../telemetry/types.js'; /** * Parameters for the WriteFile tool @@ -314,23 +314,32 @@ class WriteFileToolInvocation extends BaseToolInvocation< const lines = fileContent.split('\n').length; const mimetype = getSpecificMimeType(file_path); const extension = path.extname(file_path); // Get extension + const programming_language = getProgrammingLanguage({ file_path }); if (isNewFile) { - recordFileOperationMetric( + logFileOperation( this.config, - FileOperation.CREATE, - lines, - mimetype, - extension, - diffStat, + new FileOperationEvent( + WriteFileTool.Name, + FileOperation.CREATE, + lines, + mimetype, + extension, + diffStat, + programming_language, + ), ); } else { - recordFileOperationMetric( + logFileOperation( this.config, - FileOperation.UPDATE, - lines, - mimetype, - extension, - diffStat, + new FileOperationEvent( + WriteFileTool.Name, + FileOperation.UPDATE, + lines, + mimetype, + extension, + diffStat, + programming_language, + ), ); } diff --git a/packages/core/src/utils/language-detection.ts b/packages/core/src/utils/language-detection.ts new file mode 100644 index 00000000..0a1ae6f5 --- /dev/null +++ b/packages/core/src/utils/language-detection.ts @@ -0,0 +1,103 @@ +/** + * @license + * Copyright 2025 Google LLC + * SPDX-License-Identifier: Apache-2.0 + */ + +import * as path from 'path'; + +const extensionToLanguageMap: { [key: string]: string } = { + '.ts': 'TypeScript', + '.js': 'JavaScript', + '.mjs': 'JavaScript', + '.cjs': 'JavaScript', + '.jsx': 'JavaScript', + '.tsx': 'TypeScript', + '.py': 'Python', + '.java': 'Java', + '.go': 'Go', + '.rb': 'Ruby', + '.php': 'PHP', + '.phtml': 'PHP', + '.cs': 'C#', + '.cpp': 'C++', + '.cxx': 'C++', + '.cc': 'C++', + '.c': 'C', + '.h': 'C/C++', + '.hpp': 'C++', + '.swift': 'Swift', + '.kt': 'Kotlin', + '.rs': 'Rust', + '.m': 'Objective-C', + '.mm': 'Objective-C', + '.pl': 'Perl', + '.pm': 'Perl', + '.lua': 'Lua', + '.r': 'R', + '.scala': 'Scala', + '.sc': 'Scala', + '.sh': 'Shell', + '.ps1': 'PowerShell', + '.bat': 'Batch', + '.cmd': 'Batch', + '.sql': 'SQL', + '.html': 'HTML', + '.htm': 'HTML', + '.css': 'CSS', + '.less': 'Less', + '.sass': 'Sass', + '.scss': 'Sass', + '.json': 'JSON', + '.xml': 'XML', + '.yaml': 'YAML', + '.yml': 'YAML', + '.md': 'Markdown', + '.markdown': 'Markdown', + '.dockerfile': 'Dockerfile', + '.vim': 'Vim script', + '.vb': 'Visual Basic', + '.fs': 'F#', + '.clj': 'Clojure', + '.cljs': 'Clojure', + '.dart': 'Dart', + '.ex': 'Elixir', + '.erl': 'Erlang', + '.hs': 'Haskell', + '.lisp': 'Lisp', + '.rkt': 'Racket', + '.groovy': 'Groovy', + '.jl': 'Julia', + '.tex': 'LaTeX', + '.ino': 'Arduino', + '.asm': 'Assembly', + '.s': 'Assembly', + '.toml': 'TOML', + '.vue': 'Vue', + '.svelte': 'Svelte', + '.gohtml': 'Go Template', + '.hbs': 'Handlebars', + '.ejs': 'EJS', + '.erb': 'ERB', + '.jsp': 'JSP', + '.dockerignore': 'Docker', + '.gitignore': 'Git', + '.npmignore': 'npm', + '.editorconfig': 'EditorConfig', + '.prettierrc': 'Prettier', + '.eslintrc': 'ESLint', + '.babelrc': 'Babel', + '.tsconfig': 'TypeScript', + '.flow': 'Flow', + '.graphql': 'GraphQL', + '.proto': 'Protocol Buffers', +}; + +export function getLanguageFromFilePath(filePath: string): string | undefined { + const extension = path.extname(filePath).toLowerCase(); + if (extension) { + return extensionToLanguageMap[extension]; + } + const filename = path.basename(filePath).toLowerCase(); + return extensionToLanguageMap[`.${filename}`]; +}