Code Assist backend telemetry for user accept/reject of suggestions (#15206)

This commit is contained in:
Christian Gunderman
2025-12-17 15:12:59 -08:00
committed by GitHub
parent c28ff3d5a5
commit 5d13145995
8 changed files with 508 additions and 74 deletions
+137 -3
View File
@@ -8,17 +8,86 @@ import { FinishReason, type GenerateContentResponse } from '@google/genai';
import { getCitations } from '../utils/generateContentResponseUtilities.js';
import {
ActionStatus,
ConversationInteractionInteraction,
type ConversationInteraction,
type ConversationOffered,
type StreamingLatency,
} from './types.js';
import type { CompletedToolCall } from '../core/coreToolScheduler.js';
import type { Config } from '../config/config.js';
import { debugLogger } from '../utils/debugLogger.js';
import { getCodeAssistServer } from './codeAssist.js';
import { EDIT_TOOL_NAMES } from '../tools/tool-names.js';
import { getErrorMessage } from '../utils/errors.js';
import type { CodeAssistServer } from './server.js';
import { ToolConfirmationOutcome } from '../tools/tools.js';
export async function recordConversationOffered(
server: CodeAssistServer,
traceId: string | undefined,
response: GenerateContentResponse,
streamingLatency: StreamingLatency,
abortSignal: AbortSignal | undefined,
): Promise<void> {
try {
if (traceId) {
const offered = createConversationOffered(
response,
traceId,
abortSignal,
streamingLatency,
);
if (offered) {
await server.recordConversationOffered(offered);
}
}
} catch (error: unknown) {
debugLogger.warn(
`Error recording tool call interactions: ${getErrorMessage(error)}`,
);
}
}
export async function recordToolCallInteractions(
config: Config,
toolCalls: CompletedToolCall[],
): Promise<void> {
// Only send interaction events for responses that contain function calls.
if (toolCalls.length === 0) {
return;
}
try {
const server = getCodeAssistServer(config);
if (!server) {
return;
}
const interaction = summarizeToolCalls(toolCalls);
if (interaction) {
await server.recordConversationInteraction(interaction);
}
} catch (error: unknown) {
debugLogger.warn(
`Error recording tool call interactions: ${getErrorMessage(error)}`,
);
}
}
export function createConversationOffered(
response: GenerateContentResponse,
traceId: string,
signal: AbortSignal | undefined,
streamingLatency: StreamingLatency,
): ConversationOffered {
const actionStatus = getStatus(response, signal);
): ConversationOffered | undefined {
// Only send conversation offered events for responses that contain function
// calls. Non-function call events don't represent user actionable
// 'suggestions'.
if ((response.functionCalls?.length || 0) === 0) {
return;
}
const actionStatus = getStatusFromResponse(response, signal);
return {
citationCount: String(getCitations(response).length),
@@ -30,6 +99,71 @@ export function createConversationOffered(
};
}
function summarizeToolCalls(
toolCalls: CompletedToolCall[],
): ConversationInteraction | undefined {
let acceptedToolCalls = 0;
let actionStatus = undefined;
let traceId = undefined;
// Treat file edits as ACCEPT_FILE and everything else as unknown.
let isEdit = false;
// Iterate the tool calls and summarize them into a single conversation
// interaction so that the ConversationOffered and ConversationInteraction
// events are 1:1 in telemetry.
for (const toolCall of toolCalls) {
traceId ||= toolCall.request.traceId;
// If any tool call is canceled, we treat the entire interaction as canceled.
if (toolCall.status === 'cancelled') {
actionStatus = ActionStatus.ACTION_STATUS_CANCELLED;
break;
}
// If any tool call encounters an error, we treat the entire interaction as
// having errored.
if (toolCall.status === 'error') {
actionStatus = ActionStatus.ACTION_STATUS_ERROR_UNKNOWN;
break;
}
// Record if the tool call was accepted.
if (toolCall.outcome !== ToolConfirmationOutcome.Cancel) {
acceptedToolCalls++;
// Edits are ACCEPT_FILE, everything else is UNKNOWN.
if (EDIT_TOOL_NAMES.has(toolCall.request.name)) {
isEdit ||= true;
}
}
}
// Only file interaction telemetry if 100% of the tool calls were accepted.
return traceId && acceptedToolCalls / toolCalls.length >= 1
? createConversationInteraction(
traceId,
actionStatus || ActionStatus.ACTION_STATUS_NO_ERROR,
isEdit
? ConversationInteractionInteraction.ACCEPT_FILE
: ConversationInteractionInteraction.UNKNOWN,
)
: undefined;
}
function createConversationInteraction(
traceId: string,
status: ActionStatus,
interaction: ConversationInteractionInteraction,
): ConversationInteraction {
return {
traceId,
status,
interaction,
isAgentic: true,
};
}
function includesCode(resp: GenerateContentResponse): boolean {
if (!resp.candidates) {
return false;
@@ -47,7 +181,7 @@ function includesCode(resp: GenerateContentResponse): boolean {
return false;
}
function getStatus(
function getStatusFromResponse(
response: GenerateContentResponse,
signal: AbortSignal | undefined,
): ActionStatus {