mirror of
https://github.com/Kilo-Org/kilocode.git
synced 2026-08-28 19:11:03 +08:00
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:
@@ -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)
|
||||
|
||||
|
||||
@@ -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()
|
||||
|
||||
|
||||
@@ -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
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
})
|
||||
|
||||
|
||||
+4
-22
@@ -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)
|
||||
},
|
||||
),
|
||||
)
|
||||
@@ -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",
|
||||
|
||||
Reference in New Issue
Block a user