mirror of
https://github.com/QwenLM/qwen-code.git
synced 2025-12-20 16:57:46 +00:00
Merge tag 'v0.3.0' into chore/sync-gemini-cli-v0.3.0
This commit is contained in:
@@ -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,
|
||||
|
||||
@@ -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];
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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 };
|
||||
|
||||
@@ -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(() => {
|
||||
|
||||
@@ -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', () => {
|
||||
|
||||
@@ -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,
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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';
|
||||
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
|
||||
@@ -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', () => ({
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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 {
|
||||
|
||||
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;
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
@@ -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!',
|
||||
|
||||
@@ -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 })
|
||||
|
||||
Reference in New Issue
Block a user