From ff10b9fded58531d3a79d49a924eb87642314c9a Mon Sep 17 00:00:00 2001 From: "mingholy.lmh" Date: Fri, 10 Oct 2025 20:32:57 +0800 Subject: [PATCH] fix: add missing trace info and cancellation events --- packages/cli/src/ui/hooks/useGeminiStream.ts | 23 +++++- packages/core/src/core/coreToolScheduler.ts | 21 ++++- .../core/openaiContentGenerator/pipeline.ts | 6 ++ packages/core/src/telemetry/constants.ts | 1 + packages/core/src/telemetry/index.ts | 4 + packages/core/src/telemetry/loggers.ts | 24 ++++++ .../telemetry/qwen-logger/qwen-logger.test.ts | 81 +++++++++++++++++++ .../src/telemetry/qwen-logger/qwen-logger.ts | 19 +++++ packages/core/src/telemetry/types.ts | 30 ++++++- 9 files changed, 206 insertions(+), 3 deletions(-) diff --git a/packages/cli/src/ui/hooks/useGeminiStream.ts b/packages/cli/src/ui/hooks/useGeminiStream.ts index 5bac2c41..8e654bae 100644 --- a/packages/cli/src/ui/hooks/useGeminiStream.ts +++ b/packages/cli/src/ui/hooks/useGeminiStream.ts @@ -31,6 +31,9 @@ import { ConversationFinishedEvent, ApprovalMode, parseAndFormatApiError, + logUserCancellation, + UserCancellationEvent, + UserCancellationType, } from '@qwen-code/qwen-code-core'; import { type Part, type PartListUnion, FinishReason } from '@google/genai'; import type { @@ -448,6 +451,17 @@ export const useGeminiStream = ( if (turnCancelledRef.current) { return; } + + // Log user cancellation event + const prompt_id = config.getSessionId() + '########' + getPromptCount(); + const cancellationEvent = new UserCancellationEvent( + UserCancellationType.REQUEST_CANCELLED, + { + prompt_id, + }, + ); + logUserCancellation(config, cancellationEvent); + if (pendingHistoryItemRef.current) { if (pendingHistoryItemRef.current.type === 'tool_group') { const updatedTools = pendingHistoryItemRef.current.tools.map( @@ -475,7 +489,14 @@ export const useGeminiStream = ( setIsResponding(false); setThought(null); // Reset thought when user cancels }, - [addItem, pendingHistoryItemRef, setPendingHistoryItem, setThought], + [ + addItem, + pendingHistoryItemRef, + setPendingHistoryItem, + setThought, + config, + getPromptCount, + ], ); const handleErrorEvent = useCallback( diff --git a/packages/core/src/core/coreToolScheduler.ts b/packages/core/src/core/coreToolScheduler.ts index 60e2b04f..855bd06e 100644 --- a/packages/core/src/core/coreToolScheduler.ts +++ b/packages/core/src/core/coreToolScheduler.ts @@ -23,6 +23,9 @@ import { logToolCall, ToolErrorType, ToolCallEvent, + logUserCancellation, + UserCancellationEvent, + UserCancellationType, } from '../index.js'; import type { Part, PartListUnion } from '@google/genai'; import { getResponseTextFromParts } from '../utils/generateContentResponseUtilities.js'; @@ -401,7 +404,7 @@ export class CoreToolScheduler { } } - return { + const cancelledCall = { request: currentCall.request, tool: toolInstance, invocation, @@ -426,6 +429,11 @@ export class CoreToolScheduler { durationMs, outcome, } as CancelledToolCall; + + // Log the tool call cancellation + this.logToolCallCancellation(cancelledCall); + + return cancelledCall; } case 'validating': return { @@ -1011,6 +1019,17 @@ export class CoreToolScheduler { } } + private logToolCallCancellation(toolCall: ToolCall): void { + const cancellationEvent = new UserCancellationEvent( + UserCancellationType.TOOL_CALL_CANCELLED, + { + prompt_id: toolCall.request.prompt_id, + tool_name: toolCall.request.name, + }, + ); + logUserCancellation(this.config, cancellationEvent); + } + private setToolCallOutcome(callId: string, outcome: ToolConfirmationOutcome) { this.toolCalls = this.toolCalls.map((call) => { if (call.request.callId !== callId) return call; diff --git a/packages/core/src/core/openaiContentGenerator/pipeline.ts b/packages/core/src/core/openaiContentGenerator/pipeline.ts index a911936c..28294967 100644 --- a/packages/core/src/core/openaiContentGenerator/pipeline.ts +++ b/packages/core/src/core/openaiContentGenerator/pipeline.ts @@ -221,6 +221,12 @@ export class ContentGenerationPipeline { mergedResponse.usageMetadata = lastResponse.usageMetadata; } + // Copy other essential properties from the current response + mergedResponse.responseId = response.responseId; + mergedResponse.createTime = response.createTime; + mergedResponse.modelVersion = response.modelVersion; + mergedResponse.promptFeedback = response.promptFeedback; + // Update the collected responses with the merged response collectedGeminiResponses[collectedGeminiResponses.length - 1] = mergedResponse; diff --git a/packages/core/src/telemetry/constants.ts b/packages/core/src/telemetry/constants.ts index 8cc69ce5..496818b1 100644 --- a/packages/core/src/telemetry/constants.ts +++ b/packages/core/src/telemetry/constants.ts @@ -25,6 +25,7 @@ export const EVENT_CONVERSATION_FINISHED = 'qwen-code.conversation_finished'; export const EVENT_MALFORMED_JSON_RESPONSE = 'qwen-code.malformed_json_response'; export const EVENT_SUBAGENT_EXECUTION = 'qwen-code.subagent_execution'; +export const EVENT_USER_CANCELLATION = 'qwen-code.user_cancellation'; export const METRIC_TOOL_CALL_COUNT = 'qwen-code.tool.call.count'; export const METRIC_TOOL_CALL_LATENCY = 'qwen-code.tool.call.latency'; diff --git a/packages/core/src/telemetry/index.ts b/packages/core/src/telemetry/index.ts index e70e2f64..68720bdd 100644 --- a/packages/core/src/telemetry/index.ts +++ b/packages/core/src/telemetry/index.ts @@ -26,6 +26,7 @@ export { logKittySequenceOverflow, logSlashCommand, logToolCall, + logUserCancellation, logUserPrompt, } from './loggers.js'; export { @@ -46,6 +47,8 @@ export { SlashCommandStatus, StartSessionEvent, ToolCallEvent, + UserCancellationEvent, + UserCancellationType, UserPromptEvent, } from './types.js'; export type { @@ -54,4 +57,5 @@ export type { TelemetryEvent, } from './types.js'; export * from './uiTelemetry.js'; +export { QwenLogger } from './qwen-logger/qwen-logger.js'; export { DEFAULT_OTLP_ENDPOINT, DEFAULT_TELEMETRY_TARGET }; diff --git a/packages/core/src/telemetry/loggers.ts b/packages/core/src/telemetry/loggers.ts index 627ac86d..b58140a1 100644 --- a/packages/core/src/telemetry/loggers.ts +++ b/packages/core/src/telemetry/loggers.ts @@ -26,6 +26,7 @@ import { EVENT_SLASH_COMMAND, EVENT_SUBAGENT_EXECUTION, EVENT_TOOL_CALL, + EVENT_USER_CANCELLATION, EVENT_USER_PROMPT, SERVICE_NAME, } from './constants.js'; @@ -62,6 +63,7 @@ import type { StartSessionEvent, SubagentExecutionEvent, ToolCallEvent, + UserCancellationEvent, UserPromptEvent, } from './types.js'; import { type UiEvent, uiTelemetryService } from './uiTelemetry.js'; @@ -591,3 +593,25 @@ export function logSubagentExecution( event.terminate_reason, ); } + +export function logUserCancellation( + config: Config, + event: UserCancellationEvent, +): void { + QwenLogger.getInstance(config)?.logUserCancellationEvent(event); + if (!isTelemetrySdkInitialized()) return; + + const attributes: LogAttributes = { + ...getCommonAttributes(config), + ...event, + 'event.name': EVENT_USER_CANCELLATION, + 'event.timestamp': new Date().toISOString(), + }; + + const logger = logs.getLogger(SERVICE_NAME); + const logRecord: LogRecord = { + body: `User cancellation: ${event.cancellation_type}.`, + attributes, + }; + logger.emit(logRecord); +} diff --git a/packages/core/src/telemetry/qwen-logger/qwen-logger.test.ts b/packages/core/src/telemetry/qwen-logger/qwen-logger.test.ts index 0aa44d98..804d6992 100644 --- a/packages/core/src/telemetry/qwen-logger/qwen-logger.test.ts +++ b/packages/core/src/telemetry/qwen-logger/qwen-logger.test.ts @@ -21,6 +21,8 @@ import { IdeConnectionEvent, KittySequenceOverflowEvent, IdeConnectionType, + UserCancellationEvent, + UserCancellationType, } from '../types.js'; import type { RumEvent } from './event-types.js'; @@ -318,6 +320,85 @@ describe('QwenLogger', () => { expect(flushSpy).toHaveBeenCalled(); }); + + it('should log user cancellation events for request cancellation', () => { + const logger = QwenLogger.getInstance(mockConfig)!; + const enqueueSpy = vi.spyOn(logger, 'enqueueLogEvent'); + + const event = new UserCancellationEvent( + UserCancellationType.REQUEST_CANCELLED, + { + prompt_id: 'test-prompt-123', + }, + ); + + logger.logUserCancellationEvent(event); + + expect(enqueueSpy).toHaveBeenCalledWith( + expect.objectContaining({ + event_type: 'action', + type: 'cancellation', + name: 'user_cancellation', + properties: { + cancellation_type: UserCancellationType.REQUEST_CANCELLED, + prompt_id: 'test-prompt-123', + tool_name: undefined, + }, + }), + ); + }); + + it('should log user cancellation events for tool call cancellation', () => { + const logger = QwenLogger.getInstance(mockConfig)!; + const enqueueSpy = vi.spyOn(logger, 'enqueueLogEvent'); + + const event = new UserCancellationEvent( + UserCancellationType.TOOL_CALL_CANCELLED, + { + prompt_id: 'test-prompt-456', + tool_name: 'read_file', + }, + ); + + logger.logUserCancellationEvent(event); + + expect(enqueueSpy).toHaveBeenCalledWith( + expect.objectContaining({ + event_type: 'action', + type: 'cancellation', + name: 'user_cancellation', + properties: { + cancellation_type: UserCancellationType.TOOL_CALL_CANCELLED, + prompt_id: 'test-prompt-456', + tool_name: 'read_file', + }, + }), + ); + }); + + it('should log user cancellation events with minimal data', () => { + const logger = QwenLogger.getInstance(mockConfig)!; + const enqueueSpy = vi.spyOn(logger, 'enqueueLogEvent'); + + const event = new UserCancellationEvent( + UserCancellationType.REQUEST_CANCELLED, + ); + + logger.logUserCancellationEvent(event); + + expect(enqueueSpy).toHaveBeenCalledWith( + expect.objectContaining({ + event_type: 'action', + type: 'cancellation', + name: 'user_cancellation', + properties: { + cancellation_type: UserCancellationType.REQUEST_CANCELLED, + prompt_id: undefined, + tool_name: undefined, + }, + }), + ); + }); }); describe('flush timing', () => { diff --git a/packages/core/src/telemetry/qwen-logger/qwen-logger.ts b/packages/core/src/telemetry/qwen-logger/qwen-logger.ts index 215e6d95..75a2f6f2 100644 --- a/packages/core/src/telemetry/qwen-logger/qwen-logger.ts +++ b/packages/core/src/telemetry/qwen-logger/qwen-logger.ts @@ -687,6 +687,25 @@ export class QwenLogger { this.flushIfNeeded(); } + logUserCancellationEvent( + event: import('../types.js').UserCancellationEvent, + ): void { + const rumEvent = this.createActionEvent( + 'cancellation', + 'user_cancellation', + { + properties: { + cancellation_type: event.cancellation_type, + prompt_id: event.prompt_id, + tool_name: event.tool_name, + }, + }, + ); + + this.enqueueLogEvent(rumEvent); + this.flushIfNeeded(); + } + logEndSessionEvent(_event: EndSessionEvent): void { const applicationEvent = this.createViewEvent('session', 'session_end', {}); diff --git a/packages/core/src/telemetry/types.ts b/packages/core/src/telemetry/types.ts index 400fcc09..b1727302 100644 --- a/packages/core/src/telemetry/types.ts +++ b/packages/core/src/telemetry/types.ts @@ -535,6 +535,33 @@ export class SubagentExecutionEvent implements BaseTelemetryEvent { } } +export enum UserCancellationType { + REQUEST_CANCELLED = 'request_cancelled', + TOOL_CALL_CANCELLED = 'tool_call_cancelled', +} + +export class UserCancellationEvent implements BaseTelemetryEvent { + 'event.name': 'user_cancellation'; + 'event.timestamp': string; + cancellation_type: UserCancellationType; + prompt_id?: string; + tool_name?: string; + + constructor( + cancellation_type: UserCancellationType, + options?: { + prompt_id?: string; + tool_name?: string; + }, + ) { + this['event.name'] = 'user_cancellation'; + this['event.timestamp'] = new Date().toISOString(); + this.cancellation_type = cancellation_type; + this.prompt_id = options?.prompt_id; + this.tool_name = options?.tool_name; + } +} + export type TelemetryEvent = | StartSessionEvent | EndSessionEvent @@ -555,4 +582,5 @@ export type TelemetryEvent = | InvalidChunkEvent | ContentRetryEvent | ContentRetryFailureEvent - | SubagentExecutionEvent; + | SubagentExecutionEvent + | UserCancellationEvent;