feat: Add programming language to CLI events (#6071)

Co-authored-by: christine betts <chrstn@uw.edu>
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 <diehreo@gmail.com>
Co-authored-by: Jacob Richman <jacob314@gmail.com>
Co-authored-by: Victor May <mayvic@google.com>
Co-authored-by: Gaurav <39389231+gsquared94@users.noreply.github.com>
Co-authored-by: joshualitt <joshualitt@google.com>
Co-authored-by: Billy Biggs <bbiggs@google.com>
Co-authored-by: Ricardo Fabbri <rfabbri@gmail.com>
Co-authored-by: Arya Gummadi <aryagummadi@google.com>
Co-authored-by: Tommaso Sciortino <sciortino@gmail.com>
Co-authored-by: Gal Zahavi <38544478+galz10@users.noreply.github.com>
Co-authored-by: Shreya Keshive <skeshive@gmail.com>
Co-authored-by: Ben Guo <36952867+HunDun0Ben@users.noreply.github.com>
Co-authored-by: Ben Guo <hundunben@gmail.com>
Co-authored-by: mkusaka <hinoshita1992@gmail.com>
This commit is contained in:
Nanda Kishore
2025-08-22 17:47:32 +05:30
committed by GitHub
parent 4ced997d63
commit 528227a0f8
18 changed files with 420 additions and 46 deletions

View File

@@ -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. - `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_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. - `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 - `gemini_cli.chat_compression` (Counter, Int): Counts chat compression operations
- **Attributes**: - **Attributes**:

View File

@@ -69,6 +69,7 @@ describe('handleAtCommand', () => {
getPromptsByServer: () => [], getPromptsByServer: () => [],
}), }),
getDebugMode: () => false, getDebugMode: () => false,
getUsageStatisticsEnabled: () => false,
} as unknown as Config; } as unknown as Config;
const registry = new ToolRegistry(mockConfig); const registry = new ToolRegistry(mockConfig);

View File

@@ -19,6 +19,7 @@ import {
IdeConnectionEvent, IdeConnectionEvent,
KittySequenceOverflowEvent, KittySequenceOverflowEvent,
ChatCompressionEvent, ChatCompressionEvent,
FileOperationEvent,
} from '../types.js'; } from '../types.js';
import { EventMetadataKey } from './event-metadata-key.js'; import { EventMetadataKey } from './event-metadata-key.js';
import { Config } from '../../config/config.js'; import { Config } from '../../config/config.js';
@@ -33,6 +34,7 @@ export enum EventNames {
START_SESSION = 'start_session', START_SESSION = 'start_session',
NEW_PROMPT = 'new_prompt', NEW_PROMPT = 'new_prompt',
TOOL_CALL = 'tool_call', TOOL_CALL = 'tool_call',
FILE_OPERATION = 'file_operation',
API_REQUEST = 'api_request', API_REQUEST = 'api_request',
API_RESPONSE = 'api_response', API_RESPONSE = 'api_response',
API_ERROR = 'api_error', API_ERROR = 'api_error',
@@ -476,6 +478,64 @@ export class ClearcutLogger {
this.flushIfNeeded(); 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 { logApiRequestEvent(event: ApiRequestEvent): void {
const data: EventValue[] = [ const data: EventValue[] = [
{ {

View File

@@ -219,6 +219,9 @@ export enum EventMetadataKey {
// Logs user removed lines in edit/write tool response. // Logs user removed lines in edit/write tool response.
GEMINI_CLI_USER_REMOVED_LINES = 50, GEMINI_CLI_USER_REMOVED_LINES = 50,
// Logs the programming language of the project.
GEMINI_CLI_PROGRAMMING_LANGUAGE = 56,
// ========================================================================== // ==========================================================================
// Kitty Sequence Overflow Event Keys // Kitty Sequence Overflow Event Keys
// =========================================================================== // ===========================================================================
@@ -246,4 +249,20 @@ export enum EventMetadataKey {
// Logs name of MCP tools as comma seperated string // Logs name of MCP tools as comma seperated string
GEMINI_CLI_START_SESSION_MCP_TOOLS = 65, 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,
} }

View File

@@ -25,6 +25,7 @@ import {
ApiErrorEvent, ApiErrorEvent,
ApiRequestEvent, ApiRequestEvent,
ApiResponseEvent, ApiResponseEvent,
FileOperationEvent,
IdeConnectionEvent, IdeConnectionEvent,
StartSessionEvent, StartSessionEvent,
ToolCallEvent, ToolCallEvent,
@@ -42,6 +43,7 @@ import {
recordApiResponseMetrics, recordApiResponseMetrics,
recordToolCallMetrics, recordToolCallMetrics,
recordChatCompressionMetrics, recordChatCompressionMetrics,
recordFileOperationMetric,
} from './metrics.js'; } from './metrics.js';
import { isTelemetrySdkInitialized } from './sdk.js'; import { isTelemetrySdkInitialized } from './sdk.js';
import { uiTelemetryService, UiEvent } from './uiTelemetry.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 { export function logApiRequest(config: Config, event: ApiRequestEvent): void {
ClearcutLogger.getInstance(config)?.logApiRequestEvent(event); ClearcutLogger.getInstance(config)?.logApiRequestEvent(event);
if (!isTelemetrySdkInitialized()) return; if (!isTelemetrySdkInitialized()) return;

View File

@@ -210,6 +210,7 @@ export function recordFileOperationMetric(
mimetype?: string, mimetype?: string,
extension?: string, extension?: string,
diffStat?: DiffStat, diffStat?: DiffStat,
programming_language?: string,
): void { ): void {
if (!fileOperationCounter || !isMetricsInitialized) return; if (!fileOperationCounter || !isMetricsInitialized) return;
const attributes: Attributes = { const attributes: Attributes = {
@@ -225,5 +226,8 @@ export function recordFileOperationMetric(
attributes['user_added_lines'] = diffStat.user_added_lines; attributes['user_added_lines'] = diffStat.user_added_lines;
attributes['user_removed_lines'] = diffStat.user_removed_lines; attributes['user_removed_lines'] = diffStat.user_removed_lines;
} }
if (programming_language !== undefined) {
attributes['programming_language'] = programming_language;
}
fileOperationCounter.add(1, attributes); fileOperationCounter.add(1, attributes);
} }

View File

@@ -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();
});
});

View File

@@ -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, unknown>,
): string | undefined {
const filePath = args['file_path'] || args['path'] || args['absolute_path'];
if (typeof filePath === 'string') {
return getLanguageFromFilePath(filePath);
}
return undefined;
}

View File

@@ -8,12 +8,14 @@ import { GenerateContentResponseUsageMetadata } from '@google/genai';
import { Config } from '../config/config.js'; import { Config } from '../config/config.js';
import { CompletedToolCall } from '../core/coreToolScheduler.js'; import { CompletedToolCall } from '../core/coreToolScheduler.js';
import { DiscoveredMCPTool } from '../tools/mcp-tool.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 { AuthType } from '../core/contentGenerator.js';
import { import {
getDecisionFromOutcome, getDecisionFromOutcome,
ToolCallDecision, ToolCallDecision,
} from './tool-call-decision.js'; } from './tool-call-decision.js';
import { FileOperation } from './metrics.js';
export { ToolCallDecision };
import { ToolRegistry } from '../tools/tool-registry.js'; import { ToolRegistry } from '../tools/tool-registry.js';
export interface BaseTelemetryEvent { 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 = export type TelemetryEvent =
| StartSessionEvent | StartSessionEvent
| EndSessionEvent | EndSessionEvent
@@ -413,4 +447,5 @@ export type TelemetryEvent =
| KittySequenceOverflowEvent | KittySequenceOverflowEvent
| MalformedJsonResponseEvent | MalformedJsonResponseEvent
| IdeConnectionEvent | IdeConnectionEvent
| SlashCommandEvent; | SlashCommandEvent
| FileOperationEvent;

View File

@@ -26,6 +26,10 @@ vi.mock('../utils/editor.js', () => ({
openDiff: mockOpenDiff, openDiff: mockOpenDiff,
})); }));
vi.mock('../telemetry/loggers.js', () => ({
logFileOperation: vi.fn(),
}));
import { describe, it, expect, beforeEach, afterEach, vi, Mock } from 'vitest'; import { describe, it, expect, beforeEach, afterEach, vi, Mock } from 'vitest';
import { applyReplacement, EditTool, EditToolParams } from './edit.js'; import { applyReplacement, EditTool, EditToolParams } from './edit.js';
import { FileDiff, ToolConfirmationOutcome } from './tools.js'; import { FileDiff, ToolConfirmationOutcome } from './tools.js';

View File

@@ -27,6 +27,11 @@ import { DEFAULT_DIFF_OPTIONS, getDiffStat } from './diffOptions.js';
import { ReadFileTool } from './read-file.js'; import { ReadFileTool } from './read-file.js';
import { ModifiableDeclarativeTool, ModifyContext } from './modifiable-tool.js'; import { ModifiableDeclarativeTool, ModifyContext } from './modifiable-tool.js';
import { IDEConnectionStatus } from '../ide/ide-client.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( export function applyReplacement(
currentContent: string | null, currentContent: string | null,
@@ -345,12 +350,21 @@ class EditToolInvocation implements ToolInvocation<EditToolParams, ToolResult> {
.writeTextFile(this.params.file_path, editData.newContent); .writeTextFile(this.params.file_path, editData.newContent);
let displayResult: ToolResultDisplay; 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) { if (editData.isNewFile) {
displayResult = `Created ${shortenPath(makeRelative(this.params.file_path, this.config.getTargetDir()))}`; displayResult = `Created ${shortenPath(makeRelative(this.params.file_path, this.config.getTargetDir()))}`;
} else { } else {
// Generate diff for display, even though core logic doesn't technically need it // Generate diff for display, even though core logic doesn't technically need it
// The CLI wrapper will use this part of the ToolResult // The CLI wrapper will use this part of the ToolResult
const fileName = path.basename(this.params.file_path);
const fileDiff = Diff.createPatch( const fileDiff = Diff.createPatch(
fileName, fileName,
editData.currentContent ?? '', // Should not be null here if not isNewFile editData.currentContent ?? '', // Should not be null here if not isNewFile
@@ -359,14 +373,6 @@ class EditToolInvocation implements ToolInvocation<EditToolParams, ToolResult> {
'Proposed', 'Proposed',
DEFAULT_DIFF_OPTIONS, 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 = { displayResult = {
fileDiff, fileDiff,
fileName, fileName,
@@ -387,6 +393,26 @@ class EditToolInvocation implements ToolInvocation<EditToolParams, ToolResult> {
); );
} }
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 { return {
llmContent: llmSuccessMessageParts.join(' '), llmContent: llmSuccessMessageParts.join(' '),
returnDisplay: displayResult, returnDisplay: displayResult,

View File

@@ -4,7 +4,7 @@
* SPDX-License-Identifier: Apache-2.0 * 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 { ReadFileTool, ReadFileToolParams } from './read-file.js';
import { ToolErrorType } from './tool-error.js'; import { ToolErrorType } from './tool-error.js';
import path from 'path'; import path from 'path';
@@ -17,6 +17,10 @@ import { StandardFileSystemService } from '../services/fileSystemService.js';
import { createMockWorkspaceContext } from '../test-utils/mockWorkspaceContext.js'; import { createMockWorkspaceContext } from '../test-utils/mockWorkspaceContext.js';
import { ToolInvocation, ToolResult } from './tools.js'; import { ToolInvocation, ToolResult } from './tools.js';
vi.mock('../telemetry/loggers.js', () => ({
logFileOperation: vi.fn(),
}));
describe('ReadFileTool', () => { describe('ReadFileTool', () => {
let tempRootDir: string; let tempRootDir: string;
let tool: ReadFileTool; let tool: ReadFileTool;

View File

@@ -21,10 +21,10 @@ import {
getSpecificMimeType, getSpecificMimeType,
} from '../utils/fileUtils.js'; } from '../utils/fileUtils.js';
import { Config } from '../config/config.js'; import { Config } from '../config/config.js';
import { import { FileOperation } from '../telemetry/metrics.js';
recordFileOperationMetric, import { getProgrammingLanguage } from '../telemetry/telemetry-utils.js';
FileOperation, import { logFileOperation } from '../telemetry/loggers.js';
} from '../telemetry/metrics.js'; import { FileOperationEvent } from '../telemetry/types.js';
/** /**
* Parameters for the ReadFile tool * Parameters for the ReadFile tool
@@ -112,12 +112,20 @@ ${result.llmContent}`;
? result.llmContent.split('\n').length ? result.llmContent.split('\n').length
: undefined; : undefined;
const mimetype = getSpecificMimeType(this.params.absolute_path); const mimetype = getSpecificMimeType(this.params.absolute_path);
recordFileOperationMetric( const programming_language = getProgrammingLanguage({
absolute_path: this.params.absolute_path,
});
logFileOperation(
this.config, this.config,
FileOperation.READ, new FileOperationEvent(
lines, ReadFileTool.Name,
mimetype, FileOperation.READ,
path.extname(this.params.absolute_path), lines,
mimetype,
path.extname(this.params.absolute_path),
undefined,
programming_language,
),
); );
return { return {

View File

@@ -47,6 +47,10 @@ vi.mock('mime-types', () => {
}; };
}); });
vi.mock('../telemetry/loggers.js', () => ({
logFileOperation: vi.fn(),
}));
describe('ReadManyFilesTool', () => { describe('ReadManyFilesTool', () => {
let tool: ReadManyFilesTool; let tool: ReadManyFilesTool;
let tempRootDir: string; let tempRootDir: string;

View File

@@ -25,10 +25,10 @@ import {
} from '../utils/fileUtils.js'; } from '../utils/fileUtils.js';
import { PartListUnion } from '@google/genai'; import { PartListUnion } from '@google/genai';
import { Config, DEFAULT_FILE_FILTERING_OPTIONS } from '../config/config.js'; import { Config, DEFAULT_FILE_FILTERING_OPTIONS } from '../config/config.js';
import { import { FileOperation } from '../telemetry/metrics.js';
recordFileOperationMetric, import { getProgrammingLanguage } from '../telemetry/telemetry-utils.js';
FileOperation, import { logFileOperation } from '../telemetry/loggers.js';
} from '../telemetry/metrics.js'; import { FileOperationEvent } from '../telemetry/types.js';
import { ToolErrorType } from './tool-error.js'; import { ToolErrorType } from './tool-error.js';
/** /**
@@ -468,12 +468,20 @@ ${finalExclusionPatternsForDescription
? fileReadResult.llmContent.split('\n').length ? fileReadResult.llmContent.split('\n').length
: undefined; : undefined;
const mimetype = getSpecificMimeType(filePath); const mimetype = getSpecificMimeType(filePath);
recordFileOperationMetric( const programming_language = getProgrammingLanguage({
absolute_path: filePath,
});
logFileOperation(
this.config, this.config,
FileOperation.READ, new FileOperationEvent(
lines, ReadManyFilesTool.Name,
mimetype, FileOperation.READ,
path.extname(filePath), lines,
mimetype,
path.extname(filePath),
undefined,
programming_language,
),
); );
} }
} else { } else {

View File

@@ -88,6 +88,11 @@ const mockConfigInternal = {
}) as unknown as ToolRegistry, }) as unknown as ToolRegistry,
}; };
const mockConfig = mockConfigInternal as unknown as Config; const mockConfig = mockConfigInternal as unknown as Config;
vi.mock('../telemetry/loggers.js', () => ({
logFileOperation: vi.fn(),
}));
// --- END MOCKS --- // --- END MOCKS ---
describe('WriteFileTool', () => { describe('WriteFileTool', () => {

View File

@@ -30,11 +30,11 @@ import {
import { DEFAULT_DIFF_OPTIONS, getDiffStat } from './diffOptions.js'; import { DEFAULT_DIFF_OPTIONS, getDiffStat } from './diffOptions.js';
import { ModifiableDeclarativeTool, ModifyContext } from './modifiable-tool.js'; import { ModifiableDeclarativeTool, ModifyContext } from './modifiable-tool.js';
import { getSpecificMimeType } from '../utils/fileUtils.js'; import { getSpecificMimeType } from '../utils/fileUtils.js';
import { import { FileOperation } from '../telemetry/metrics.js';
recordFileOperationMetric,
FileOperation,
} from '../telemetry/metrics.js';
import { IDEConnectionStatus } from '../ide/ide-client.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 * Parameters for the WriteFile tool
@@ -314,23 +314,32 @@ class WriteFileToolInvocation extends BaseToolInvocation<
const lines = fileContent.split('\n').length; const lines = fileContent.split('\n').length;
const mimetype = getSpecificMimeType(file_path); const mimetype = getSpecificMimeType(file_path);
const extension = path.extname(file_path); // Get extension const extension = path.extname(file_path); // Get extension
const programming_language = getProgrammingLanguage({ file_path });
if (isNewFile) { if (isNewFile) {
recordFileOperationMetric( logFileOperation(
this.config, this.config,
FileOperation.CREATE, new FileOperationEvent(
lines, WriteFileTool.Name,
mimetype, FileOperation.CREATE,
extension, lines,
diffStat, mimetype,
extension,
diffStat,
programming_language,
),
); );
} else { } else {
recordFileOperationMetric( logFileOperation(
this.config, this.config,
FileOperation.UPDATE, new FileOperationEvent(
lines, WriteFileTool.Name,
mimetype, FileOperation.UPDATE,
extension, lines,
diffStat, mimetype,
extension,
diffStat,
programming_language,
),
); );
} }

View File

@@ -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}`];
}