mirror of
https://github.com/QwenLM/qwen-code.git
synced 2025-12-19 09:33:53 +00:00
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:
@@ -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**:
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|||||||
@@ -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[] = [
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -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,
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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);
|
||||||
}
|
}
|
||||||
|
|||||||
46
packages/core/src/telemetry/telemetry-utils.test.ts
Normal file
46
packages/core/src/telemetry/telemetry-utils.test.ts
Normal 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();
|
||||||
|
});
|
||||||
|
});
|
||||||
17
packages/core/src/telemetry/telemetry-utils.ts
Normal file
17
packages/core/src/telemetry/telemetry-utils.ts
Normal 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;
|
||||||
|
}
|
||||||
@@ -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;
|
||||||
|
|||||||
@@ -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';
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
@@ -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', () => {
|
||||||
|
|||||||
@@ -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,
|
||||||
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
103
packages/core/src/utils/language-detection.ts
Normal file
103
packages/core/src/utils/language-detection.ts
Normal 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}`];
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user