diff --git a/packages/kilo-telemetry/src/identity.ts b/packages/kilo-telemetry/src/identity.ts index 4d66e763d4..532834a595 100644 --- a/packages/kilo-telemetry/src/identity.ts +++ b/packages/kilo-telemetry/src/identity.ts @@ -14,6 +14,14 @@ export namespace Identity { export async function getMachineId(): Promise { if (machineId) return machineId + // kilocode_change start - Allow env var override for identity continuity with VS Code extension + const override = process.env.KILO_MACHINE_ID + if (override) { + machineId = override + return machineId + } + // kilocode_change end + const filepath = path.join(dataPath, "telemetry-id") const file = Bun.file(filepath) diff --git a/packages/kilo-telemetry/src/telemetry.ts b/packages/kilo-telemetry/src/telemetry.ts index a85a7f2baa..d982d4fcfa 100644 --- a/packages/kilo-telemetry/src/telemetry.ts +++ b/packages/kilo-telemetry/src/telemetry.ts @@ -8,6 +8,7 @@ export interface TelemetryProperties { appName: string appVersion: string platform: string + editorName?: string // kilocode_change } export namespace Telemetry { @@ -25,11 +26,23 @@ export namespace Telemetry { Identity.setDataPath(options.dataPath) props.appVersion = options.version + // kilocode_change start - Apply env var overrides for editor name and platform + const editor = process.env.KILO_EDITOR_NAME + if (editor) props.editorName = editor + const platform = process.env.KILO_PLATFORM + if (platform) props.platform = platform + // kilocode_change end + Client.init() - Client.setEnabled(options.enabled) + + // kilocode_change start - Allow KILO_TELEMETRY_LEVEL to override enabled state + const level = process.env.KILO_TELEMETRY_LEVEL + const enabled = level ? level === "all" : options.enabled + // kilocode_change end + Client.setEnabled(enabled) // Initialize OpenTelemetry tracer for AI SDK spans - TracerSetup.init({ version: options.version, enabled: options.enabled }) + TracerSetup.init({ version: options.version, enabled }) await Identity.getMachineId() diff --git a/packages/kilo-vscode/src/KiloProvider.ts b/packages/kilo-vscode/src/KiloProvider.ts index 516180b4fd..e6ca78f315 100644 --- a/packages/kilo-vscode/src/KiloProvider.ts +++ b/packages/kilo-vscode/src/KiloProvider.ts @@ -4,8 +4,11 @@ import { type HttpClient, type SessionInfo, type SSEEvent, type KiloConnectionSe import { handleChatCompletionRequest } from "./services/autocomplete/chat-autocomplete/handleChatCompletionRequest" import { handleChatCompletionAccepted } from "./services/autocomplete/chat-autocomplete/handleChatCompletionAccepted" import { buildWebviewHtml } from "./utils" +// kilocode_change start +import { TelemetryProxy, type TelemetryPropertiesProvider } from "./services/telemetry" +// kilocode_change end -export class KiloProvider implements vscode.WebviewViewProvider { +export class KiloProvider implements vscode.WebviewViewProvider, TelemetryPropertiesProvider /* kilocode_change */ { public static readonly viewType = "kilo-code.new.sidebarView" private webview: vscode.Webview | null = null @@ -36,7 +39,30 @@ export class KiloProvider implements vscode.WebviewViewProvider { constructor( private readonly extensionUri: vscode.Uri, private readonly connectionService: KiloConnectionService, - ) {} + ) { + // kilocode_change start — register as telemetry properties provider + try { + TelemetryProxy.getInstance().setProvider(this) + } catch { + // TelemetryProxy may not be initialized yet — safe to ignore + } + // kilocode_change end + } + + // kilocode_change start + getTelemetryProperties(): Record { + return { + appName: "Kilo", + appVersion: this.extensionVersion, + vscodeVersion: vscode.version, + platform: process.platform, + editorName: vscode.env.appName, + machineId: vscode.env.machineId, + vscodeIsTelemetryEnabled: vscode.env.isTelemetryEnabled, + architecture: "new", + } + } + // kilocode_change end /** * Convenience getter that returns the shared HttpClient or null if not yet connected. @@ -378,6 +404,15 @@ export class KiloProvider implements vscode.WebviewViewProvider { case "resetAllSettings": await this.handleResetAllSettings() break + // kilocode_change start — forward webview telemetry to TelemetryProxy + case "telemetry": + try { + TelemetryProxy.getInstance().capture(message.event, message.properties) + } catch { + // TelemetryProxy not initialized — safe to ignore + } + break + // kilocode_change end } }) } diff --git a/packages/kilo-vscode/src/extension.ts b/packages/kilo-vscode/src/extension.ts index a2dfdb2713..6d6caf390d 100644 --- a/packages/kilo-vscode/src/extension.ts +++ b/packages/kilo-vscode/src/extension.ts @@ -5,10 +5,15 @@ import { EXTENSION_DISPLAY_NAME } from "./constants" import { KiloConnectionService } from "./services/cli-backend" import { registerAutocompleteProvider } from "./services/autocomplete" import { BrowserAutomationService } from "./services/browser-automation" +import { TelemetryProxy } from "./services/telemetry" // kilocode_change export function activate(context: vscode.ExtensionContext) { console.log("Kilo Code extension is now active") + // kilocode_change start — initialize telemetry proxy + const telemetry = TelemetryProxy.createInstance() + // kilocode_change end + // Create shared connection service (one server for all webviews) const connectionService = new KiloConnectionService(context) @@ -17,9 +22,16 @@ export function activate(context: vscode.ExtensionContext) { browserAutomationService.syncWithSettings() // Re-register browser automation MCP server on CLI backend reconnect + // kilocode_change — also configure telemetry when connected const unsubscribeStateChange = connectionService.onStateChange((state) => { if (state === "connected") { browserAutomationService.reregisterIfEnabled() + // kilocode_change start — configure telemetry with server URL + password + const config = connectionService.getServerConfig() + if (config) { + telemetry.configure(config.baseUrl, config.password) + } + // kilocode_change end } }) @@ -83,7 +95,15 @@ export function activate(context: vscode.ExtensionContext) { }) } -export function deactivate() {} +export function deactivate() { + // kilocode_change start + try { + TelemetryProxy.getInstance().shutdown() + } catch { + // Instance may not exist — safe to ignore + } + // kilocode_change end +} async function openKiloInNewTab(context: vscode.ExtensionContext, connectionService: KiloConnectionService) { const lastCol = Math.max(...vscode.window.visibleTextEditors.map((e) => e.viewColumn || 0), 0) diff --git a/packages/kilo-vscode/src/services/autocomplete/AutocompleteServiceManager.ts b/packages/kilo-vscode/src/services/autocomplete/AutocompleteServiceManager.ts index 64797049cb..16eed5ccf7 100644 --- a/packages/kilo-vscode/src/services/autocomplete/AutocompleteServiceManager.ts +++ b/packages/kilo-vscode/src/services/autocomplete/AutocompleteServiceManager.ts @@ -1,12 +1,12 @@ import crypto from "crypto" import * as vscode from "vscode" import { t } from "./shims/i18n" -import { TelemetryStub } from "./shims/TelemetryStub" +import { TelemetryProxy, TelemetryEventName } from "../telemetry" // kilocode_change import { AutocompleteModel } from "./AutocompleteModel" import { AutocompleteStatusBar } from "./AutocompleteStatusBar" import { AutocompleteCodeActionProvider } from "./AutocompleteCodeActionProvider" import { AutocompleteInlineCompletionProvider } from "./classic-auto-complete/AutocompleteInlineCompletionProvider" -import { AutocompleteTelemetry, TelemetryEventName } from "./classic-auto-complete/AutocompleteTelemetry" +import { AutocompleteTelemetry } from "./classic-auto-complete/AutocompleteTelemetry" import type { KiloConnectionService } from "../cli-backend" const CONFIG_SECTION = "kilo-code.new.autocomplete" @@ -43,7 +43,6 @@ export class AutocompleteServiceManager { private readonly model: AutocompleteModel private readonly context: vscode.ExtensionContext - private readonly telemetry = new TelemetryStub() private settings: AutocompleteServiceSettings | null = null private taskId: string | null = null @@ -134,7 +133,7 @@ export class AutocompleteServiceManager { enableSmartInlineTaskKeybinding: false, }) - this.telemetry.captureEvent(TelemetryEventName.GHOST_SERVICE_DISABLED) + TelemetryProxy.tryGetInstance()?.capture(TelemetryEventName.GHOST_SERVICE_DISABLED) await this.load() } @@ -232,7 +231,7 @@ export class AutocompleteServiceManager { } this.taskId = crypto.randomUUID() - this.telemetry.captureEvent(TelemetryEventName.INLINE_ASSIST_AUTO_TASK, { + TelemetryProxy.tryGetInstance()?.capture(TelemetryEventName.INLINE_ASSIST_AUTO_TASK, { taskId: this.taskId, }) diff --git a/packages/kilo-vscode/src/services/autocomplete/classic-auto-complete/AutocompleteTelemetry.ts b/packages/kilo-vscode/src/services/autocomplete/classic-auto-complete/AutocompleteTelemetry.ts index 3c168e60ff..41e287f2cb 100644 --- a/packages/kilo-vscode/src/services/autocomplete/classic-auto-complete/AutocompleteTelemetry.ts +++ b/packages/kilo-vscode/src/services/autocomplete/classic-auto-complete/AutocompleteTelemetry.ts @@ -1,19 +1,6 @@ -import { TelemetryStub, type ITelemetryClient } from "../shims/TelemetryStub" +import { TelemetryProxy, TelemetryEventName } from "../../telemetry" // kilocode_change import type { AutocompleteContext, CacheMatchType, FillInAtCursorSuggestion } from "../types" -export const TelemetryEventName = { - AUTOCOMPLETE_SUGGESTION_REQUESTED: "Autocomplete Suggestion Requested", - AUTOCOMPLETE_LLM_REQUEST_COMPLETED: "Autocomplete LLM Request Completed", - AUTOCOMPLETE_LLM_REQUEST_FAILED: "Autocomplete LLM Request Failed", - AUTOCOMPLETE_LLM_SUGGESTION_RETURNED: "Autocomplete LLM Suggestion Returned", - AUTOCOMPLETE_SUGGESTION_CACHE_HIT: "Autocomplete Suggestion Cache Hit", - AUTOCOMPLETE_ACCEPT_SUGGESTION: "Autocomplete Accept Suggestion", - AUTOCOMPLETE_SUGGESTION_FILTERED: "Autocomplete Suggestion Filtered", - AUTOCOMPLETE_UNIQUE_SUGGESTION_SHOWN: "Autocomplete Unique Suggestion Shown", - INLINE_ASSIST_AUTO_TASK: "Inline Assist Auto Task", - GHOST_SERVICE_DISABLED: "Ghost Service Disabled", -} as const - export type { AutocompleteContext, CacheMatchType, FillInAtCursorSuggestion } /** @@ -95,17 +82,12 @@ export class AutocompleteTelemetry { this.autocompleteType = autocompleteType } - private telemetryClient: ITelemetryClient = new TelemetryStub() - - private captureEvent( - event: (typeof TelemetryEventName)[keyof typeof TelemetryEventName], - properties?: Record, - ): void { - const propsWithType = { + private captureEvent(event: TelemetryEventName, properties?: Record): void { + const props = { ...properties, autocompleteType: this.autocompleteType, } - this.telemetryClient.captureEvent(event, propsWithType) + TelemetryProxy.tryGetInstance()?.capture(event, props) } /** diff --git a/packages/kilo-vscode/src/services/autocomplete/shims/TelemetryStub.ts b/packages/kilo-vscode/src/services/autocomplete/shims/TelemetryStub.ts deleted file mode 100644 index 99fe2fea69..0000000000 --- a/packages/kilo-vscode/src/services/autocomplete/shims/TelemetryStub.ts +++ /dev/null @@ -1,18 +0,0 @@ -/** - * No-op telemetry client for autocomplete. - * - * The extension has no telemetry system yet. This stub logs events - * to the console for debugging but does not send them anywhere. - */ - -export type TelemetryEventName = string - -export interface ITelemetryClient { - captureEvent(event: TelemetryEventName, properties?: Record): void -} - -export class TelemetryStub implements ITelemetryClient { - captureEvent(event: TelemetryEventName, properties?: Record): void { - console.log("[Kilo New] [Telemetry]", event, properties ?? "") - } -} diff --git a/packages/kilo-vscode/src/services/cli-backend/connection-service.ts b/packages/kilo-vscode/src/services/cli-backend/connection-service.ts index c1144936b5..2d26257708 100644 --- a/packages/kilo-vscode/src/services/cli-backend/connection-service.ts +++ b/packages/kilo-vscode/src/services/cli-backend/connection-service.ts @@ -18,6 +18,7 @@ export class KiloConnectionService { private client: HttpClient | null = null private sseClient: SSEClient | null = null private info: { port: number } | null = null + private config: ServerConfig | null = null // kilocode_change — store for telemetry private state: ConnectionState = "disconnected" private connectPromise: Promise | null = null @@ -77,6 +78,16 @@ export class KiloConnectionService { return this.info } + // kilocode_change start + /** + * Get server config (baseUrl + password). Returns null if not connected. + * Used by TelemetryProxy to POST events to the CLI server. + */ + getServerConfig(): ServerConfig | null { + return this.config + } + // kilocode_change end + /** * Current connection state. */ @@ -175,6 +186,7 @@ export class KiloConnectionService { this.messageSessionIdsByMessageId.clear() this.client = null this.sseClient = null + this.config = null // kilocode_change this.info = null this.state = "disconnected" } @@ -198,6 +210,7 @@ export class KiloConnectionService { password: server.password, } + this.config = config // kilocode_change this.client = new HttpClient(config) this.sseClient = new SSEClient(config) diff --git a/packages/kilo-vscode/src/services/cli-backend/server-manager.ts b/packages/kilo-vscode/src/services/cli-backend/server-manager.ts index 128c687f1f..161a9d6aa3 100644 --- a/packages/kilo-vscode/src/services/cli-backend/server-manager.ts +++ b/packages/kilo-vscode/src/services/cli-backend/server-manager.ts @@ -66,6 +66,12 @@ export class ServerManager { ...process.env, KILO_SERVER_PASSWORD: password, KILO_CLIENT: "vscode", + // kilocode_change start — pass telemetry env vars to CLI + KILO_TELEMETRY_LEVEL: vscode.workspace.getConfiguration("telemetry").get("telemetryLevel", "all"), + KILO_EDITOR_NAME: "Kilo VSCode", + KILO_PLATFORM: "vscode", + KILO_MACHINE_ID: vscode.env.machineId, + // kilocode_change end }, stdio: ["ignore", "pipe", "pipe"], }) diff --git a/packages/kilo-vscode/src/services/telemetry/errors.ts b/packages/kilo-vscode/src/services/telemetry/errors.ts new file mode 100644 index 0000000000..85476aa039 --- /dev/null +++ b/packages/kilo-vscode/src/services/telemetry/errors.ts @@ -0,0 +1,92 @@ +// kilocode_change - new file + +/** + * Generic API provider error for structured error tracking. + */ +export class ApiProviderError extends Error { + constructor( + message: string, + public readonly provider: string, + public readonly modelId: string, + public readonly operation: string, + public readonly errorCode?: number, + ) { + super(message) + this.name = "ApiProviderError" + } +} + +/** + * Type guard for ApiProviderError. + */ +export function isApiProviderError(error: unknown): error is ApiProviderError { + return ( + error instanceof Error && + error.name === "ApiProviderError" && + "provider" in error && + "modelId" in error && + "operation" in error + ) +} + +/** + * Extract telemetry properties from an ApiProviderError. + */ +export function getApiProviderErrorProperties(error: ApiProviderError): Record { + return { + provider: error.provider, + modelId: error.modelId, + operation: error.operation, + ...(error.errorCode !== undefined && { errorCode: error.errorCode }), + } +} + +/** + * Reason why the consecutive mistake limit was reached. + */ +export type ConsecutiveMistakeReason = "no_tools_used" | "tool_repetition" | "unknown" + +/** + * Error for consecutive mistake scenarios (agent keeps failing). + */ +export class ConsecutiveMistakeError extends Error { + constructor( + message: string, + public readonly taskId: string, + public readonly consecutiveMistakeCount: number, + public readonly consecutiveMistakeLimit: number, + public readonly reason: ConsecutiveMistakeReason = "unknown", + public readonly provider?: string, + public readonly modelId?: string, + ) { + super(message) + this.name = "ConsecutiveMistakeError" + } +} + +/** + * Type guard for ConsecutiveMistakeError. + */ +export function isConsecutiveMistakeError(error: unknown): error is ConsecutiveMistakeError { + return ( + error instanceof Error && + error.name === "ConsecutiveMistakeError" && + "taskId" in error && + "consecutiveMistakeCount" in error && + "consecutiveMistakeLimit" in error + ) +} + +/** + * Extract telemetry properties from a ConsecutiveMistakeError. + */ +export function getConsecutiveMistakeErrorProperties(error: ConsecutiveMistakeError): Record { + return { + taskId: error.taskId, + consecutiveMistakeCount: error.consecutiveMistakeCount, + consecutiveMistakeLimit: error.consecutiveMistakeLimit, + reason: error.reason, + ...(error.provider !== undefined && { provider: error.provider }), + ...(error.modelId !== undefined && { modelId: error.modelId }), + } +} diff --git a/packages/kilo-vscode/src/services/telemetry/index.ts b/packages/kilo-vscode/src/services/telemetry/index.ts new file mode 100644 index 0000000000..a74ce1be2f --- /dev/null +++ b/packages/kilo-vscode/src/services/telemetry/index.ts @@ -0,0 +1,13 @@ +// kilocode_change - new file + +export { TelemetryEventName, type TelemetryPropertiesProvider, type TelemetrySetting } from "./types" +export { + ApiProviderError, + isApiProviderError, + getApiProviderErrorProperties, + ConsecutiveMistakeError, + isConsecutiveMistakeError, + getConsecutiveMistakeErrorProperties, + type ConsecutiveMistakeReason, +} from "./errors" +export { TelemetryProxy } from "./telemetry-proxy" diff --git a/packages/kilo-vscode/src/services/telemetry/telemetry-proxy.ts b/packages/kilo-vscode/src/services/telemetry/telemetry-proxy.ts new file mode 100644 index 0000000000..16975109a8 --- /dev/null +++ b/packages/kilo-vscode/src/services/telemetry/telemetry-proxy.ts @@ -0,0 +1,196 @@ +// kilocode_change - new file + +import * as vscode from "vscode" +import { TelemetryEventName, type TelemetryPropertiesProvider, type TelemetrySetting } from "./types" +import { + isApiProviderError, + getApiProviderErrorProperties, + isConsecutiveMistakeError, + getConsecutiveMistakeErrorProperties, +} from "./errors" + +/** + * Singleton proxy that captures telemetry events and forwards them to the CLI + * server via POST /telemetry/capture. The CLI handles PostHog delivery. + */ +export class TelemetryProxy { + private static instance: TelemetryProxy | undefined + + private url: string | undefined + private password: string | undefined + private provider: TelemetryPropertiesProvider | undefined + private setting: TelemetrySetting = "unset" + + private constructor() {} + + static createInstance(): TelemetryProxy { + TelemetryProxy.instance = new TelemetryProxy() + return TelemetryProxy.instance + } + + static getInstance(): TelemetryProxy { + if (!TelemetryProxy.instance) { + throw new Error("TelemetryProxy not initialized — call createInstance() first") + } + return TelemetryProxy.instance + } + + /** + * Return the singleton if it exists, or undefined. Useful for fire-and-forget + * callers (e.g. autocomplete telemetry) that should silently no-op when the + * proxy has not been initialised yet. + */ + static tryGetInstance(): TelemetryProxy | undefined { + return TelemetryProxy.instance + } + + /** + * Configure the CLI server connection. Must be called before capture() will send events. + */ + configure(url: string, password: string) { + this.url = url + this.password = password + } + + /** + * Attach a provider that supplies context properties for every event. + */ + setProvider(provider: TelemetryPropertiesProvider) { + this.provider = provider + } + + /** + * Update the extension-level telemetry preference. + */ + updateTelemetryState(setting: TelemetrySetting) { + this.setting = setting + } + + /** + * Check whether telemetry is enabled based on VS Code level + extension setting. + */ + isTelemetryEnabled(): boolean { + // Extension setting: "disabled" always wins + if (this.setting === "disabled") return false + + // Respect VS Code's global telemetry level (from workspace config) + const level = vscode.workspace.getConfiguration("telemetry").get("telemetryLevel", "all") + if (level !== "all") return false + + return true + } + + /** + * Fire-and-forget capture. Enriches with provider properties, then POSTs to CLI. + */ + capture(event: TelemetryEventName, properties?: Record) { + if (!this.isTelemetryEnabled()) return + if (!this.url || !this.password) return + + const merged = { + ...this.provider?.getTelemetryProperties(), + ...properties, + } + + const payload = JSON.stringify({ event, properties: merged }) + const auth = `Basic ${Buffer.from(`kilo:${this.password}`).toString("base64")}` + + fetch(`${this.url}/telemetry/capture`, { + method: "POST", + headers: { + Authorization: auth, + "Content-Type": "application/json", + }, + body: payload, + }).catch(() => {}) + } + + /** + * Capture an exception. Extracts structured properties from known error types. + */ + captureException(error: Error, extra?: Record) { + const properties: Record = { + error: error.message, + errorName: error.name, + ...extra, + } + + if (isApiProviderError(error)) { + Object.assign(properties, getApiProviderErrorProperties(error)) + } + + if (isConsecutiveMistakeError(error)) { + Object.assign(properties, getConsecutiveMistakeErrorProperties(error)) + this.capture(TelemetryEventName.CONSECUTIVE_MISTAKE_ERROR, properties) + return + } + + // Generic exception — use the error name as the event or a fallback + this.capture(TelemetryEventName.SCHEMA_VALIDATION_ERROR, properties) + } + + // ============================================ + // Typed capture helpers + // ============================================ + + captureTaskCreated(properties: Record) { + this.capture(TelemetryEventName.TASK_CREATED, properties) + } + + captureTaskReopened(properties: Record) { + this.capture(TelemetryEventName.TASK_RESTARTED, properties) + } + + captureTaskCompleted(properties: Record) { + this.capture(TelemetryEventName.TASK_COMPLETED, properties) + } + + captureConversationMessage(properties: Record) { + this.capture(TelemetryEventName.TASK_CONVERSATION_MESSAGE, properties) + } + + captureLlmCompletion(properties: Record) { + this.capture(TelemetryEventName.LLM_COMPLETION, properties) + } + + captureToolUsed(properties: Record) { + this.capture(TelemetryEventName.TOOL_USED, properties) + } + + captureModeSwitched(properties: Record) { + this.capture(TelemetryEventName.MODE_SWITCH, properties) + } + + captureCheckpointCreated(properties: Record) { + this.capture(TelemetryEventName.CHECKPOINT_CREATED, properties) + } + + captureCheckpointRestored(properties: Record) { + this.capture(TelemetryEventName.CHECKPOINT_RESTORED, properties) + } + + captureCheckpointDiffed(properties: Record) { + this.capture(TelemetryEventName.CHECKPOINT_DIFFED, properties) + } + + captureTabShown(properties: Record) { + this.capture(TelemetryEventName.TAB_SHOWN, properties) + } + + captureTitleButtonClicked(properties: Record) { + this.capture(TelemetryEventName.TITLE_BUTTON_CLICKED, properties) + } + + capturePromptEnhanced(properties: Record) { + this.capture(TelemetryEventName.PROMPT_ENHANCED, properties) + } + + captureCodeActionUsed(properties: Record) { + this.capture(TelemetryEventName.CODE_ACTION_USED, properties) + } + + /** + * No-op — the CLI server handles PostHog shutdown. + */ + shutdown() {} +} diff --git a/packages/kilo-vscode/src/services/telemetry/types.ts b/packages/kilo-vscode/src/services/telemetry/types.ts new file mode 100644 index 0000000000..7e172f62f3 --- /dev/null +++ b/packages/kilo-vscode/src/services/telemetry/types.ts @@ -0,0 +1,97 @@ +// kilocode_change - new file + +/** + * Telemetry event names for the Kilo VS Code extension. + * These are forwarded to the CLI server via POST /telemetry/capture. + */ +export enum TelemetryEventName { + // Task Lifecycle + TASK_CREATED = "Task Created", + TASK_RESTARTED = "Task Reopened", + TASK_COMPLETED = "Task Completed", + TASK_CONVERSATION_MESSAGE = "Conversation Message", + + // LLM & AI + LLM_COMPLETION = "LLM Completion", + CONTEXT_CONDENSED = "Context Condensed", + SLIDING_WINDOW_TRUNCATION = "Sliding Window Truncation", + + // Tools & Modes + TOOL_USED = "Tool Used", + MODE_SWITCH = "Mode Switched", + MODE_SETTINGS_CHANGED = "Mode Setting Changed", + CUSTOM_MODE_CREATED = "Custom Mode Created", + CODE_ACTION_USED = "Code Action Used", + + // Checkpoints + CHECKPOINT_CREATED = "Checkpoint Created", + CHECKPOINT_RESTORED = "Checkpoint Restored", + CHECKPOINT_DIFFED = "Checkpoint Diffed", + + // UI Interactions + TAB_SHOWN = "Tab Shown", + TITLE_BUTTON_CLICKED = "Title Button Clicked", + PROMPT_ENHANCED = "Prompt Enhanced", + + // Marketplace + MARKETPLACE_ITEM_INSTALLED = "Marketplace Item Installed", + MARKETPLACE_ITEM_REMOVED = "Marketplace Item Removed", + + // Account & Auth + ACCOUNT_CONNECT_CLICKED = "Account Connect Clicked", + ACCOUNT_CONNECT_SUCCESS = "Account Connect Success", + ACCOUNT_LOGOUT_CLICKED = "Account Logout Clicked", + ACCOUNT_LOGOUT_SUCCESS = "Account Logout Success", + + // Error Tracking + SCHEMA_VALIDATION_ERROR = "Schema Validation Error", + DIFF_APPLICATION_ERROR = "Diff Application Error", + SHELL_INTEGRATION_ERROR = "Shell Integration Error", + CONSECUTIVE_MISTAKE_ERROR = "Consecutive Mistake Error", + + // Autocomplete + AUTOCOMPLETE_SUGGESTION_REQUESTED = "Autocomplete Suggestion Requested", + AUTOCOMPLETE_LLM_REQUEST_COMPLETED = "Autocomplete LLM Request Completed", + AUTOCOMPLETE_LLM_REQUEST_FAILED = "Autocomplete LLM Request Failed", + AUTOCOMPLETE_LLM_SUGGESTION_RETURNED = "Autocomplete LLM Suggestion Returned", + AUTOCOMPLETE_SUGGESTION_CACHE_HIT = "Autocomplete Suggestion Cache Hit", + AUTOCOMPLETE_ACCEPT_SUGGESTION = "Autocomplete Accept Suggestion", + AUTOCOMPLETE_SUGGESTION_FILTERED = "Autocomplete Suggestion Filtered", + AUTOCOMPLETE_UNIQUE_SUGGESTION_SHOWN = "Autocomplete Unique Suggestion Shown", + + // Inline Assist + INLINE_ASSIST_AUTO_TASK = "Inline Assist Auto Task", + + // Kilo-specific + COMMIT_MSG_GENERATED = "Commit Message Generated", + AGENT_MANAGER_OPENED = "Agent Manager Opened", + AGENT_MANAGER_SESSION_STARTED = "Agent Manager Session Started", + AGENT_MANAGER_SESSION_COMPLETED = "Agent Manager Session Completed", + AGENT_MANAGER_SESSION_STOPPED = "Agent Manager Session Stopped", + AGENT_MANAGER_SESSION_ERROR = "Agent Manager Session Error", + AGENT_MANAGER_LOGIN_ISSUE = "Agent Manager Login Issue", + AUTO_PURGE_STARTED = "Auto Purge Started", + AUTO_PURGE_COMPLETED = "Auto Purge Completed", + AUTO_PURGE_FAILED = "Auto Purge Failed", + MANUAL_PURGE_TRIGGERED = "Manual Purge Triggered", + WEBVIEW_MEMORY_USAGE = "Webview Memory Usage", + MEMORY_WARNING_SHOWN = "Memory Warning Shown", + ASK_APPROVAL = "Ask Approval", + NOTIFICATION_CLICKED = "Notification Clicked", + SUGGESTION_BUTTON_CLICKED = "Suggestion Button Clicked", + FREE_MODELS_LINK_CLICKED = "Free Models Link Clicked", + CREATE_ORGANIZATION_LINK_CLICKED = "Create Organization Link Clicked", + GHOST_SERVICE_DISABLED = "Ghost Service Disabled", +} + +/** + * Provider that supplies context properties to be merged into every telemetry event. + */ +export interface TelemetryPropertiesProvider { + getTelemetryProperties(): Record +} + +/** + * Extension-level telemetry preference. + */ +export type TelemetrySetting = "unset" | "enabled" | "disabled" diff --git a/packages/kilo-vscode/webview-ui/src/context/vscode.tsx b/packages/kilo-vscode/webview-ui/src/context/vscode.tsx index c7c5104ed1..192a87244c 100644 --- a/packages/kilo-vscode/webview-ui/src/context/vscode.tsx +++ b/packages/kilo-vscode/webview-ui/src/context/vscode.tsx @@ -23,6 +23,8 @@ function getVSCodeAPI(): VSCodeAPI { setState: () => {}, } } + // kilocode_change — expose on globalThis so non-context code (e.g. telemetry util) can access it + ;(globalThis as Record).__vscodeApi = vscodeApi } return vscodeApi } diff --git a/packages/kilo-vscode/webview-ui/src/types/messages.ts b/packages/kilo-vscode/webview-ui/src/types/messages.ts index 1642782cac..76d1d679c2 100644 --- a/packages/kilo-vscode/webview-ui/src/types/messages.ts +++ b/packages/kilo-vscode/webview-ui/src/types/messages.ts @@ -738,6 +738,14 @@ export interface CreateWorktreeSessionRequest { agent?: string } +// kilocode_change start +export interface TelemetryRequest { + type: "telemetry" + event: string + properties?: Record +} +// kilocode_change end + export type WebviewMessage = | SendMessageRequest | AbortRequest @@ -773,6 +781,7 @@ export type WebviewMessage = | RequestNotificationSettingsMessage | ResetAllSettingsRequest | CreateWorktreeSessionRequest + | TelemetryRequest // kilocode_change // ============================================ // VS Code API type diff --git a/packages/kilo-vscode/webview-ui/src/utils/telemetry.ts b/packages/kilo-vscode/webview-ui/src/utils/telemetry.ts new file mode 100644 index 0000000000..3fc6ad4d69 --- /dev/null +++ b/packages/kilo-vscode/webview-ui/src/utils/telemetry.ts @@ -0,0 +1,29 @@ +// kilocode_change - new file + +/** + * Thin helper for the webview to send telemetry events to the extension host. + * The extension host's KiloProvider forwards these to TelemetryProxy → CLI server. + * + * Uses window.postMessage-style access so it works without the Solid.js context. + * The VS Code webview API is stored globally by the VSCodeProvider context on first mount, + * but we access it via the global `acquireVsCodeApi` cache stored on window by vscode.tsx. + */ + +/** + * Fire-and-forget telemetry capture from the webview. + * Posts a message to the extension host which forwards it to the CLI server. + */ +export function captureTelemetryEvent(event: string, properties?: Record) { + try { + // The VS Code webview API is available globally after acquireVsCodeApi() is called. + // We access __vscodeApi which is set by the VSCodeProvider in vscode.tsx. + // Fallback: if not available yet, silently drop — telemetry is fire-and-forget. + const api = (globalThis as Record).__vscodeApi as + | { postMessage(msg: unknown): void } + | undefined + if (!api) return + api.postMessage({ type: "telemetry", event, properties }) + } catch { + // Never crash on telemetry failures + } +} diff --git a/packages/opencode/src/server/routes/telemetry.ts b/packages/opencode/src/server/routes/telemetry.ts new file mode 100644 index 0000000000..b0509598be --- /dev/null +++ b/packages/opencode/src/server/routes/telemetry.ts @@ -0,0 +1,48 @@ +// kilocode_change - new file +import { Hono } from "hono" +import { describeRoute, validator, resolver } from "hono-openapi" +import z from "zod" +import { Telemetry } from "@kilocode/kilo-telemetry" +import { lazy } from "../../util/lazy" +import { errors } from "../error" + +export const TelemetryRoutes = lazy(() => + new Hono().post( + "/capture", + describeRoute({ + summary: "Capture telemetry event", + description: "Forward a telemetry event to PostHog via kilo-telemetry.", + operationId: "telemetry.capture", + responses: { + 200: { + description: "Event captured", + content: { + "application/json": { + schema: resolver(z.boolean()), + }, + }, + }, + ...errors(400), + }, + }), + validator( + "json", + z.object({ + event: z.string().meta({ description: "Event name" }), + properties: z + .record(z.string(), z.any()) + .optional() + .meta({ description: "Event properties" }), + }), + ), + async (c) => { + const body = c.req.valid("json") + try { + Telemetry.track(body.event as any, body.properties) + } catch { + // fire-and-forget: swallow errors + } + return c.json(true) + }, + ), +) diff --git a/packages/opencode/src/server/server.ts b/packages/opencode/src/server/server.ts index 34e8b4a951..8bf922dfca 100644 --- a/packages/opencode/src/server/server.ts +++ b/packages/opencode/src/server/server.ts @@ -28,6 +28,7 @@ import { McpRoutes } from "./routes/mcp" import { FileRoutes } from "./routes/file" import { ConfigRoutes } from "./routes/config" import { ExperimentalRoutes } from "./routes/experimental" +import { TelemetryRoutes } from "./routes/telemetry" // kilocode_change import { ProviderRoutes } from "./routes/provider" import { createKiloRoutes } from "@kilocode/kilo-gateway" // kilocode_change import { lazy } from "../util/lazy" @@ -233,6 +234,7 @@ export namespace Server { .route("/permission", PermissionRoutes()) .route("/question", QuestionRoutes()) .route("/provider", ProviderRoutes()) + .route("/telemetry", TelemetryRoutes()) // kilocode_change // kilocode_change start - Kilo Gateway routes .route( "/kilo",