fix: add missing trace info and cancellation events

This commit is contained in:
mingholy.lmh
2025-10-10 20:32:57 +08:00
parent 9fce177bd8
commit ff10b9fded
9 changed files with 206 additions and 3 deletions

View File

@@ -31,6 +31,9 @@ import {
ConversationFinishedEvent, ConversationFinishedEvent,
ApprovalMode, ApprovalMode,
parseAndFormatApiError, parseAndFormatApiError,
logUserCancellation,
UserCancellationEvent,
UserCancellationType,
} from '@qwen-code/qwen-code-core'; } from '@qwen-code/qwen-code-core';
import { type Part, type PartListUnion, FinishReason } from '@google/genai'; import { type Part, type PartListUnion, FinishReason } from '@google/genai';
import type { import type {
@@ -448,6 +451,17 @@ export const useGeminiStream = (
if (turnCancelledRef.current) { if (turnCancelledRef.current) {
return; 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) {
if (pendingHistoryItemRef.current.type === 'tool_group') { if (pendingHistoryItemRef.current.type === 'tool_group') {
const updatedTools = pendingHistoryItemRef.current.tools.map( const updatedTools = pendingHistoryItemRef.current.tools.map(
@@ -475,7 +489,14 @@ export const useGeminiStream = (
setIsResponding(false); setIsResponding(false);
setThought(null); // Reset thought when user cancels setThought(null); // Reset thought when user cancels
}, },
[addItem, pendingHistoryItemRef, setPendingHistoryItem, setThought], [
addItem,
pendingHistoryItemRef,
setPendingHistoryItem,
setThought,
config,
getPromptCount,
],
); );
const handleErrorEvent = useCallback( const handleErrorEvent = useCallback(

View File

@@ -23,6 +23,9 @@ import {
logToolCall, logToolCall,
ToolErrorType, ToolErrorType,
ToolCallEvent, ToolCallEvent,
logUserCancellation,
UserCancellationEvent,
UserCancellationType,
} from '../index.js'; } from '../index.js';
import type { Part, PartListUnion } from '@google/genai'; import type { Part, PartListUnion } from '@google/genai';
import { getResponseTextFromParts } from '../utils/generateContentResponseUtilities.js'; import { getResponseTextFromParts } from '../utils/generateContentResponseUtilities.js';
@@ -401,7 +404,7 @@ export class CoreToolScheduler {
} }
} }
return { const cancelledCall = {
request: currentCall.request, request: currentCall.request,
tool: toolInstance, tool: toolInstance,
invocation, invocation,
@@ -426,6 +429,11 @@ export class CoreToolScheduler {
durationMs, durationMs,
outcome, outcome,
} as CancelledToolCall; } as CancelledToolCall;
// Log the tool call cancellation
this.logToolCallCancellation(cancelledCall);
return cancelledCall;
} }
case 'validating': case 'validating':
return { 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) { private setToolCallOutcome(callId: string, outcome: ToolConfirmationOutcome) {
this.toolCalls = this.toolCalls.map((call) => { this.toolCalls = this.toolCalls.map((call) => {
if (call.request.callId !== callId) return call; if (call.request.callId !== callId) return call;

View File

@@ -221,6 +221,12 @@ export class ContentGenerationPipeline {
mergedResponse.usageMetadata = lastResponse.usageMetadata; 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 // Update the collected responses with the merged response
collectedGeminiResponses[collectedGeminiResponses.length - 1] = collectedGeminiResponses[collectedGeminiResponses.length - 1] =
mergedResponse; mergedResponse;

View File

@@ -25,6 +25,7 @@ export const EVENT_CONVERSATION_FINISHED = 'qwen-code.conversation_finished';
export const EVENT_MALFORMED_JSON_RESPONSE = export const EVENT_MALFORMED_JSON_RESPONSE =
'qwen-code.malformed_json_response'; 'qwen-code.malformed_json_response';
export const EVENT_SUBAGENT_EXECUTION = 'qwen-code.subagent_execution'; 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_COUNT = 'qwen-code.tool.call.count';
export const METRIC_TOOL_CALL_LATENCY = 'qwen-code.tool.call.latency'; export const METRIC_TOOL_CALL_LATENCY = 'qwen-code.tool.call.latency';

View File

@@ -26,6 +26,7 @@ export {
logKittySequenceOverflow, logKittySequenceOverflow,
logSlashCommand, logSlashCommand,
logToolCall, logToolCall,
logUserCancellation,
logUserPrompt, logUserPrompt,
} from './loggers.js'; } from './loggers.js';
export { export {
@@ -46,6 +47,8 @@ export {
SlashCommandStatus, SlashCommandStatus,
StartSessionEvent, StartSessionEvent,
ToolCallEvent, ToolCallEvent,
UserCancellationEvent,
UserCancellationType,
UserPromptEvent, UserPromptEvent,
} from './types.js'; } from './types.js';
export type { export type {
@@ -54,4 +57,5 @@ export type {
TelemetryEvent, TelemetryEvent,
} from './types.js'; } from './types.js';
export * from './uiTelemetry.js'; export * from './uiTelemetry.js';
export { QwenLogger } from './qwen-logger/qwen-logger.js';
export { DEFAULT_OTLP_ENDPOINT, DEFAULT_TELEMETRY_TARGET }; export { DEFAULT_OTLP_ENDPOINT, DEFAULT_TELEMETRY_TARGET };

View File

@@ -26,6 +26,7 @@ import {
EVENT_SLASH_COMMAND, EVENT_SLASH_COMMAND,
EVENT_SUBAGENT_EXECUTION, EVENT_SUBAGENT_EXECUTION,
EVENT_TOOL_CALL, EVENT_TOOL_CALL,
EVENT_USER_CANCELLATION,
EVENT_USER_PROMPT, EVENT_USER_PROMPT,
SERVICE_NAME, SERVICE_NAME,
} from './constants.js'; } from './constants.js';
@@ -62,6 +63,7 @@ import type {
StartSessionEvent, StartSessionEvent,
SubagentExecutionEvent, SubagentExecutionEvent,
ToolCallEvent, ToolCallEvent,
UserCancellationEvent,
UserPromptEvent, UserPromptEvent,
} from './types.js'; } from './types.js';
import { type UiEvent, uiTelemetryService } from './uiTelemetry.js'; import { type UiEvent, uiTelemetryService } from './uiTelemetry.js';
@@ -591,3 +593,25 @@ export function logSubagentExecution(
event.terminate_reason, 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);
}

View File

@@ -21,6 +21,8 @@ import {
IdeConnectionEvent, IdeConnectionEvent,
KittySequenceOverflowEvent, KittySequenceOverflowEvent,
IdeConnectionType, IdeConnectionType,
UserCancellationEvent,
UserCancellationType,
} from '../types.js'; } from '../types.js';
import type { RumEvent } from './event-types.js'; import type { RumEvent } from './event-types.js';
@@ -318,6 +320,85 @@ describe('QwenLogger', () => {
expect(flushSpy).toHaveBeenCalled(); 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', () => { describe('flush timing', () => {

View File

@@ -687,6 +687,25 @@ export class QwenLogger {
this.flushIfNeeded(); 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 { logEndSessionEvent(_event: EndSessionEvent): void {
const applicationEvent = this.createViewEvent('session', 'session_end', {}); const applicationEvent = this.createViewEvent('session', 'session_end', {});

View File

@@ -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 = export type TelemetryEvent =
| StartSessionEvent | StartSessionEvent
| EndSessionEvent | EndSessionEvent
@@ -555,4 +582,5 @@ export type TelemetryEvent =
| InvalidChunkEvent | InvalidChunkEvent
| ContentRetryEvent | ContentRetryEvent
| ContentRetryFailureEvent | ContentRetryFailureEvent
| SubagentExecutionEvent; | SubagentExecutionEvent
| UserCancellationEvent;