Przeglądaj źródła

feat: add telemetry for chat textarea autocomplete

## Summary
Adds telemetry tracking for the chat textarea autocomplete feature by reusing the existing `AutocompleteTelemetry` class with a new autocomplete type discriminator.

## Changes

### Extended AutocompleteTelemetry Class
- Added `AutocompleteType` discriminator (`"inline"` | `"chat-textarea"`) to distinguish between editor autocomplete and chat textarea autocomplete
- All telemetry events now include `autocompleteType` property for filtering in analytics
- Made token/cost properties optional in `captureLlmRequestCompleted` (not available for chat completions)
- Added optional `suggestionLength` parameter to `captureAcceptSuggestion`

### Chat Textarea Integration
- Updated `ChatTextAreaAutocomplete.ts` to track:
  - Suggestion requests (`captureSuggestionRequested`)
  - LLM request completion with latency (`captureLlmRequestCompleted`)
  - LLM request failures with error details (`captureLlmRequestFailed`)
  - Suggestion filtering with reasons (`captureSuggestionFiltered`)
  - Successful suggestion returns (`captureLlmSuggestionReturned`)

### Acceptance Tracking
- Added new `chatCompletionAccepted` webview message type
- Created `handleChatCompletionAccepted.ts` handler for acceptance events
- Updated `useChatGhostText.ts` to send telemetry when user accepts suggestions via Tab or ArrowRight
- Tracks suggestion length for accepted completions

### Files Changed
- `src/services/ghost/classic-auto-complete/AutocompleteTelemetry.ts` - Extended with type discriminator
- `src/services/ghost/chat-autocomplete/ChatTextAreaAutocomplete.ts` - Added telemetry calls
- `src/services/ghost/chat-autocomplete/handleChatCompletionAccepted.ts` - New handler
- `src/core/webview/webviewMessageHandler.ts` - Added message handler
- `src/shared/WebviewMessage.ts` - Added new message type
- `webview-ui/src/components/chat/hooks/useChatGhostText.ts` - Send acceptance events

## Design Decisions
- **Reused existing telemetry class** rather than creating a separate one, using a type discriminator for filtering
- **Same event names** as inline autocomplete for consistent analytics, differentiated by `autocompleteType`
- **Acceptance tracked from webview** since that's where user interaction happens
- **Error handling** wraps LLM calls to capture failures with latency and error details

## Testing
- ✅ All tests pass
- ✅ Type checking passes
- ✅ Linting passes
Mark IJbema 2 miesięcy temu
rodzic
commit
e259b04037

+ 5 - 0
.changeset/small-towns-march.md

@@ -0,0 +1,5 @@
+---
+"kilo-code": patch
+---
+
+Add chat autocomplete telemetry

+ 7 - 0
src/core/webview/webviewMessageHandler.ts

@@ -3717,6 +3717,13 @@ export const webviewMessageHandler = async (
 			)
 			break
 		}
+		case "chatCompletionAccepted": {
+			const { handleChatCompletionAccepted } = await import(
+				"../../services/ghost/chat-autocomplete/handleChatCompletionAccepted"
+			)
+			handleChatCompletionAccepted(message as WebviewMessage & { type: "chatCompletionAccepted" })
+			break
+		}
 		// kilocode_change end: Chat text area FIM autocomplete
 		case "openCommandFile": {
 			try {

+ 68 - 22
src/services/ghost/chat-autocomplete/ChatTextAreaAutocomplete.ts

@@ -1,9 +1,10 @@
 import * as vscode from "vscode"
 import { GhostModel } from "../GhostModel"
 import { ProviderSettingsManager } from "../../../core/config/ProviderSettingsManager"
-import { VisibleCodeContext } from "../types"
+import { AutocompleteContext, VisibleCodeContext } from "../types"
 import { ApiStreamChunk } from "../../../api/transform/stream"
 import { removePrefixOverlap } from "../../continuedev/core/autocomplete/postprocessing/removePrefixOverlap.js"
+import { AutocompleteTelemetry } from "../classic-auto-complete/AutocompleteTelemetry"
 
 /**
  * Service for providing FIM-based autocomplete suggestions in ChatTextArea
@@ -11,10 +12,12 @@ import { removePrefixOverlap } from "../../continuedev/core/autocomplete/postpro
 export class ChatTextAreaAutocomplete {
 	private model: GhostModel
 	private providerSettingsManager: ProviderSettingsManager
+	private telemetry: AutocompleteTelemetry
 
 	constructor(providerSettingsManager: ProviderSettingsManager) {
 		this.model = new GhostModel()
 		this.providerSettingsManager = providerSettingsManager
+		this.telemetry = new AutocompleteTelemetry("chat-textarea")
 	}
 
 	async initialize(): Promise<boolean> {
@@ -30,6 +33,18 @@ export class ChatTextAreaAutocomplete {
 	}
 
 	async getCompletion(userText: string, visibleCodeContext?: VisibleCodeContext): Promise<{ suggestion: string }> {
+		const startTime = Date.now()
+
+		// Build context for telemetry
+		const context: AutocompleteContext = {
+			languageId: "chat", // Chat textarea doesn't have a language ID
+			modelId: this.model.getModelName(),
+			provider: this.model.getProviderDisplayName(),
+		}
+
+		// Capture suggestion requested
+		this.telemetry.captureSuggestionRequested(context)
+
 		if (!this.model.loaded) {
 			const loaded = await this.initialize()
 			if (!loaded) {
@@ -47,29 +62,60 @@ export class ChatTextAreaAutocomplete {
 
 		let response = ""
 
-		// Use FIM if supported, otherwise fall back to chat-based completion
-		if (this.model.supportsFim()) {
-			await this.model.generateFimResponse(prefix, suffix, (chunk) => {
-				response += chunk
-			})
-		} else {
-			// Fall back to chat-based completion for models without FIM support
-			const systemPrompt = this.getChatSystemPrompt()
-			const userPrompt = this.getChatUserPrompt(prefix)
-
-			await this.model.generateResponse(systemPrompt, userPrompt, (chunk) => {
-				if (chunk.type === "text") {
-					response += chunk.text
-				}
-			})
-		}
+		try {
+			// Use FIM if supported, otherwise fall back to chat-based completion
+			if (this.model.supportsFim()) {
+				await this.model.generateFimResponse(prefix, suffix, (chunk) => {
+					response += chunk
+				})
+			} else {
+				// Fall back to chat-based completion for models without FIM support
+				const systemPrompt = this.getChatSystemPrompt()
+				const userPrompt = this.getChatUserPrompt(prefix)
+
+				await this.model.generateResponse(systemPrompt, userPrompt, (chunk) => {
+					if (chunk.type === "text") {
+						response += chunk.text
+					}
+				})
+			}
 
-		const cleanedSuggestion = this.cleanSuggestion(response, userText)
-		console.log(
-			`[ChatAutocomplete] prefix: ${JSON.stringify(userText)} | response: ${JSON.stringify(response)} | cleanedSuggestion: ${JSON.stringify(cleanedSuggestion)}`,
-		)
+			const latencyMs = Date.now() - startTime
+
+			// Capture successful LLM request
+			this.telemetry.captureLlmRequestCompleted(
+				{
+					latencyMs,
+					// Token counts not available from current API
+				},
+				context,
+			)
+
+			const cleanedSuggestion = this.cleanSuggestion(response, userText)
+
+			// Track if suggestion was filtered or returned
+			if (!cleanedSuggestion) {
+				if (!response.trim()) {
+					this.telemetry.captureSuggestionFiltered("empty_response", context)
+				} else {
+					this.telemetry.captureSuggestionFiltered("filtered_by_postprocessing", context)
+				}
+			} else {
+				this.telemetry.captureLlmSuggestionReturned(context, cleanedSuggestion.length)
+			}
 
-		return { suggestion: cleanedSuggestion }
+			return { suggestion: cleanedSuggestion }
+		} catch (error) {
+			const latencyMs = Date.now() - startTime
+			this.telemetry.captureLlmRequestFailed(
+				{
+					latencyMs,
+					error: error instanceof Error ? error.message : String(error),
+				},
+				context,
+			)
+			return { suggestion: "" }
+		}
 	}
 
 	/**

+ 25 - 0
src/services/ghost/chat-autocomplete/handleChatCompletionAccepted.ts

@@ -0,0 +1,25 @@
+import { WebviewMessage } from "../../../shared/WebviewMessage"
+import { AutocompleteTelemetry } from "../classic-auto-complete/AutocompleteTelemetry"
+
+// Singleton telemetry instance for chat-textarea autocomplete
+// This ensures we use the same instance across requests and acceptance events
+let telemetryInstance: AutocompleteTelemetry | null = null
+
+/**
+ * Get or create the telemetry instance for chat-textarea autocomplete
+ */
+export function getChatAutocompleteTelemetry(): AutocompleteTelemetry {
+	if (!telemetryInstance) {
+		telemetryInstance = new AutocompleteTelemetry("chat-textarea")
+	}
+	return telemetryInstance
+}
+
+/**
+ * Handles a chat completion accepted event from the webview.
+ * Captures telemetry when the user accepts a suggestion via Tab or ArrowRight.
+ */
+export function handleChatCompletionAccepted(message: WebviewMessage & { type: "chatCompletionAccepted" }): void {
+	const telemetry = getChatAutocompleteTelemetry()
+	telemetry.captureAcceptSuggestion(message.suggestionLength)
+}

+ 31 - 12
src/services/ghost/classic-auto-complete/AutocompleteTelemetry.ts

@@ -4,23 +4,38 @@ import type { AutocompleteContext, CacheMatchType } from "../types"
 
 export type { AutocompleteContext, CacheMatchType }
 
+/**
+ * Type of autocomplete being used
+ * - "inline": Classic inline code completion in the editor
+ * - "chat-textarea": Autocomplete in the chat input textarea
+ */
+export type AutocompleteType = "inline" | "chat-textarea"
+
 /**
  * Telemetry service for autocomplete events.
  * Can be initialized without parameters and injected into components that need telemetry tracking.
+ * Supports different autocomplete types via the `autocompleteType` property.
  */
 export class AutocompleteTelemetry {
-	constructor() {}
+	private readonly autocompleteType: AutocompleteType
+
+	/**
+	 * Create a new AutocompleteTelemetry instance
+	 * @param autocompleteType - The type of autocomplete (defaults to "inline" for backward compatibility)
+	 */
+	constructor(autocompleteType: AutocompleteType = "inline") {
+		this.autocompleteType = autocompleteType
+	}
 
 	private captureEvent(event: TelemetryEventName, properties?: Record<string, unknown>): void {
 		// also log to console:
 		if (TelemetryService.hasInstance()) {
-			if (properties !== undefined) {
-				TelemetryService.instance.captureEvent(event, properties)
-				console.log(`Autocomplete Telemetry event: ${event}`, properties)
-			} else {
-				TelemetryService.instance.captureEvent(event)
-				console.log(`Autocomplete Telemetry event: ${event}`)
+			const propsWithType = {
+				...properties,
+				autocompleteType: this.autocompleteType,
 			}
+			TelemetryService.instance.captureEvent(event, propsWithType)
+			console.log(`Autocomplete Telemetry event: ${event}`, propsWithType)
 		}
 	}
 
@@ -98,9 +113,9 @@ export class AutocompleteTelemetry {
 	public captureLlmRequestCompleted(
 		properties: {
 			latencyMs: number
-			cost: number
-			inputTokens: number
-			outputTokens: number
+			cost?: number
+			inputTokens?: number
+			outputTokens?: number
 		},
 		context: AutocompleteContext,
 	): void {
@@ -132,8 +147,12 @@ export class AutocompleteTelemetry {
 	 * There are two ways to analyze what percentage was accepted:
 	 * 1. Sum of this event divided by the sum of the suggestion returned event
 	 * 2. Sum of this event divided by the sum of the suggestion returned + cache hit events
+	 *
+	 * @param suggestionLength - Optional length of the accepted suggestion
 	 */
-	public captureAcceptSuggestion(): void {
-		this.captureEvent(TelemetryEventName.AUTOCOMPLETE_ACCEPT_SUGGESTION)
+	public captureAcceptSuggestion(suggestionLength?: number): void {
+		this.captureEvent(TelemetryEventName.AUTOCOMPLETE_ACCEPT_SUGGESTION, {
+			...(suggestionLength !== undefined && { suggestionLength }),
+		})
 	}
 }

+ 2 - 0
src/shared/WebviewMessage.ts

@@ -287,7 +287,9 @@ export interface WebviewMessage {
 		| "cancelDeviceAuth" // kilocode_change: Cancel device auth flow
 		| "deviceAuthCompleteWithProfile" // kilocode_change: Device auth complete with specific profile
 		| "requestChatCompletion" // kilocode_change: Request FIM completion for chat text area
+		| "chatCompletionAccepted" // kilocode_change: User accepted a chat completion suggestion
 	text?: string
+	suggestionLength?: number // kilocode_change: Length of accepted suggestion for telemetry
 	completionRequestId?: string // kilocode_change
 	shareId?: string // kilocode_change - for sessionFork
 	sessionId?: string // kilocode_change - for sessionSelect

+ 10 - 0
webview-ui/src/components/chat/hooks/useChatGhostText.ts

@@ -65,6 +65,11 @@ export function useChatGhostText({
 				event.preventDefault()
 				skipNextCompletionRef.current = true
 				insertTextAtCursor(textArea, ghostText)
+				// Send telemetry event for accepted suggestion
+				vscode.postMessage({
+					type: "chatCompletionAccepted",
+					suggestionLength: ghostText.length,
+				})
 				setGhostText("")
 				return true
 			}
@@ -81,6 +86,11 @@ export function useChatGhostText({
 				skipNextCompletionRef.current = true
 				const { word, remainder } = extractNextWord(ghostText)
 				insertTextAtCursor(textArea, word)
+				// Send telemetry event for accepted word
+				vscode.postMessage({
+					type: "chatCompletionAccepted",
+					suggestionLength: word.length,
+				})
 				setGhostText(remainder)
 				return true
 			}