mirror of
https://github.com/QwenLM/qwen-code.git
synced 2025-12-21 09:17:53 +00:00
fix: add missing trace info and cancellation events
This commit is contained in:
@@ -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(
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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';
|
||||||
|
|||||||
@@ -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 };
|
||||||
|
|||||||
@@ -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);
|
||||||
|
}
|
||||||
|
|||||||
@@ -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', () => {
|
||||||
|
|||||||
@@ -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', {});
|
||||||
|
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
Reference in New Issue
Block a user