feat: implement proxy-based telemetry for VS Code extension

- Add POST /telemetry/capture endpoint to CLI server
- Add TelemetryProxy singleton in extension (forwards to CLI)
- Add webview telemetry helper (postMessage → extension → CLI)
- Pass telemetry env vars (level, editor_name, platform, machineId) to CLI
- Wire up KiloProvider as TelemetryPropertiesProvider
- Replace TelemetryStub with real TelemetryProxy in autocomplete
- 53 telemetry events covering tasks, LLM, tools, UI, autocomplete, etc.
This commit is contained in:
Mark IJbema
2026-02-18 12:09:22 +01:00
parent 36b9c26f35
commit 623d37f36f
18 changed files with 596 additions and 50 deletions
+8
View File
@@ -14,6 +14,14 @@ export namespace Identity {
export async function getMachineId(): Promise<string> {
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)
+15 -2
View File
@@ -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()
+37 -2
View File
@@ -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<string, unknown> {
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
}
})
}
+21 -1
View File
@@ -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)
@@ -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,
})
@@ -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<string, unknown>,
): void {
const propsWithType = {
private captureEvent(event: TelemetryEventName, properties?: Record<string, unknown>): void {
const props = {
...properties,
autocompleteType: this.autocompleteType,
}
this.telemetryClient.captureEvent(event, propsWithType)
TelemetryProxy.tryGetInstance()?.capture(event, props)
}
/**
@@ -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<string, unknown>): void
}
export class TelemetryStub implements ITelemetryClient {
captureEvent(event: TelemetryEventName, properties?: Record<string, unknown>): void {
console.log("[Kilo New] [Telemetry]", event, properties ?? "")
}
}
@@ -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<void> | 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)
@@ -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<string>("telemetryLevel", "all"),
KILO_EDITOR_NAME: "Kilo VSCode",
KILO_PLATFORM: "vscode",
KILO_MACHINE_ID: vscode.env.machineId,
// kilocode_change end
},
stdio: ["ignore", "pipe", "pipe"],
})
@@ -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<string, unknown> {
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<string, unknown> {
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 }),
}
}
@@ -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"
@@ -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<string>("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<string, unknown>) {
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<string, unknown>) {
const properties: Record<string, unknown> = {
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<string, unknown>) {
this.capture(TelemetryEventName.TASK_CREATED, properties)
}
captureTaskReopened(properties: Record<string, unknown>) {
this.capture(TelemetryEventName.TASK_RESTARTED, properties)
}
captureTaskCompleted(properties: Record<string, unknown>) {
this.capture(TelemetryEventName.TASK_COMPLETED, properties)
}
captureConversationMessage(properties: Record<string, unknown>) {
this.capture(TelemetryEventName.TASK_CONVERSATION_MESSAGE, properties)
}
captureLlmCompletion(properties: Record<string, unknown>) {
this.capture(TelemetryEventName.LLM_COMPLETION, properties)
}
captureToolUsed(properties: Record<string, unknown>) {
this.capture(TelemetryEventName.TOOL_USED, properties)
}
captureModeSwitched(properties: Record<string, unknown>) {
this.capture(TelemetryEventName.MODE_SWITCH, properties)
}
captureCheckpointCreated(properties: Record<string, unknown>) {
this.capture(TelemetryEventName.CHECKPOINT_CREATED, properties)
}
captureCheckpointRestored(properties: Record<string, unknown>) {
this.capture(TelemetryEventName.CHECKPOINT_RESTORED, properties)
}
captureCheckpointDiffed(properties: Record<string, unknown>) {
this.capture(TelemetryEventName.CHECKPOINT_DIFFED, properties)
}
captureTabShown(properties: Record<string, unknown>) {
this.capture(TelemetryEventName.TAB_SHOWN, properties)
}
captureTitleButtonClicked(properties: Record<string, unknown>) {
this.capture(TelemetryEventName.TITLE_BUTTON_CLICKED, properties)
}
capturePromptEnhanced(properties: Record<string, unknown>) {
this.capture(TelemetryEventName.PROMPT_ENHANCED, properties)
}
captureCodeActionUsed(properties: Record<string, unknown>) {
this.capture(TelemetryEventName.CODE_ACTION_USED, properties)
}
/**
* No-op — the CLI server handles PostHog shutdown.
*/
shutdown() {}
}
@@ -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<string, unknown>
}
/**
* Extension-level telemetry preference.
*/
export type TelemetrySetting = "unset" | "enabled" | "disabled"
@@ -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<string, unknown>).__vscodeApi = vscodeApi
}
return vscodeApi
}
@@ -738,6 +738,14 @@ export interface CreateWorktreeSessionRequest {
agent?: string
}
// kilocode_change start
export interface TelemetryRequest {
type: "telemetry"
event: string
properties?: Record<string, unknown>
}
// 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
@@ -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<string, unknown>) {
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<string, unknown>).__vscodeApi as
| { postMessage(msg: unknown): void }
| undefined
if (!api) return
api.postMessage({ type: "telemetry", event, properties })
} catch {
// Never crash on telemetry failures
}
}
@@ -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)
},
),
)
+2
View File
@@ -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",