Merge tag 'v0.3.0' into chore/sync-gemini-cli-v0.3.0

This commit is contained in:
mingholy.lmh
2025-09-10 21:01:40 +08:00
583 changed files with 30160 additions and 10770 deletions

View File

@@ -14,21 +14,19 @@ import {
beforeAll,
afterAll,
} from 'vitest';
import {
ClearcutLogger,
LogEvent,
LogEventEntry,
EventNames,
TEST_ONLY,
} from './clearcut-logger.js';
import { ConfigParameters } from '../../config/config.js';
import * as userAccount from '../../utils/user_account.js';
import * as userId from '../../utils/user_id.js';
import type { LogEvent, LogEventEntry } from './clearcut-logger.js';
import { ClearcutLogger, EventNames, TEST_ONLY } from './clearcut-logger.js';
import type { ContentGeneratorConfig } from '../../core/contentGenerator.js';
import { AuthType } from '../../core/contentGenerator.js';
import type { ConfigParameters } from '../../config/config.js';
import { EventMetadataKey } from './event-metadata-key.js';
import { makeFakeConfig } from '../../test-utils/config.js';
import { http, HttpResponse } from 'msw';
import { server } from '../../mocks/msw.js';
import { makeChatCompressionEvent } from '../types.js';
import { UserPromptEvent, makeChatCompressionEvent } from '../types.js';
import { GIT_COMMIT_INFO, CLI_VERSION } from '../../generated/git-commit.js';
import { UserAccountManager } from '../../utils/userAccountManager.js';
import { InstallationManager } from '../../utils/installationManager.js';
interface CustomMatchers<R = unknown> {
toHaveMetadataValue: ([key, value]: [EventMetadataKey, string]) => R;
@@ -71,11 +69,11 @@ expect.extend({
},
});
vi.mock('../../utils/user_account');
vi.mock('../../utils/user_id');
vi.mock('../../utils/userAccountManager.js');
vi.mock('../../utils/installationManager.js');
const mockUserAccount = vi.mocked(userAccount);
const mockUserId = vi.mocked(userId);
const mockUserAccount = vi.mocked(UserAccountManager.prototype);
const mockInstallMgr = vi.mocked(InstallationManager.prototype);
// TODO(richieforeman): Consider moving this to test setup globally.
beforeAll(() => {
@@ -113,7 +111,6 @@ describe('ClearcutLogger', () => {
config = {} as Partial<ConfigParameters>,
lifetimeGoogleAccounts = 1,
cachedGoogleAccount = 'test@google.com',
installationId = 'test-installation-id',
} = {}) {
server.resetHandlers(
http.post(CLEARCUT_URL, () => HttpResponse.text(EXAMPLE_RESPONSE)),
@@ -131,7 +128,9 @@ describe('ClearcutLogger', () => {
mockUserAccount.getLifetimeGoogleAccounts.mockReturnValue(
lifetimeGoogleAccounts,
);
mockUserId.getInstallationId.mockReturnValue(installationId);
mockInstallMgr.getInstallationId = vi
.fn()
.mockReturnValue('test-installation-id');
const logger = ClearcutLogger.getInstance(loggerConfig);
@@ -200,6 +199,69 @@ describe('ClearcutLogger', () => {
});
});
it('logs default metadata', () => {
// Define expected values
const session_id = 'my-session-id';
const auth_type = AuthType.USE_GEMINI;
const google_accounts = 123;
const surface = 'ide-1234';
const cli_version = CLI_VERSION;
const git_commit_hash = GIT_COMMIT_INFO;
const prompt_id = 'my-prompt-123';
// Setup logger with expected values
const { logger, loggerConfig } = setup({
lifetimeGoogleAccounts: google_accounts,
config: { sessionId: session_id },
});
vi.spyOn(loggerConfig, 'getContentGeneratorConfig').mockReturnValue({
authType: auth_type,
} as ContentGeneratorConfig);
logger?.logNewPromptEvent(new UserPromptEvent(1, prompt_id)); // prompt_id == session_id before this
vi.stubEnv('SURFACE', surface);
// Create log event
const event = logger?.createLogEvent(EventNames.API_ERROR, []);
// Ensure expected values exist
expect(event?.event_metadata[0]).toEqual(
expect.arrayContaining([
{
gemini_cli_key: EventMetadataKey.GEMINI_CLI_SESSION_ID,
value: session_id,
},
{
gemini_cli_key: EventMetadataKey.GEMINI_CLI_AUTH_TYPE,
value: JSON.stringify(auth_type),
},
{
gemini_cli_key: EventMetadataKey.GEMINI_CLI_GOOGLE_ACCOUNTS_COUNT,
value: `${google_accounts}`,
},
{
gemini_cli_key: EventMetadataKey.GEMINI_CLI_SURFACE,
value: surface,
},
{
gemini_cli_key: EventMetadataKey.GEMINI_CLI_VERSION,
value: cli_version,
},
{
gemini_cli_key: EventMetadataKey.GEMINI_CLI_GIT_COMMIT_HASH,
value: git_commit_hash,
},
{
gemini_cli_key: EventMetadataKey.GEMINI_CLI_PROMPT_ID,
value: prompt_id,
},
{
gemini_cli_key: EventMetadataKey.GEMINI_CLI_OS,
value: process.platform,
},
]),
);
});
it('logs the current surface', () => {
const { logger } = setup({});
@@ -219,6 +281,7 @@ describe('ClearcutLogger', () => {
env: {
CURSOR_TRACE_ID: 'abc123',
GITHUB_SHA: undefined,
TERM_PROGRAM: 'vscode',
},
expectedValue: 'cursor',
},
@@ -226,6 +289,7 @@ describe('ClearcutLogger', () => {
env: {
TERM_PROGRAM: 'vscode',
GITHUB_SHA: undefined,
MONOSPACE_ENV: '',
},
expectedValue: 'vscode',
},
@@ -233,6 +297,7 @@ describe('ClearcutLogger', () => {
env: {
MONOSPACE_ENV: 'true',
GITHUB_SHA: undefined,
TERM_PROGRAM: 'vscode',
},
expectedValue: 'firebasestudio',
},
@@ -240,6 +305,7 @@ describe('ClearcutLogger', () => {
env: {
__COG_BASHRC_SOURCED: 'true',
GITHUB_SHA: undefined,
TERM_PROGRAM: 'vscode',
},
expectedValue: 'devin',
},
@@ -247,6 +313,7 @@ describe('ClearcutLogger', () => {
env: {
CLOUD_SHELL: 'true',
GITHUB_SHA: undefined,
TERM_PROGRAM: 'vscode',
},
expectedValue: 'cloudshell',
},
@@ -272,7 +339,6 @@ describe('ClearcutLogger', () => {
for (const [key, value] of Object.entries(env)) {
vi.stubEnv(key, value);
}
vi.stubEnv('TERM_PROGRAM', 'vscode');
const event = logger?.createLogEvent(EventNames.API_ERROR, []);
expect(event?.event_metadata[0][3]).toEqual({
gemini_cli_key: EventMetadataKey.GEMINI_CLI_SURFACE,

View File

@@ -5,7 +5,7 @@
*/
import { HttpsProxyAgent } from 'https-proxy-agent';
import {
import type {
StartSessionEvent,
UserPromptEvent,
ToolCallEvent,
@@ -17,28 +17,28 @@ import {
SlashCommandEvent,
MalformedJsonResponseEvent,
IdeConnectionEvent,
ConversationFinishedEvent,
KittySequenceOverflowEvent,
ChatCompressionEvent,
FileOperationEvent,
InvalidChunkEvent,
ContentRetryEvent,
ContentRetryFailureEvent,
} from '../types.js';
import { EventMetadataKey } from './event-metadata-key.js';
import { Config } from '../../config/config.js';
import type { Config } from '../../config/config.js';
import { InstallationManager } from '../../utils/installationManager.js';
import { UserAccountManager } from '../../utils/userAccountManager.js';
import { safeJsonStringify } from '../../utils/safeJsonStringify.js';
import {
getCachedGoogleAccount,
getLifetimeGoogleAccounts,
} from '../../utils/user_account.js';
import { getInstallationId } from '../../utils/user_id.js';
import { FixedDeque } from 'mnemonist';
import { GIT_COMMIT_INFO, CLI_VERSION } from '../../generated/git-commit.js';
import { DetectedIde, detectIde } from '../../ide/detect-ide.js';
import { DetectedIde, detectIdeFromEnv } from '../../ide/detect-ide.js';
export enum EventNames {
START_SESSION = 'start_session',
NEW_PROMPT = 'new_prompt',
TOOL_CALL = 'tool_call',
FILE_OPERATION = 'file_operation',
API_REQUEST = 'api_request',
API_RESPONSE = 'api_response',
API_ERROR = 'api_error',
@@ -51,6 +51,7 @@ export enum EventNames {
IDE_CONNECTION = 'ide_connection',
KITTY_SEQUENCE_OVERFLOW = 'kitty_sequence_overflow',
CHAT_COMPRESSION = 'chat_compression',
CONVERSATION_FINISHED = 'conversation_finished',
INVALID_CHUNK = 'invalid_chunk',
CONTENT_RETRY = 'content_retry',
CONTENT_RETRY_FAILURE = 'content_retry_failure',
@@ -100,7 +101,7 @@ function determineSurface(): string {
} else if (process.env['GITHUB_SHA']) {
return 'GitHub';
} else if (process.env['TERM_PROGRAM'] === 'vscode') {
return detectIde() || DetectedIde.VSCode;
return detectIdeFromEnv() || DetectedIde.VSCode;
} else {
return 'SURFACE_NOT_SET';
}
@@ -135,6 +136,8 @@ export class ClearcutLogger {
private config?: Config;
private sessionData: EventValue[] = [];
private promptId: string = '';
private readonly installationManager: InstallationManager;
private readonly userAccountManager: UserAccountManager;
/**
* Queue of pending events that need to be flushed to the server. New events
@@ -158,10 +161,12 @@ export class ClearcutLogger {
*/
private pendingFlush: boolean = false;
private constructor(config?: Config) {
private constructor(config: Config) {
this.config = config;
this.events = new FixedDeque<LogEventEntry[]>(Array, MAX_EVENTS);
this.promptId = config?.getSessionId() ?? '';
this.installationManager = new InstallationManager();
this.userAccountManager = new UserAccountManager();
}
static getInstance(config?: Config): ClearcutLogger | undefined {
@@ -208,12 +213,14 @@ export class ClearcutLogger {
}
createLogEvent(eventName: EventNames, data: EventValue[] = []): LogEvent {
const email = getCachedGoogleAccount();
const email = this.userAccountManager.getCachedGoogleAccount();
if (eventName !== EventNames.START_SESSION) {
data.push(...this.sessionData);
}
data = this.addDefaultFields(data);
const totalAccounts = this.userAccountManager.getLifetimeGoogleAccounts();
data = this.addDefaultFields(data, totalAccounts);
const logEvent: LogEvent = {
console_type: 'GEMINI_CLI',
@@ -226,7 +233,7 @@ export class ClearcutLogger {
if (email) {
logEvent.client_email = email;
} else {
logEvent.client_install_id = getInstallationId();
logEvent.client_install_id = this.installationManager.getInstallationId();
}
return logEvent;
@@ -385,6 +392,22 @@ export class ClearcutLogger {
EventMetadataKey.GEMINI_CLI_START_SESSION_TELEMETRY_LOG_USER_PROMPTS_ENABLED,
value: event.telemetry_log_user_prompts_enabled.toString(),
},
{
gemini_cli_key:
EventMetadataKey.GEMINI_CLI_START_SESSION_MCP_SERVERS_COUNT,
value: event.mcp_servers_count
? event.mcp_servers_count.toString()
: '',
},
{
gemini_cli_key:
EventMetadataKey.GEMINI_CLI_START_SESSION_MCP_TOOLS_COUNT,
value: event.mcp_tools_count?.toString() ?? '',
},
{
gemini_cli_key: EventMetadataKey.GEMINI_CLI_START_SESSION_MCP_TOOLS,
value: event.mcp_tools ? event.mcp_tools : '',
},
];
this.sessionData = data;
@@ -463,6 +486,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[] = [
{
@@ -655,6 +736,28 @@ export class ClearcutLogger {
this.flushIfNeeded();
}
logConversationFinishedEvent(event: ConversationFinishedEvent): void {
const data: EventValue[] = [
{
gemini_cli_key: EventMetadataKey.GEMINI_CLI_SESSION_ID,
value: this.config?.getSessionId() ?? '',
},
{
gemini_cli_key: EventMetadataKey.GEMINI_CLI_CONVERSATION_TURN_COUNT,
value: JSON.stringify(event.turnCount),
},
{
gemini_cli_key: EventMetadataKey.GEMINI_CLI_APPROVAL_MODE,
value: event.approvalMode,
},
];
this.enqueueLogEvent(
this.createLogEvent(EventNames.CONVERSATION_FINISHED, data),
);
this.flushIfNeeded();
}
logKittySequenceOverflowEvent(event: KittySequenceOverflowEvent): void {
const data: EventValue[] = [
{
@@ -748,8 +851,7 @@ export class ClearcutLogger {
* Adds default fields to data, and returns a new data array. This fields
* should exist on all log events.
*/
addDefaultFields(data: EventValue[]): EventValue[] {
const totalAccounts = getLifetimeGoogleAccounts();
addDefaultFields(data: EventValue[], totalAccounts: number): EventValue[] {
const surface = determineSurface();
const defaultLogMetadata: EventValue[] = [
@@ -783,6 +885,10 @@ export class ClearcutLogger {
gemini_cli_key: EventMetadataKey.GEMINI_CLI_PROMPT_ID,
value: this.promptId,
},
{
gemini_cli_key: EventMetadataKey.GEMINI_CLI_OS,
value: process.platform,
},
];
return [...data, ...defaultLogMetadata];
}

View File

@@ -163,6 +163,9 @@ export enum EventMetadataKey {
// Logs the Gemini CLI Git commit hash
GEMINI_CLI_GIT_COMMIT_HASH = 55,
// Logs the Gemini CLI OS
GEMINI_CLI_OS = 82,
// ==========================================================================
// Loop Detected Event Keys
// ===========================================================================
@@ -247,6 +250,13 @@ export enum EventMetadataKey {
// Logs tool type whether it is mcp or native.
GEMINI_CLI_TOOL_TYPE = 62,
// Logs count of MCP servers in Start Session Event
GEMINI_CLI_START_SESSION_MCP_SERVERS_COUNT = 63,
// Logs count of MCP tools in Start Session Event
GEMINI_CLI_START_SESSION_MCP_TOOLS_COUNT = 64,
// Logs name of MCP tools as comma separated string
GEMINI_CLI_START_SESSION_MCP_TOOLS = 65,

View File

@@ -21,6 +21,9 @@ export const EVENT_INVALID_CHUNK = 'qwen-code.chat.invalid_chunk';
export const EVENT_CONTENT_RETRY = 'qwen-code.chat.content_retry';
export const EVENT_CONTENT_RETRY_FAILURE =
'qwen-code.chat.content_retry_failure';
export const EVENT_CONVERSATION_FINISHED = 'qwen-code.conversation_finished';
export const EVENT_MALFORMED_JSON_RESPONSE =
'qwen-code.malformed_json_response';
export const METRIC_TOOL_CALL_COUNT = 'qwen-code.tool.call.count';
export const METRIC_TOOL_CALL_LATENCY = 'qwen-code.tool.call.latency';

View File

@@ -5,14 +5,18 @@
*/
import * as fs from 'node:fs';
import { ExportResult, ExportResultCode } from '@opentelemetry/core';
import { ReadableSpan, SpanExporter } from '@opentelemetry/sdk-trace-base';
import { ReadableLogRecord, LogRecordExporter } from '@opentelemetry/sdk-logs';
import {
import type { ExportResult } from '@opentelemetry/core';
import { ExportResultCode } from '@opentelemetry/core';
import type { ReadableSpan, SpanExporter } from '@opentelemetry/sdk-trace-base';
import type {
ReadableLogRecord,
LogRecordExporter,
} from '@opentelemetry/sdk-logs';
import type {
ResourceMetrics,
PushMetricExporter,
AggregationTemporality,
} from '@opentelemetry/sdk-metrics';
import { AggregationTemporality } from '@opentelemetry/sdk-metrics';
class FileExporter {
protected writeStream: fs.WriteStream;

View File

@@ -13,41 +13,45 @@ export enum TelemetryTarget {
const DEFAULT_TELEMETRY_TARGET = TelemetryTarget.LOCAL;
const DEFAULT_OTLP_ENDPOINT = 'http://localhost:4317';
export { DEFAULT_TELEMETRY_TARGET, DEFAULT_OTLP_ENDPOINT };
export {
initializeTelemetry,
shutdownTelemetry,
isTelemetrySdkInitialized,
} from './sdk.js';
export {
logCliConfiguration,
logUserPrompt,
logToolCall,
logApiRequest,
logApiError,
logApiResponse,
logFlashFallback,
logSlashCommand,
logKittySequenceOverflow,
logChatCompression,
} from './loggers.js';
export {
StartSessionEvent,
EndSessionEvent,
UserPromptEvent,
ToolCallEvent,
ApiRequestEvent,
ApiErrorEvent,
ApiResponseEvent,
TelemetryEvent,
FlashFallbackEvent,
KittySequenceOverflowEvent,
SlashCommandEvent,
makeSlashCommandEvent,
SlashCommandStatus,
ChatCompressionEvent,
makeChatCompressionEvent,
} from './types.js';
export { SpanStatusCode, ValueType } from '@opentelemetry/api';
export { SemanticAttributes } from '@opentelemetry/semantic-conventions';
export {
logApiError,
logApiRequest,
logApiResponse,
logChatCompression,
logCliConfiguration,
logConversationFinishedEvent,
logFlashFallback,
logKittySequenceOverflow,
logSlashCommand,
logToolCall,
logUserPrompt,
} from './loggers.js';
export {
initializeTelemetry,
isTelemetrySdkInitialized,
shutdownTelemetry,
} from './sdk.js';
export {
ApiErrorEvent,
ApiRequestEvent,
ApiResponseEvent,
ConversationFinishedEvent,
EndSessionEvent,
FlashFallbackEvent,
KittySequenceOverflowEvent,
makeChatCompressionEvent,
makeSlashCommandEvent,
SlashCommandStatus,
StartSessionEvent,
ToolCallEvent,
UserPromptEvent,
} from './types.js';
export type {
ChatCompressionEvent,
SlashCommandEvent,
TelemetryEvent,
} from './types.js';
export * from './uiTelemetry.js';
export { DEFAULT_OTLP_ENDPOINT, DEFAULT_TELEMETRY_TARGET };

View File

@@ -8,10 +8,10 @@
* Integration test to verify circular reference handling with proxy agents
*/
import { describe, it, expect, beforeEach, afterEach } from 'vitest';
import { afterEach, beforeEach, describe, expect, it } from 'vitest';
import type { Config } from '../config/config.js';
import type { RumEvent } from './qwen-logger/event-types.js';
import { QwenLogger } from './qwen-logger/qwen-logger.js';
import { RumEvent } from './qwen-logger/event-types.js';
import { Config } from '../config/config.js';
describe('Circular Reference Integration Test', () => {
beforeEach(() => {

View File

@@ -11,9 +11,12 @@
import { describe, it, expect } from 'vitest';
import { logToolCall } from './loggers.js';
import { ToolCallEvent } from './types.js';
import { Config } from '../config/config.js';
import { CompletedToolCall } from '../core/coreToolScheduler.js';
import { ToolCallRequestInfo, ToolCallResponseInfo } from '../core/turn.js';
import type { Config } from '../config/config.js';
import type { CompletedToolCall } from '../core/coreToolScheduler.js';
import type {
ToolCallRequestInfo,
ToolCallResponseInfo,
} from '../core/turn.js';
import { MockTool } from '../test-utils/tools.js';
describe('Circular Reference Handling', () => {

View File

@@ -4,55 +4,58 @@
* SPDX-License-Identifier: Apache-2.0
*/
import {
import type { GenerateContentResponseUsageMetadata } from '@google/genai';
import { logs } from '@opentelemetry/api-logs';
import { SemanticAttributes } from '@opentelemetry/semantic-conventions';
import { beforeEach, describe, expect, it, vi } from 'vitest';
import type { Config } from '../config/config.js';
import type {
AnyToolInvocation,
AuthType,
CompletedToolCall,
ContentGeneratorConfig,
EditTool,
ErroredToolCall,
} from '../index.js';
import {
AuthType,
EditTool,
GeminiClient,
ToolConfirmationOutcome,
ToolErrorType,
ToolRegistry,
} from '../index.js';
import { logs } from '@opentelemetry/api-logs';
import { SemanticAttributes } from '@opentelemetry/semantic-conventions';
import { Config } from '../config/config.js';
import { makeFakeConfig } from '../test-utils/config.js';
import { UserAccountManager } from '../utils/userAccountManager.js';
import {
EVENT_API_REQUEST,
EVENT_API_RESPONSE,
EVENT_CLI_CONFIG,
EVENT_FLASH_FALLBACK,
EVENT_TOOL_CALL,
EVENT_USER_PROMPT,
EVENT_FLASH_FALLBACK,
} from './constants.js';
import {
logApiRequest,
logApiResponse,
logCliConfiguration,
logUserPrompt,
logToolCall,
logFlashFallback,
logChatCompression,
logCliConfiguration,
logFlashFallback,
logToolCall,
logUserPrompt,
} from './loggers.js';
import * as metrics from './metrics.js';
import { QwenLogger } from './qwen-logger/qwen-logger.js';
import * as sdk from './sdk.js';
import { ToolCallDecision } from './tool-call-decision.js';
import {
ApiRequestEvent,
ApiResponseEvent,
FlashFallbackEvent,
StartSessionEvent,
ToolCallEvent,
UserPromptEvent,
FlashFallbackEvent,
makeChatCompressionEvent,
} from './types.js';
import * as metrics from './metrics.js';
import * as sdk from './sdk.js';
import { vi, describe, beforeEach, it, expect } from 'vitest';
import { GenerateContentResponseUsageMetadata } from '@google/genai';
import * as uiTelemetry from './uiTelemetry.js';
import { makeFakeConfig } from '../test-utils/config.js';
import { QwenLogger } from './qwen-logger/qwen-logger.js';
describe('loggers', () => {
const mockLogger = {
@@ -63,11 +66,16 @@ describe('loggers', () => {
};
beforeEach(() => {
vi.clearAllMocks();
vi.spyOn(sdk, 'isTelemetrySdkInitialized').mockReturnValue(true);
vi.spyOn(logs, 'getLogger').mockReturnValue(mockLogger);
vi.spyOn(uiTelemetry.uiTelemetryService, 'addEvent').mockImplementation(
mockUiEvent.addEvent,
);
vi.spyOn(
UserAccountManager.prototype,
'getCachedGoogleAccount',
).mockReturnValue('test-user@example.com');
vi.useFakeTimers();
vi.setSystemTime(new Date('2025-01-01T00:00:00.000Z'));
});
@@ -148,6 +156,7 @@ describe('loggers', () => {
body: 'CLI configuration loaded.',
attributes: {
'session.id': 'test-session-id',
'user.email': 'test-user@example.com',
'event.name': EVENT_CLI_CONFIG,
'event.timestamp': '2025-01-01T00:00:00.000Z',
model: 'test-model',
@@ -161,6 +170,9 @@ describe('loggers', () => {
file_filtering_respect_git_ignore: true,
debug_mode: true,
mcp_servers: 'test-server',
mcp_servers_count: 1,
mcp_tools: undefined,
mcp_tools_count: undefined,
},
});
});
@@ -188,10 +200,13 @@ describe('loggers', () => {
body: 'User prompt. Length: 11.',
attributes: {
'session.id': 'test-session-id',
'user.email': 'test-user@example.com',
'event.name': EVENT_USER_PROMPT,
'event.timestamp': '2025-01-01T00:00:00.000Z',
prompt_length: 11,
prompt: 'test-prompt',
prompt_id: 'prompt-id-8',
auth_type: 'vertex-ai',
},
});
});
@@ -206,8 +221,9 @@ describe('loggers', () => {
} as unknown as Config;
const event = new UserPromptEvent(
11,
'test-prompt',
'prompt-id-9',
AuthType.CLOUD_SHELL,
'test-prompt',
);
logUserPrompt(mockConfig, event);
@@ -216,9 +232,12 @@ describe('loggers', () => {
body: 'User prompt. Length: 11.',
attributes: {
'session.id': 'test-session-id',
'user.email': 'test-user@example.com',
'event.name': EVENT_USER_PROMPT,
'event.timestamp': '2025-01-01T00:00:00.000Z',
prompt_length: 11,
prompt_id: 'prompt-id-9',
auth_type: 'cloud-shell',
},
});
});
@@ -271,6 +290,7 @@ describe('loggers', () => {
body: 'API response from test-model. Status: 200. Duration: 100ms.',
attributes: {
'session.id': 'test-session-id',
'user.email': 'test-user@example.com',
'event.name': EVENT_API_RESPONSE,
'event.timestamp': '2025-01-01T00:00:00.000Z',
[SemanticAttributes.HTTP_STATUS_CODE]: 200,
@@ -287,6 +307,7 @@ describe('loggers', () => {
response_text: 'test-response',
prompt_id: 'prompt-id-1',
auth_type: 'oauth-personal',
error: undefined,
},
});
@@ -337,6 +358,7 @@ describe('loggers', () => {
body: 'API response from test-model. Status: 200. Duration: 100ms.',
attributes: {
'session.id': 'test-session-id',
'user.email': 'test-user@example.com',
...event,
'event.name': EVENT_API_RESPONSE,
'event.timestamp': '2025-01-01T00:00:00.000Z',
@@ -374,6 +396,7 @@ describe('loggers', () => {
body: 'API request to test-model.',
attributes: {
'session.id': 'test-session-id',
'user.email': 'test-user@example.com',
'event.name': EVENT_API_REQUEST,
'event.timestamp': '2025-01-01T00:00:00.000Z',
model: 'test-model',
@@ -392,6 +415,7 @@ describe('loggers', () => {
body: 'API request to test-model.',
attributes: {
'session.id': 'test-session-id',
'user.email': 'test-user@example.com',
'event.name': EVENT_API_REQUEST,
'event.timestamp': '2025-01-01T00:00:00.000Z',
model: 'test-model',
@@ -416,6 +440,7 @@ describe('loggers', () => {
body: 'Switching to flash as Fallback.',
attributes: {
'session.id': 'test-session-id',
'user.email': 'test-user@example.com',
'event.name': EVENT_FLASH_FALLBACK,
'event.timestamp': '2025-01-01T00:00:00.000Z',
auth_type: 'vertex-ai',
@@ -494,7 +519,7 @@ describe('loggers', () => {
},
response: {
callId: 'test-call-id',
responseParts: 'test-response',
responseParts: [{ text: 'test-response' }],
resultDisplay: undefined,
error: undefined,
errorType: undefined,
@@ -512,6 +537,7 @@ describe('loggers', () => {
body: 'Tool call: test-function. Decision: accept. Success: true. Duration: 100ms.',
attributes: {
'session.id': 'test-session-id',
'user.email': 'test-user@example.com',
'event.name': EVENT_TOOL_CALL,
'event.timestamp': '2025-01-01T00:00:00.000Z',
function_name: 'test-function',
@@ -528,6 +554,9 @@ describe('loggers', () => {
decision: ToolCallDecision.ACCEPT,
prompt_id: 'prompt-id-1',
tool_type: 'native',
error: undefined,
error_type: undefined,
metadata: undefined,
},
});
@@ -561,7 +590,7 @@ describe('loggers', () => {
},
response: {
callId: 'test-call-id',
responseParts: 'test-response',
responseParts: [{ text: 'test-response' }],
resultDisplay: undefined,
error: undefined,
errorType: undefined,
@@ -577,6 +606,7 @@ describe('loggers', () => {
body: 'Tool call: test-function. Decision: reject. Success: false. Duration: 100ms.',
attributes: {
'session.id': 'test-session-id',
'user.email': 'test-user@example.com',
'event.name': EVENT_TOOL_CALL,
'event.timestamp': '2025-01-01T00:00:00.000Z',
function_name: 'test-function',
@@ -593,6 +623,9 @@ describe('loggers', () => {
decision: ToolCallDecision.REJECT,
prompt_id: 'prompt-id-2',
tool_type: 'native',
error: undefined,
error_type: undefined,
metadata: undefined,
},
});
@@ -627,7 +660,7 @@ describe('loggers', () => {
},
response: {
callId: 'test-call-id',
responseParts: 'test-response',
responseParts: [{ text: 'test-response' }],
resultDisplay: undefined,
error: undefined,
errorType: undefined,
@@ -645,6 +678,7 @@ describe('loggers', () => {
body: 'Tool call: test-function. Decision: modify. Success: true. Duration: 100ms.',
attributes: {
'session.id': 'test-session-id',
'user.email': 'test-user@example.com',
'event.name': EVENT_TOOL_CALL,
'event.timestamp': '2025-01-01T00:00:00.000Z',
function_name: 'test-function',
@@ -661,6 +695,9 @@ describe('loggers', () => {
decision: ToolCallDecision.MODIFY,
prompt_id: 'prompt-id-3',
tool_type: 'native',
error: undefined,
error_type: undefined,
metadata: undefined,
},
});
@@ -695,7 +732,7 @@ describe('loggers', () => {
},
response: {
callId: 'test-call-id',
responseParts: 'test-response',
responseParts: [{ text: 'test-response' }],
resultDisplay: undefined,
error: undefined,
errorType: undefined,
@@ -712,6 +749,7 @@ describe('loggers', () => {
body: 'Tool call: test-function. Success: true. Duration: 100ms.',
attributes: {
'session.id': 'test-session-id',
'user.email': 'test-user@example.com',
'event.name': EVENT_TOOL_CALL,
'event.timestamp': '2025-01-01T00:00:00.000Z',
function_name: 'test-function',
@@ -727,6 +765,10 @@ describe('loggers', () => {
success: true,
prompt_id: 'prompt-id-4',
tool_type: 'native',
decision: undefined,
error: undefined,
error_type: undefined,
metadata: undefined,
},
});
@@ -761,7 +803,7 @@ describe('loggers', () => {
},
response: {
callId: 'test-call-id',
responseParts: 'test-response',
responseParts: [{ text: 'test-response' }],
resultDisplay: undefined,
error: {
name: 'test-error-type',
@@ -779,6 +821,7 @@ describe('loggers', () => {
body: 'Tool call: test-function. Success: false. Duration: 100ms.',
attributes: {
'session.id': 'test-session-id',
'user.email': 'test-user@example.com',
'event.name': EVENT_TOOL_CALL,
'event.timestamp': '2025-01-01T00:00:00.000Z',
function_name: 'test-function',
@@ -798,6 +841,8 @@ describe('loggers', () => {
'error.type': ToolErrorType.UNKNOWN,
prompt_id: 'prompt-id-5',
tool_type: 'native',
decision: undefined,
metadata: undefined,
},
});

View File

@@ -4,32 +4,55 @@
* SPDX-License-Identifier: Apache-2.0
*/
import { LogAttributes, LogRecord, logs } from '@opentelemetry/api-logs';
import type { LogAttributes, LogRecord } from '@opentelemetry/api-logs';
import { logs } from '@opentelemetry/api-logs';
import { SemanticAttributes } from '@opentelemetry/semantic-conventions';
import { Config } from '../config/config.js';
import type { Config } from '../config/config.js';
import { safeJsonStringify } from '../utils/safeJsonStringify.js';
import { UserAccountManager } from '../utils/userAccountManager.js';
import {
EVENT_API_ERROR,
EVENT_API_REQUEST,
EVENT_API_RESPONSE,
EVENT_CHAT_COMPRESSION,
EVENT_CLI_CONFIG,
EVENT_CONTENT_RETRY,
EVENT_CONTENT_RETRY_FAILURE,
EVENT_CONVERSATION_FINISHED,
EVENT_FLASH_FALLBACK,
EVENT_IDE_CONNECTION,
EVENT_INVALID_CHUNK,
EVENT_NEXT_SPEAKER_CHECK,
EVENT_SLASH_COMMAND,
EVENT_TOOL_CALL,
EVENT_USER_PROMPT,
SERVICE_NAME,
EVENT_CHAT_COMPRESSION,
EVENT_INVALID_CHUNK,
EVENT_CONTENT_RETRY,
EVENT_CONTENT_RETRY_FAILURE,
} from './constants.js';
import {
recordApiErrorMetrics,
recordApiResponseMetrics,
recordChatCompressionMetrics,
recordContentRetry,
recordContentRetryFailure,
recordFileOperationMetric,
recordInvalidChunk,
recordTokenUsageMetrics,
recordToolCallMetrics,
} from './metrics.js';
import { QwenLogger } from './qwen-logger/qwen-logger.js';
import { isTelemetrySdkInitialized } from './sdk.js';
import type {
ApiErrorEvent,
ApiRequestEvent,
ApiResponseEvent,
ChatCompressionEvent,
ContentRetryEvent,
ContentRetryFailureEvent,
ConversationFinishedEvent,
FileOperationEvent,
FlashFallbackEvent,
IdeConnectionEvent,
InvalidChunkEvent,
KittySequenceOverflowEvent,
LoopDetectedEvent,
NextSpeakerCheckEvent,
@@ -37,32 +60,19 @@ import {
StartSessionEvent,
ToolCallEvent,
UserPromptEvent,
ChatCompressionEvent,
InvalidChunkEvent,
ContentRetryEvent,
ContentRetryFailureEvent,
} from './types.js';
import {
recordApiErrorMetrics,
recordTokenUsageMetrics,
recordApiResponseMetrics,
recordToolCallMetrics,
recordChatCompressionMetrics,
recordInvalidChunk,
recordContentRetry,
recordContentRetryFailure,
} from './metrics.js';
import { QwenLogger } from './qwen-logger/qwen-logger.js';
import { isTelemetrySdkInitialized } from './sdk.js';
import { uiTelemetryService, UiEvent } from './uiTelemetry.js';
import { safeJsonStringify } from '../utils/safeJsonStringify.js';
import type { UiEvent } from './uiTelemetry.js';
import { uiTelemetryService } from './uiTelemetry.js';
const shouldLogUserPrompts = (config: Config): boolean =>
config.getTelemetryLogPromptsEnabled();
function getCommonAttributes(config: Config): LogAttributes {
const userAccountManager = new UserAccountManager();
const email = userAccountManager.getCachedGoogleAccount();
return {
'session.id': config.getSessionId(),
...(email && { 'user.email': email }),
};
}
@@ -88,6 +98,9 @@ export function logCliConfiguration(
file_filtering_respect_git_ignore: event.file_filtering_respect_git_ignore,
debug_mode: event.debug_enabled,
mcp_servers: event.mcp_servers,
mcp_servers_count: event.mcp_servers_count,
mcp_tools: event.mcp_tools,
mcp_tools_count: event.mcp_tools_count,
};
const logger = logs.getLogger(SERVICE_NAME);
@@ -107,8 +120,13 @@ export function logUserPrompt(config: Config, event: UserPromptEvent): void {
'event.name': EVENT_USER_PROMPT,
'event.timestamp': new Date().toISOString(),
prompt_length: event.prompt_length,
prompt_id: event.prompt_id,
};
if (event.auth_type) {
attributes['auth_type'] = event.auth_type;
}
if (shouldLogUserPrompts(config)) {
attributes['prompt'] = event.prompt;
}
@@ -161,6 +179,24 @@ export function logToolCall(config: Config, event: ToolCallEvent): void {
);
}
export function logFileOperation(
config: Config,
event: FileOperationEvent,
): void {
QwenLogger.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 {
// QwenLogger.getInstance(config)?.logApiRequestEvent(event);
if (!isTelemetrySdkInitialized()) return;
@@ -393,6 +429,27 @@ export function logIdeConnection(
logger.emit(logRecord);
}
export function logConversationFinishedEvent(
config: Config,
event: ConversationFinishedEvent,
): void {
QwenLogger.getInstance(config)?.logConversationFinishedEvent(event);
if (!isTelemetrySdkInitialized()) return;
const attributes: LogAttributes = {
...getCommonAttributes(config),
...event,
'event.name': EVENT_CONVERSATION_FINISHED,
};
const logger = logs.getLogger(SERVICE_NAME);
const logRecord: LogRecord = {
body: `Conversation finished.`,
attributes,
};
logger.emit(logRecord);
}
export function logChatCompression(
config: Config,
event: ChatCompressionEvent,

View File

@@ -12,7 +12,7 @@ import type {
Context,
Histogram,
} from '@opentelemetry/api';
import { Config } from '../config/config.js';
import type { Config } from '../config/config.js';
import { FileOperation } from './metrics.js';
import { makeFakeConfig } from '../test-utils/config.js';

View File

@@ -4,14 +4,8 @@
* SPDX-License-Identifier: Apache-2.0
*/
import {
metrics,
Attributes,
ValueType,
Meter,
Counter,
Histogram,
} from '@opentelemetry/api';
import type { Attributes, Meter, Counter, Histogram } from '@opentelemetry/api';
import { metrics, ValueType } from '@opentelemetry/api';
import {
SERVICE_NAME,
METRIC_TOOL_CALL_COUNT,
@@ -26,8 +20,8 @@ import {
METRIC_CONTENT_RETRY_COUNT,
METRIC_CONTENT_RETRY_FAILURE_COUNT,
} from './constants.js';
import { Config } from '../config/config.js';
import { DiffStat } from '../tools/tools.js';
import type { Config } from '../config/config.js';
import type { DiffStat } from '../tools/tools.js';
export enum FileOperation {
CREATE = 'create',
@@ -234,6 +228,7 @@ export function recordFileOperationMetric(
mimetype?: string,
extension?: string,
diffStat?: DiffStat,
programming_language?: string,
): void {
if (!fileOperationCounter || !isMetricsInitialized) return;
const attributes: Attributes = {
@@ -249,6 +244,9 @@ 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);
}

View File

@@ -14,7 +14,7 @@ import {
afterAll,
} from 'vitest';
import { QwenLogger, TEST_ONLY } from './qwen-logger.js';
import { Config } from '../../config/config.js';
import type { Config } from '../../config/config.js';
import {
StartSessionEvent,
EndSessionEvent,
@@ -22,7 +22,7 @@ import {
KittySequenceOverflowEvent,
IdeConnectionType,
} from '../types.js';
import { RumEvent } from './event-types.js';
import type { RumEvent } from './event-types.js';
// Mock dependencies
vi.mock('../../utils/user_id.js', () => ({

View File

@@ -8,14 +8,14 @@ import { Buffer } from 'buffer';
import * as https from 'https';
import { HttpsProxyAgent } from 'https-proxy-agent';
import {
import type {
StartSessionEvent,
EndSessionEvent,
UserPromptEvent,
ToolCallEvent,
ApiRequestEvent,
ApiResponseEvent,
ApiErrorEvent,
FileOperationEvent,
FlashFallbackEvent,
LoopDetectedEvent,
NextSpeakerCheckEvent,
@@ -27,8 +27,10 @@ import {
InvalidChunkEvent,
ContentRetryEvent,
ContentRetryFailureEvent,
ConversationFinishedEvent,
} from '../types.js';
import {
import { EndSessionEvent } from '../types.js';
import type {
RumEvent,
RumViewEvent,
RumActionEvent,
@@ -36,10 +38,10 @@ import {
RumExceptionEvent,
RumPayload,
} from './event-types.js';
import { Config } from '../../config/config.js';
import type { Config } from '../../config/config.js';
import { safeJsonStringify } from '../../utils/safeJsonStringify.js';
import { HttpError, retryWithBackoff } from '../../utils/retry.js';
import { getInstallationId } from '../../utils/user_id.js';
import { type HttpError, retryWithBackoff } from '../../utils/retry.js';
import { InstallationManager } from '../../utils/installationManager.js';
import { FixedDeque } from 'mnemonist';
import { AuthType } from '../../core/contentGenerator.js';
@@ -75,6 +77,7 @@ export interface LogResponse {
export class QwenLogger {
private static instance: QwenLogger;
private config?: Config;
private readonly installationManager: InstallationManager;
/**
* Queue of pending events that need to be flushed to the server. New events
@@ -106,6 +109,7 @@ export class QwenLogger {
private constructor(config?: Config) {
this.config = config;
this.events = new FixedDeque<RumEvent>(Array, MAX_EVENTS);
this.installationManager = new InstallationManager();
this.userId = this.generateUserId();
this.sessionId =
typeof this.config?.getSessionId === 'function'
@@ -114,8 +118,9 @@ export class QwenLogger {
}
private generateUserId(): string {
// Use installation ID as user ID for consistency
return `user-${getInstallationId()}`;
// Use InstallationManager to get installationId for userId
const installationId = this.installationManager.getInstallationId();
return `user-${installationId ?? 'unknown'}`;
}
static getInstance(config?: Config): QwenLogger | undefined {
@@ -421,6 +426,29 @@ export class QwenLogger {
this.flushIfNeeded();
}
logFileOperationEvent(event: FileOperationEvent): void {
const rumEvent = this.createActionEvent(
'file_operation',
`file_operation#${event.tool_name}`,
{
properties: {
tool_name: event.tool_name,
operation: event.operation,
lines: event.lines,
mimetype: event.mimetype,
extension: event.extension,
programming_language: event.programming_language,
},
snapshots: event.diff_stat
? JSON.stringify({ diff_stat: event.diff_stat })
: undefined,
},
);
this.enqueueLogEvent(rumEvent);
this.flushIfNeeded();
}
logApiRequestEvent(event: ApiRequestEvent): void {
const rumEvent = this.createResourceEvent('api', 'api_request', {
properties: {
@@ -557,6 +585,22 @@ export class QwenLogger {
this.flushIfNeeded();
}
logConversationFinishedEvent(event: ConversationFinishedEvent): void {
const rumEvent = this.createActionEvent(
'conversation',
'conversation_finished',
{
snapshots: JSON.stringify({
approval_mode: event.approvalMode,
turn_count: event.turnCount,
}),
},
);
this.enqueueLogEvent(rumEvent);
this.flushIfNeeded();
}
logKittySequenceOverflowEvent(event: KittySequenceOverflowEvent): void {
const rumEvent = this.createExceptionEvent(
'overflow',

View File

@@ -5,7 +5,7 @@
*/
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
import { Config } from '../config/config.js';
import type { Config } from '../config/config.js';
import { initializeTelemetry, shutdownTelemetry } from './sdk.js';
import { OTLPTraceExporter } from '@opentelemetry/exporter-trace-otlp-grpc';
import { OTLPLogExporter } from '@opentelemetry/exporter-logs-otlp-grpc';

View File

@@ -28,7 +28,7 @@ import {
PeriodicExportingMetricReader,
} from '@opentelemetry/sdk-metrics';
import { HttpInstrumentation } from '@opentelemetry/instrumentation-http';
import { Config } from '../config/config.js';
import type { Config } from '../config/config.js';
import { SERVICE_NAME } from './constants.js';
import { initializeMetrics } from './metrics.js';
import {

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

@@ -4,16 +4,20 @@
* SPDX-License-Identifier: Apache-2.0
*/
import { GenerateContentResponseUsageMetadata } from '@google/genai';
import { Config } from '../config/config.js';
import { CompletedToolCall } from '../core/coreToolScheduler.js';
import type { GenerateContentResponseUsageMetadata } from '@google/genai';
import type { Config } from '../config/config.js';
import type { ApprovalMode } from '../config/config.js';
import type { CompletedToolCall } from '../core/coreToolScheduler.js';
import { DiscoveredMCPTool } from '../tools/mcp-tool.js';
import { FileDiff } from '../tools/tools.js';
import type { DiffStat, FileDiff } from '../tools/tools.js';
import { AuthType } from '../core/contentGenerator.js';
import {
getDecisionFromOutcome,
ToolCallDecision,
} from './tool-call-decision.js';
import type { FileOperation } from './metrics.js';
export { ToolCallDecision };
import type { ToolRegistry } from '../tools/tool-registry.js';
export interface BaseTelemetryEvent {
'event.name': string;
@@ -38,8 +42,11 @@ export class StartSessionEvent implements BaseTelemetryEvent {
telemetry_enabled: boolean;
telemetry_log_user_prompts_enabled: boolean;
file_filtering_respect_git_ignore: boolean;
mcp_servers_count: number;
mcp_tools_count?: number;
mcp_tools?: string;
constructor(config: Config) {
constructor(config: Config, toolRegistry?: ToolRegistry) {
const generatorConfig = config.getContentGeneratorConfig();
const mcpServers = config.getMcpServers();
@@ -66,6 +73,16 @@ export class StartSessionEvent implements BaseTelemetryEvent {
config.getTelemetryLogPromptsEnabled();
this.file_filtering_respect_git_ignore =
config.getFileFilteringRespectGitIgnore();
this.mcp_servers_count = mcpServers ? Object.keys(mcpServers).length : 0;
if (toolRegistry) {
const mcpTools = toolRegistry
.getAllTools()
.filter((tool) => tool instanceof DiscoveredMCPTool);
this.mcp_tools_count = mcpTools.length;
this.mcp_tools = mcpTools
.map((tool) => (tool as DiscoveredMCPTool).name)
.join(',');
}
}
}
@@ -377,6 +394,20 @@ export class IdeConnectionEvent {
}
}
export class ConversationFinishedEvent {
'event_name': 'conversation_finished';
'event.timestamp': string; // ISO 8601;
approvalMode: ApprovalMode;
turnCount: number;
constructor(approvalMode: ApprovalMode, turnCount: number) {
this['event_name'] = 'conversation_finished';
this['event.timestamp'] = new Date().toISOString();
this.approvalMode = approvalMode;
this.turnCount = turnCount;
}
}
export class KittySequenceOverflowEvent {
'event.name': 'kitty_sequence_overflow';
'event.timestamp': string; // ISO 8601
@@ -391,6 +422,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;
}
}
// Add these new event interfaces
export class InvalidChunkEvent implements BaseTelemetryEvent {
'event.name': 'invalid_chunk';
@@ -458,7 +521,9 @@ export type TelemetryEvent =
| KittySequenceOverflowEvent
| MalformedJsonResponseEvent
| IdeConnectionEvent
| ConversationFinishedEvent
| SlashCommandEvent
| FileOperationEvent
| InvalidChunkEvent
| ContentRetryEvent
| ContentRetryFailureEvent;

View File

@@ -7,13 +7,14 @@
import { describe, it, expect, vi, beforeEach } from 'vitest';
import { UiTelemetryService } from './uiTelemetry.js';
import { ToolCallDecision } from './tool-call-decision.js';
import { ApiErrorEvent, ApiResponseEvent, ToolCallEvent } from './types.js';
import type { ApiErrorEvent, ApiResponseEvent } from './types.js';
import { ToolCallEvent } from './types.js';
import {
EVENT_API_ERROR,
EVENT_API_RESPONSE,
EVENT_TOOL_CALL,
} from './constants.js';
import {
import type {
CompletedToolCall,
ErroredToolCall,
SuccessfulToolCall,
@@ -46,13 +47,15 @@ const createFakeCompletedToolCall = (
invocation: tool.build({ param: 'test' }),
response: {
callId: request.callId,
responseParts: {
functionResponse: {
id: request.callId,
name,
response: { output: 'Success!' },
responseParts: [
{
functionResponse: {
id: request.callId,
name,
response: { output: 'Success!' },
},
},
},
],
error: undefined,
errorType: undefined,
resultDisplay: 'Success!',
@@ -67,13 +70,15 @@ const createFakeCompletedToolCall = (
tool,
response: {
callId: request.callId,
responseParts: {
functionResponse: {
id: request.callId,
name,
response: { error: 'Tool failed' },
responseParts: [
{
functionResponse: {
id: request.callId,
name,
response: { error: 'Tool failed' },
},
},
},
],
error: error || new Error('Tool failed'),
errorType: ToolErrorType.UNKNOWN,
resultDisplay: 'Failure!',

View File

@@ -4,7 +4,7 @@
* SPDX-License-Identifier: Apache-2.0
*/
import { EventEmitter } from 'events';
import { EventEmitter } from 'node:events';
import {
EVENT_API_ERROR,
EVENT_API_RESPONSE,
@@ -12,7 +12,11 @@ import {
} from './constants.js';
import { ToolCallDecision } from './tool-call-decision.js';
import { ApiErrorEvent, ApiResponseEvent, ToolCallEvent } from './types.js';
import type {
ApiErrorEvent,
ApiResponseEvent,
ToolCallEvent,
} from './types.js';
export type UiEvent =
| (ApiResponseEvent & { 'event.name': typeof EVENT_API_RESPONSE })