From e762c48ec81b03110593dbc12a54ac590bf9a895 Mon Sep 17 00:00:00 2001 From: kirillk Date: Fri, 29 May 2026 13:59:24 -0400 Subject: [PATCH 01/16] feat(jetbrains): add telemetry reporting --- .../backend/app/KiloBackendAppService.kt | 5 +- .../backend/cli/KiloBackendCliManager.kt | 62 ++++++++++++- .../ai/kilocode/backend/dev/KiloDevMode.kt | 5 ++ .../kilocode/backend/rpc/KiloAppRpcApiImpl.kt | 6 ++ .../backend/telemetry/KiloBackendTelemetry.kt | 83 +++++++++++++++++ .../telemetry/KiloCliStartupTelemetry.kt | 88 +++++++++++++++++++ .../cli/KiloBackendCliManagerEnvTest.kt | 12 +++ .../telemetry/KiloBackendTelemetryTest.kt | 73 +++++++++++++++ .../kilocode/client/KiloToolWindowFactory.kt | 4 + .../kilocode/client/actions/HistoryAction.kt | 2 + .../client/actions/KiloSettingsAction.kt | 2 + .../client/actions/NewSessionAction.kt | 2 + .../client/actions/ReinstallKiloAction.kt | 2 + .../client/actions/RestartKiloAction.kt | 2 + .../client/actions/ShowProfileAction.kt | 2 + .../client/migration/KiloMigrationService.kt | 35 ++++++++ .../ai/kilocode/client/session/SessionUi.kt | 9 +- .../session/controller/SessionController.kt | 60 ++++++++++++- .../client/settings/profile/ProfileUi.kt | 17 +++- .../client/telemetry/KiloTelemetryService.kt | 49 +++++++++++ .../kilocode/client/testing/FakeAppRpcApi.kt | 7 ++ .../kotlin/ai/kilocode/rpc/KiloAppRpcApi.kt | 4 + .../ai/kilocode/rpc/dto/TelemetryDto.kt | 9 ++ 23 files changed, 533 insertions(+), 7 deletions(-) create mode 100644 packages/kilo-jetbrains/backend/src/main/kotlin/ai/kilocode/backend/dev/KiloDevMode.kt create mode 100644 packages/kilo-jetbrains/backend/src/main/kotlin/ai/kilocode/backend/telemetry/KiloBackendTelemetry.kt create mode 100644 packages/kilo-jetbrains/backend/src/main/kotlin/ai/kilocode/backend/telemetry/KiloCliStartupTelemetry.kt create mode 100644 packages/kilo-jetbrains/backend/src/test/kotlin/ai/kilocode/backend/telemetry/KiloBackendTelemetryTest.kt create mode 100644 packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/telemetry/KiloTelemetryService.kt create mode 100644 packages/kilo-jetbrains/shared/src/main/kotlin/ai/kilocode/rpc/dto/TelemetryDto.kt diff --git a/packages/kilo-jetbrains/backend/src/main/kotlin/ai/kilocode/backend/app/KiloBackendAppService.kt b/packages/kilo-jetbrains/backend/src/main/kotlin/ai/kilocode/backend/app/KiloBackendAppService.kt index 468760d8321..bed5b95e0f8 100644 --- a/packages/kilo-jetbrains/backend/src/main/kotlin/ai/kilocode/backend/app/KiloBackendAppService.kt +++ b/packages/kilo-jetbrains/backend/src/main/kotlin/ai/kilocode/backend/app/KiloBackendAppService.kt @@ -4,6 +4,7 @@ import ai.kilocode.backend.cli.CliServer import ai.kilocode.backend.cli.KiloBackendCliManager import ai.kilocode.backend.migration.KiloBackendLegacyMigrationStoreService import ai.kilocode.backend.migration.LegacyMigrationDetection +import ai.kilocode.backend.telemetry.KiloBackendTelemetry import ai.kilocode.log.KiloLog import ai.kilocode.backend.workspace.KiloBackendWorkspaceManager import ai.kilocode.jetbrains.api.client.DefaultApi @@ -21,6 +22,7 @@ import ai.kilocode.rpc.dto.DeviceAuthDto import ai.kilocode.rpc.dto.HealthDto import com.intellij.openapi.Disposable import com.intellij.openapi.components.Service +import com.intellij.openapi.components.service import kotlinx.coroutines.CancellationException import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Job @@ -119,7 +121,6 @@ class KiloBackendAppService private constructor( val chat = KiloBackendChatManager(cs, log) val models = KiloBackendModelStateManager(log) val workspaces = KiloBackendWorkspaceManager(cs, sessions, log) - @Volatile var profile: KiloProfile200Response? = null private set @@ -343,6 +344,8 @@ class KiloBackendAppService private constructor( sessions.start(connection.api!!, connection.apiClient!!, connection.port, connection.events) chat.start(connection.apiClient!!, connection.port, connection.events) workspaces.start(connection.api!!, connection.apiClient!!, connection.port, connection.events) + service().setEnabled(connection.apiClient, connection.port, true) + service().capture(connection.apiClient, connection.port, "JetBrains Backend Connected", mapOf("portKnown" to "true")) setAppReady( AppData( profile = prof, diff --git a/packages/kilo-jetbrains/backend/src/main/kotlin/ai/kilocode/backend/cli/KiloBackendCliManager.kt b/packages/kilo-jetbrains/backend/src/main/kotlin/ai/kilocode/backend/cli/KiloBackendCliManager.kt index ffeb58fed7d..56647cbb3d3 100644 --- a/packages/kilo-jetbrains/backend/src/main/kotlin/ai/kilocode/backend/cli/KiloBackendCliManager.kt +++ b/packages/kilo-jetbrains/backend/src/main/kotlin/ai/kilocode/backend/cli/KiloBackendCliManager.kt @@ -1,6 +1,9 @@ package ai.kilocode.backend.cli import ai.kilocode.log.KiloLog +import ai.kilocode.backend.telemetry.CliStartupTelemetry +import ai.kilocode.backend.telemetry.KiloCliStartupTelemetry +import ai.kilocode.backend.dev.KiloDevMode import com.intellij.ide.plugins.PluginManagerCore import com.intellij.openapi.application.ApplicationInfo import com.intellij.openapi.application.PathManager @@ -30,6 +33,7 @@ import java.util.concurrent.TimeUnit */ class KiloBackendCliManager( private val log: KiloLog = KiloLog.create(KiloBackendCliManager::class.java), + private val startup: CliStartupTelemetry = KiloCliStartupTelemetry(), ) : CliServer { companion object { @@ -130,6 +134,7 @@ class KiloBackendCliManager( put("KILO_ENABLE_QUESTION_TOOL", "true") put("KILO_PLATFORM", "jetbrains") put("KILO_APP_NAME", "kilo-code") + put("KILO_TELEMETRY_LEVEL", if (KiloDevMode.enabled()) "off" else "all") put("KILO_DISABLE_CLAUDE_CODE", "true") put("KILOCODE_FEATURE", "jetbrains-plugin") putIfAbsent("KILO_CONFIG_CONTENT", DEFAULT_CONFIG) @@ -178,12 +183,26 @@ class KiloBackendCliManager( log.info("Starting CLI: ${cmd.joinToString(" ")}") log.info("CLI env: KILO_CLIENT=jetbrains KILO_PLATFORM=jetbrains KILO_APP_NAME=kilo-code") - val proc = builder.start() + val proc = try { + builder.start() + } catch (e: Exception) { + reportStartupFailure( + env = env, + cmd = cmd, + stdout = "", + stderr = "", + code = null, + details = e.message, + error = e, + ) + throw e + } log.info("CLI process started (pid=${proc.pid()})") process = proc install(proc) val stderr = StringBuilder() + val stdout = StringBuilder() Thread({ BufferedReader(InputStreamReader(proc.errorStream)).use { reader -> @@ -197,6 +216,7 @@ class KiloBackendCliManager( BufferedReader(InputStreamReader(proc.inputStream)).use { reader -> for (line in reader.lineSequence()) { log.info("CLI stdout: $line") + synchronized(stdout) { stdout.appendLine(line) } val match = PORT_REGEX.find(line) if (match != null) { val p = match.groupValues[1].toInt() @@ -210,14 +230,52 @@ class KiloBackendCliManager( val code = proc.waitFor() val details = synchronized(stderr) { stderr.toString().trim() } + val out = synchronized(stdout) { stdout.toString().trim() } process = null uninstall() + reportStartupFailure( + env = env, + cmd = cmd, + stdout = out, + stderr = details, + code = code, + details = details, + error = null, + ) CliServer.State.Error( message = "CLI process exited with code $code before announcing a port", details = details.ifEmpty { null }, ) } + private suspend fun reportStartupFailure( + env: Map, + cmd: List, + stdout: String, + stderr: String, + code: Int?, + details: String?, + error: Exception?, + ) { + startup.report(buildMap { + if (code != null) put("exitCode", code.toString()) + put("stdout", stdout) + put("stderr", stderr) + put("details", details.orEmpty()) + put("command", cmd.joinToString(" ")) + put("platform", platform()) + put("arch", CpuArch.CURRENT.name) + env["KILO_APP_VERSION"]?.let { put("pluginVersion", it) } + env["KILO_APP_VERSION"]?.let { put("appVersion", it) } + env["KILO_EDITOR_NAME"]?.let { put("editorName", it) } + env["KILO_MACHINE_ID"]?.let { put("machineId", it) } + error?.let { + put("errorClass", it::class.java.name) + put("message", it.message.orEmpty()) + } + }) + } + override fun dispose() { val proc = process ?: return process = null @@ -289,7 +347,7 @@ class KiloBackendCliManager( runCatching { val version = PluginManagerCore - .getPlugin(PluginId.getId("ai.kilocode"))?.version + .getPlugin(PluginId.getId("ai.kilocode.jetbrains"))?.version if (version != null) put("KILO_APP_VERSION", version) }.onFailure { log.info("Could not read plugin version: ${it.message}") } diff --git a/packages/kilo-jetbrains/backend/src/main/kotlin/ai/kilocode/backend/dev/KiloDevMode.kt b/packages/kilo-jetbrains/backend/src/main/kotlin/ai/kilocode/backend/dev/KiloDevMode.kt new file mode 100644 index 00000000000..80f042bfc05 --- /dev/null +++ b/packages/kilo-jetbrains/backend/src/main/kotlin/ai/kilocode/backend/dev/KiloDevMode.kt @@ -0,0 +1,5 @@ +package ai.kilocode.backend.dev + +object KiloDevMode { + fun enabled(): Boolean = System.getProperty("idea.plugin.in.sandbox.mode", "false").toBoolean() +} diff --git a/packages/kilo-jetbrains/backend/src/main/kotlin/ai/kilocode/backend/rpc/KiloAppRpcApiImpl.kt b/packages/kilo-jetbrains/backend/src/main/kotlin/ai/kilocode/backend/rpc/KiloAppRpcApiImpl.kt index d2971645924..668db20b290 100644 --- a/packages/kilo-jetbrains/backend/src/main/kotlin/ai/kilocode/backend/rpc/KiloAppRpcApiImpl.kt +++ b/packages/kilo-jetbrains/backend/src/main/kotlin/ai/kilocode/backend/rpc/KiloAppRpcApiImpl.kt @@ -4,6 +4,7 @@ package ai.kilocode.backend.rpc import ai.kilocode.backend.app.KiloAppState import ai.kilocode.backend.app.KiloBackendAppService +import ai.kilocode.backend.telemetry.KiloBackendTelemetry import ai.kilocode.backend.app.ConfigWarning import ai.kilocode.backend.app.LoadError import ai.kilocode.backend.app.LoadProgress @@ -30,6 +31,7 @@ import ai.kilocode.rpc.dto.ProfileBalanceDto import ai.kilocode.rpc.dto.ProfileDto import ai.kilocode.rpc.dto.ProfileOrganizationDto import ai.kilocode.rpc.dto.ProfileStatusDto +import ai.kilocode.rpc.dto.TelemetryCaptureDto import com.intellij.openapi.components.service import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.distinctUntilChanged @@ -94,6 +96,10 @@ class KiloAppRpcApiImpl : KiloAppRpcApi { override suspend fun setOrganization(organizationId: String?): ProfileDto? = app.setOrganization(organizationId)?.let(::profileDto) + override suspend fun captureTelemetry(capture: TelemetryCaptureDto) { + service().capture(app.http, app.port, capture.event, capture.properties) + } + private fun dto(state: KiloAppState): KiloAppStateDto = appStateDto(state) } diff --git a/packages/kilo-jetbrains/backend/src/main/kotlin/ai/kilocode/backend/telemetry/KiloBackendTelemetry.kt b/packages/kilo-jetbrains/backend/src/main/kotlin/ai/kilocode/backend/telemetry/KiloBackendTelemetry.kt new file mode 100644 index 00000000000..88648b4a6a3 --- /dev/null +++ b/packages/kilo-jetbrains/backend/src/main/kotlin/ai/kilocode/backend/telemetry/KiloBackendTelemetry.kt @@ -0,0 +1,83 @@ +package ai.kilocode.backend.telemetry + +import ai.kilocode.backend.dev.KiloDevMode +import ai.kilocode.log.KiloLog +import com.intellij.ide.plugins.PluginManagerCore +import com.intellij.openapi.application.ApplicationInfo +import com.intellij.openapi.components.Service +import com.intellij.openapi.extensions.PluginId +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.withContext +import kotlinx.serialization.json.JsonObject +import kotlinx.serialization.json.JsonPrimitive +import okhttp3.MediaType.Companion.toMediaType +import okhttp3.OkHttpClient +import okhttp3.Request +import okhttp3.RequestBody.Companion.toRequestBody + +@Service(Service.Level.APP) +class KiloBackendTelemetry( + private val log: KiloLog = KiloLog.create(KiloBackendTelemetry::class.java), +) { + suspend fun capture(http: OkHttpClient?, port: Int, event: String, properties: Map) { + val body = payload(event, properties) + if (KiloDevMode.enabled()) { + log.debug { "telemetry capture dev-mode: $body" } + return + } + post(http, port, "telemetry/capture", body) + } + + suspend fun setEnabled(http: OkHttpClient?, port: Int, enabled: Boolean) { + val body = JsonObject(mapOf("enabled" to JsonPrimitive(enabled))).toString() + if (KiloDevMode.enabled()) { + log.debug { "telemetry setEnabled dev-mode: $body" } + return + } + post(http, port, "telemetry/setEnabled", body) + } + + private suspend fun post(http: OkHttpClient?, port: Int, path: String, body: String) { + if (http == null || port <= 0) return + withContext(Dispatchers.IO) { + try { + val req = Request.Builder() + .url("http://127.0.0.1:$port/$path") + .header("Accept", "application/json") + .post(body.toRequestBody("application/json".toMediaType())) + .build() + http.newCall(req).execute().use { res -> + if (!res.isSuccessful) log.warn("telemetry $path failed: HTTP ${res.code}") + } + } catch (e: Exception) { + log.warn("telemetry $path failed: ${e.message}", e) + } + } + } + + private fun payload(event: String, properties: Map): String = JsonObject( + mapOf( + "event" to JsonPrimitive(event), + "properties" to JsonObject(base() + properties.mapValues { JsonPrimitive(it.value) }), + ), + ).toString() + + private fun base(): Map = buildMap { + put("appName", JsonPrimitive("kilo-code")) + put("platform", JsonPrimitive("jetbrains")) + put("client", JsonPrimitive("jetbrains")) + put("feature", JsonPrimitive("jetbrains-plugin")) + runCatching { + val info = ApplicationInfo.getInstance() + put("editorName", JsonPrimitive(info.fullApplicationName)) + put("jetbrainsBuild", JsonPrimitive(info.build.asString())) + }.onFailure { log.info("Could not read ApplicationInfo for telemetry: ${it.message}") } + runCatching { + val version = PluginManagerCore.getPlugin(PluginId.getId("ai.kilocode.jetbrains"))?.version + if (version != null) { + put("pluginVersion", JsonPrimitive(version)) + put("appVersion", JsonPrimitive(version)) + } + }.onFailure { log.info("Could not read plugin version for telemetry: ${it.message}") } + } +} diff --git a/packages/kilo-jetbrains/backend/src/main/kotlin/ai/kilocode/backend/telemetry/KiloCliStartupTelemetry.kt b/packages/kilo-jetbrains/backend/src/main/kotlin/ai/kilocode/backend/telemetry/KiloCliStartupTelemetry.kt new file mode 100644 index 00000000000..459debbb05d --- /dev/null +++ b/packages/kilo-jetbrains/backend/src/main/kotlin/ai/kilocode/backend/telemetry/KiloCliStartupTelemetry.kt @@ -0,0 +1,88 @@ +package ai.kilocode.backend.telemetry + +import ai.kilocode.backend.dev.KiloDevMode +import ai.kilocode.log.KiloLog +import com.intellij.ide.plugins.PluginManagerCore +import com.intellij.openapi.application.ApplicationInfo +import com.intellij.openapi.extensions.PluginId +import com.intellij.openapi.util.SystemInfo +import com.intellij.util.system.CpuArch +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.withContext +import kotlinx.serialization.json.JsonObject +import kotlinx.serialization.json.JsonPrimitive +import okhttp3.MediaType.Companion.toMediaType +import okhttp3.OkHttpClient +import okhttp3.Request +import okhttp3.RequestBody.Companion.toRequestBody +import java.util.concurrent.TimeUnit + +interface CliStartupTelemetry { + suspend fun report(properties: Map) +} + +class KiloCliStartupTelemetry( + private val http: OkHttpClient = OkHttpClient.Builder() + .callTimeout(5, TimeUnit.SECONDS) + .build(), + private val url: String = "https://us.i.posthog.com/capture/", + private val log: KiloLog = KiloLog.create(KiloCliStartupTelemetry::class.java), +) : CliStartupTelemetry { + override suspend fun report(properties: Map) { + withContext(Dispatchers.IO) { + try { + val props = base() + properties + val body = JsonObject( + mapOf( + "api_key" to JsonPrimitive("phc_GK2Pxl0HPj5ZPfwhLRjXrtdz8eD7e9MKnXiFrOqnB6z"), + "event" to JsonPrimitive("JetBrains CLI Startup Failed"), + "distinct_id" to JsonPrimitive(props["machineId"] ?: "jetbrains-unknown"), + "properties" to JsonObject(props.mapValues { JsonPrimitive(it.value) }), + ), + ).toString() + if (KiloDevMode.enabled()) { + log.debug { "startup telemetry dev-mode: $body" } + return@withContext + } + val req = Request.Builder() + .url(url) + .header("Accept", "application/json") + .post(body.toRequestBody("application/json".toMediaType())) + .build() + http.newCall(req).execute().use { res -> + if (!res.isSuccessful) log.warn("startup telemetry failed: HTTP ${res.code}") + } + } catch (e: Exception) { + log.warn("startup telemetry failed: ${e.message}", e) + } + } + } + + private fun base(): Map = buildMap { + put("appName", "kilo-code") + put("platform", "jetbrains") + put("client", "jetbrains") + put("feature", "jetbrains-plugin") + put("os", os()) + put("arch", CpuArch.CURRENT.name) + runCatching { + val info = ApplicationInfo.getInstance() + put("editorName", info.fullApplicationName) + put("jetbrainsBuild", info.build.asString()) + }.onFailure { log.info("Could not read ApplicationInfo for startup telemetry: ${it.message}") } + runCatching { + val version = PluginManagerCore.getPlugin(PluginId.getId("ai.kilocode.jetbrains"))?.version + if (version != null) { + put("pluginVersion", version) + put("appVersion", version) + } + }.onFailure { log.info("Could not read plugin version for startup telemetry: ${it.message}") } + } + + private fun os(): String = when { + SystemInfo.isMac -> "darwin" + SystemInfo.isLinux -> "linux" + SystemInfo.isWindows -> "windows" + else -> "unknown" + } +} diff --git a/packages/kilo-jetbrains/backend/src/test/kotlin/ai/kilocode/backend/cli/KiloBackendCliManagerEnvTest.kt b/packages/kilo-jetbrains/backend/src/test/kotlin/ai/kilocode/backend/cli/KiloBackendCliManagerEnvTest.kt index 77517bfea4f..1a6ed2b5701 100644 --- a/packages/kilo-jetbrains/backend/src/test/kotlin/ai/kilocode/backend/cli/KiloBackendCliManagerEnvTest.kt +++ b/packages/kilo-jetbrains/backend/src/test/kotlin/ai/kilocode/backend/cli/KiloBackendCliManagerEnvTest.kt @@ -19,12 +19,14 @@ class KiloBackendCliManagerEnvTest { tmp = Files.createTempDirectory("kilo-cli-env-test").toFile() System.clearProperty("kilo.dev.storage.isolated") System.clearProperty("kilo.dev.worktree.root") + System.clearProperty("idea.plugin.in.sandbox.mode") } @AfterTest fun tearDown() { System.clearProperty("kilo.dev.storage.isolated") System.clearProperty("kilo.dev.worktree.root") + System.clearProperty("idea.plugin.in.sandbox.mode") tmp.deleteRecursively() } @@ -36,11 +38,21 @@ class KiloBackendCliManagerEnvTest { assertEquals("true", env["KILO_ENABLE_QUESTION_TOOL"]) assertEquals("jetbrains", env["KILO_PLATFORM"]) assertEquals("kilo-code", env["KILO_APP_NAME"]) + assertEquals("all", env["KILO_TELEMETRY_LEVEL"]) assertEquals("true", env["KILO_DISABLE_CLAUDE_CODE"]) assertEquals("jetbrains-plugin", env["KILOCODE_FEATURE"]) assertEquals("pwd123", env["KILO_SERVER_PASSWORD"]) } + @Test + fun `dev mode disables CLI telemetry`() { + System.setProperty("idea.plugin.in.sandbox.mode", "true") + + val env = manager.buildEnv("pwd123", emptyMap()) + + assertEquals("off", env["KILO_TELEMETRY_LEVEL"]) + } + @Test fun `isolation disabled - default CLI config asks for edit and bash permissions`() { val env = manager.buildEnv("pwd123", emptyMap()) diff --git a/packages/kilo-jetbrains/backend/src/test/kotlin/ai/kilocode/backend/telemetry/KiloBackendTelemetryTest.kt b/packages/kilo-jetbrains/backend/src/test/kotlin/ai/kilocode/backend/telemetry/KiloBackendTelemetryTest.kt new file mode 100644 index 00000000000..2c75e66158f --- /dev/null +++ b/packages/kilo-jetbrains/backend/src/test/kotlin/ai/kilocode/backend/telemetry/KiloBackendTelemetryTest.kt @@ -0,0 +1,73 @@ +package ai.kilocode.backend.telemetry + +import ai.kilocode.backend.cli.KiloBackendHttpClients +import kotlinx.coroutines.runBlocking +import okhttp3.mockwebserver.MockResponse +import okhttp3.mockwebserver.MockWebServer +import kotlin.test.Test +import kotlin.test.assertEquals +import kotlin.test.assertTrue +import java.util.concurrent.TimeUnit + +class KiloBackendTelemetryTest { + @Test + fun `capture posts to telemetry endpoint with auth`() = runBlocking { + val server = MockWebServer() + server.enqueue(MockResponse().setBody("{}")) + server.start() + val http = KiloBackendHttpClients.api("secret") + try { + KiloBackendTelemetry().capture(http, server.port, "Test Event", mapOf("source" to "test")) + + val req = server.takeRequest() + assertEquals("/telemetry/capture", req.path) + assertTrue(req.getHeader("Authorization")?.startsWith("Basic ") == true) + val body = req.body.readUtf8() + assertTrue(body.contains("Test Event")) + assertTrue(body.contains("source")) + } finally { + KiloBackendHttpClients.shutdown(http) + server.shutdown() + } + } + + @Test + fun `set enabled posts to telemetry endpoint`() = runBlocking { + val server = MockWebServer() + server.enqueue(MockResponse().setBody("{}")) + server.start() + val http = KiloBackendHttpClients.api("secret") + try { + KiloBackendTelemetry().setEnabled(http, server.port, true) + + val req = server.takeRequest() + assertEquals("/telemetry/setEnabled", req.path) + assertTrue(req.body.readUtf8().contains("enabled")) + } finally { + KiloBackendHttpClients.shutdown(http) + server.shutdown() + } + } + + @Test + fun `capture failure does not throw`() = runBlocking { + KiloBackendTelemetry().capture(null, 0, "Test Event", emptyMap()) + } + + @Test + fun `dev mode does not post capture`() = runBlocking { + System.setProperty("idea.plugin.in.sandbox.mode", "true") + val server = MockWebServer() + server.start() + val http = KiloBackendHttpClients.api("secret") + try { + KiloBackendTelemetry().capture(http, server.port, "Test Event", emptyMap()) + + assertEquals(null, server.takeRequest(100, TimeUnit.MILLISECONDS)) + } finally { + System.clearProperty("idea.plugin.in.sandbox.mode") + KiloBackendHttpClients.shutdown(http) + server.shutdown() + } + } +} diff --git a/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/KiloToolWindowFactory.kt b/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/KiloToolWindowFactory.kt index d449a5a1da4..c4541a3548c 100644 --- a/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/KiloToolWindowFactory.kt +++ b/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/KiloToolWindowFactory.kt @@ -3,6 +3,7 @@ package ai.kilocode.client import ai.kilocode.client.app.KiloWorkspaceService import ai.kilocode.client.app.Workspace import ai.kilocode.client.session.SessionSidePanelManager +import ai.kilocode.client.telemetry.Telemetry import ai.kilocode.log.KiloLog import com.intellij.openapi.actionSystem.ActionManager import com.intellij.openapi.components.service @@ -43,8 +44,10 @@ class KiloToolWindowFactory : ToolWindowFactory, DumbAware { withContext(Dispatchers.Main) { setup(project, toolWindow, workspace) } + Telemetry.send("JetBrains Tool Window Opened", mapOf("projectResolved" to dir.isNotBlank().toString())) } } catch (e: Exception) { + Telemetry.send("JetBrains Tool Window Setup Failed", mapOf("stage" to "create", "errorClass" to e::class.java.name)) LOG.error("Failed to create Kilo tool window content", e) } } @@ -71,6 +74,7 @@ class KiloToolWindowFactory : ToolWindowFactory, DumbAware { ) toolWindow.setTitleActions(actions) } catch (e: Exception) { + Telemetry.send("JetBrains Tool Window Setup Failed", mapOf("stage" to "setup", "errorClass" to e::class.java.name)) LOG.error("Failed to set up Kilo tool window content", e) } } diff --git a/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/actions/HistoryAction.kt b/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/actions/HistoryAction.kt index 92d662e7ae5..88945b29d7e 100644 --- a/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/actions/HistoryAction.kt +++ b/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/actions/HistoryAction.kt @@ -2,6 +2,7 @@ package ai.kilocode.client.actions import ai.kilocode.client.plugin.KiloBundle import ai.kilocode.client.session.SessionManager +import ai.kilocode.client.telemetry.Telemetry import com.intellij.icons.AllIcons import com.intellij.openapi.actionSystem.AnAction import com.intellij.openapi.actionSystem.AnActionEvent @@ -13,6 +14,7 @@ class HistoryAction : AnAction( AllIcons.Vcs.History, ), DumbAware { override fun actionPerformed(e: AnActionEvent) { + Telemetry.send("JetBrains History Opened", mapOf("surface" to "tool_window")) e.getData(SessionManager.KEY)?.showHistory() } diff --git a/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/actions/KiloSettingsAction.kt b/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/actions/KiloSettingsAction.kt index b7f6ba5d12f..50078e42200 100644 --- a/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/actions/KiloSettingsAction.kt +++ b/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/actions/KiloSettingsAction.kt @@ -1,5 +1,6 @@ package ai.kilocode.client.actions +import ai.kilocode.client.telemetry.Telemetry import com.intellij.openapi.actionSystem.ActionGroup import com.intellij.openapi.actionSystem.ActionManager import com.intellij.openapi.actionSystem.AnAction @@ -22,6 +23,7 @@ class KiloSettingsAction : AnAction() { override fun actionPerformed(e: AnActionEvent) { val component = e.inputEvent?.component ?: return val group = ActionManager.getInstance().getAction(GROUP_ID) as? ActionGroup ?: return + Telemetry.send("JetBrains Settings Opened", mapOf("surface" to "tool_window")) JBPopupFactory.getInstance() .createActionGroupPopup( diff --git a/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/actions/NewSessionAction.kt b/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/actions/NewSessionAction.kt index dbe8aefcd6c..a6c0e923cc0 100644 --- a/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/actions/NewSessionAction.kt +++ b/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/actions/NewSessionAction.kt @@ -2,6 +2,7 @@ package ai.kilocode.client.actions import ai.kilocode.client.plugin.KiloBundle import ai.kilocode.client.session.SessionManager +import ai.kilocode.client.telemetry.Telemetry import com.intellij.icons.AllIcons import com.intellij.openapi.actionSystem.AnAction import com.intellij.openapi.actionSystem.AnActionEvent @@ -13,6 +14,7 @@ class NewSessionAction : AnAction( AllIcons.General.Add, ), DumbAware { override fun actionPerformed(e: AnActionEvent) { + Telemetry.send("JetBrains New Session Clicked", mapOf("surface" to "tool_window")) e.getData(SessionManager.KEY)?.newSession() } diff --git a/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/actions/ReinstallKiloAction.kt b/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/actions/ReinstallKiloAction.kt index c5bd9574cd6..8b38efd08db 100644 --- a/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/actions/ReinstallKiloAction.kt +++ b/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/actions/ReinstallKiloAction.kt @@ -1,6 +1,7 @@ package ai.kilocode.client.actions import ai.kilocode.client.app.KiloAppService +import ai.kilocode.client.telemetry.Telemetry import com.intellij.openapi.actionSystem.AnAction import com.intellij.openapi.actionSystem.AnActionEvent import com.intellij.openapi.components.service @@ -8,6 +9,7 @@ import com.intellij.openapi.project.DumbAware class ReinstallKiloAction : AnAction(), DumbAware { override fun actionPerformed(e: AnActionEvent) { + Telemetry.send("JetBrains CLI Reinstall Clicked", mapOf("surface" to "settings")) service().reinstallAsync() } diff --git a/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/actions/RestartKiloAction.kt b/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/actions/RestartKiloAction.kt index 1df776223e4..752d0eb95f1 100644 --- a/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/actions/RestartKiloAction.kt +++ b/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/actions/RestartKiloAction.kt @@ -1,6 +1,7 @@ package ai.kilocode.client.actions import ai.kilocode.client.app.KiloAppService +import ai.kilocode.client.telemetry.Telemetry import com.intellij.openapi.actionSystem.AnAction import com.intellij.openapi.actionSystem.AnActionEvent import com.intellij.openapi.components.service @@ -8,6 +9,7 @@ import com.intellij.openapi.project.DumbAware class RestartKiloAction : AnAction(), DumbAware { override fun actionPerformed(e: AnActionEvent) { + Telemetry.send("JetBrains CLI Restart Clicked", mapOf("surface" to "settings")) service().restartAsync() } diff --git a/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/actions/ShowProfileAction.kt b/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/actions/ShowProfileAction.kt index ebf954c9fa1..1236e6cbb18 100644 --- a/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/actions/ShowProfileAction.kt +++ b/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/actions/ShowProfileAction.kt @@ -2,6 +2,7 @@ package ai.kilocode.client.actions import ai.kilocode.client.plugin.KiloBundle import ai.kilocode.client.settings.profile.UserProfileConfigurable +import ai.kilocode.client.telemetry.Telemetry import com.intellij.icons.AllIcons import com.intellij.openapi.actionSystem.ActionUpdateThread import com.intellij.openapi.actionSystem.AnActionEvent @@ -24,6 +25,7 @@ class ShowProfileAction : DumbAwareAction( ) { override fun actionPerformed(e: AnActionEvent) { + Telemetry.send("JetBrains Profile Settings Opened", mapOf("surface" to "tool_window")) ShowSettingsUtil.getInstance().showSettingsDialog( e.project, Predicate { cfg: Configurable -> diff --git a/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/migration/KiloMigrationService.kt b/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/migration/KiloMigrationService.kt index 82ef9cb1fc4..c0bcef1a016 100644 --- a/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/migration/KiloMigrationService.kt +++ b/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/migration/KiloMigrationService.kt @@ -4,6 +4,7 @@ package ai.kilocode.client.migration import ai.kilocode.client.app.KiloAppService import ai.kilocode.client.autocomplete.KiloAutocompleteSettingsService +import ai.kilocode.client.telemetry.Telemetry import ai.kilocode.log.KiloLog import ai.kilocode.rpc.KiloMigrationRpcApi import ai.kilocode.rpc.dto.KiloAppStateDto @@ -105,6 +106,7 @@ class KiloMigrationService internal constructor( return } LOG.info("Migration wizard: user started migration ${selectionSummary(selections)}") + telemetry("JetBrains Migration Started", selectionProps(selections)) lastSelections.set(selections) val dto = MigrationSelectionBuilder.toDto(selections) @@ -125,6 +127,7 @@ class KiloMigrationService internal constructor( val flow = try { call { migrate(dto) } } catch (e: Exception) { + telemetry("JetBrains Migration Failed", mapOf("itemCount" to initialProgress.size.toString(), "errorCount" to "1", "stage" to "start")) LOG.warn("migration start failed", e) finishWithError(e.message ?: "Migration failed") return@launch @@ -142,6 +145,8 @@ class KiloMigrationService internal constructor( */ override fun skip() { LOG.info("Migration wizard: user chose skip") + val current = _state.value as? MigrationUiState.Needed + if (current != null) telemetry("JetBrains Migration Skipped", detectionProps(current.detection)) cs.launch { try { call { skip() } @@ -165,6 +170,7 @@ class KiloMigrationService internal constructor( val status = if (hasErrors) LegacyMigrationStatusDto.completed_with_errors else LegacyMigrationStatusDto.completed val selections = lastSelections.get() LOG.info("Migration wizard: user finished migration status=$status results=${current.results.size} errors=${current.results.count { it.status == MigrationItemStatusDto.error }}") + telemetry("JetBrains Migration Finished", mapOf("status" to status.name, "cleanupRequested" to (selections?.keepLegacySettingsFile == false).toString())) cs.launch { try { call { finalize(status) } @@ -225,6 +231,11 @@ class KiloMigrationService internal constructor( val phase = if (hasErrors) MigrationUiPhase.error else MigrationUiPhase.done val progress = if (hasErrors) current.progress else finishSilentProgress(current.progress) LOG.info("Migration wizard: migration complete phase=$phase items=${items.size} errors=${items.count { it.status == MigrationItemStatusDto.error }}") + if (hasErrors) { + telemetry("JetBrains Migration Failed", mapOf("itemCount" to items.size.toString(), "errorCount" to items.count { it.status == MigrationItemStatusDto.error }.toString(), "stage" to "complete")) + } else { + telemetry("JetBrains Migration Completed", mapOf("itemCount" to items.size.toString(), "sessionImportedCount" to current.sessionSummary.imported.size.toString())) + } _state.value = current.copy( running = false, phase = phase, @@ -234,6 +245,7 @@ class KiloMigrationService internal constructor( } is LegacyMigrationEventDto.Error -> { LOG.warn("Migration wizard: migration error message=${event.message}") + telemetry("JetBrains Migration Failed", mapOf("itemCount" to current.progress.size.toString(), "errorCount" to "1", "stage" to "event")) finishWithError(event.message) } } @@ -245,6 +257,7 @@ class KiloMigrationService internal constructor( val current = _state.value if (current is MigrationUiState.Needed && current.detection == migration && current.phase != MigrationUiPhase.selecting) return LOG.info("Migration wizard: showing because backend requires migration ${detectionSummary(migration)}") + telemetry("JetBrains Migration Shown", detectionProps(migration)) _state.value = MigrationUiState.Needed(migration) return } @@ -298,6 +311,28 @@ class KiloMigrationService internal constructor( private fun detectionSummary(detection: ai.kilocode.rpc.dto.LegacyMigrationDetectionDto): String = "providers=${detection.providers.size} mcp=${detection.mcpServers.size} modes=${detection.customModes.size} sessions=${detection.sessions.size} model=${detection.defaultModel != null} settings=${detection.settings != null}" + private fun detectionProps(detection: ai.kilocode.rpc.dto.LegacyMigrationDetectionDto): Map = mapOf( + "settings" to (detection.settings != null).toString(), + "providers" to detection.providers.size.toString(), + "mcpServers" to detection.mcpServers.size.toString(), + "customModes" to detection.customModes.size.toString(), + "sessions" to detection.sessions.size.toString(), + "defaultModel" to (detection.defaultModel != null).toString(), + ) + + private fun selectionProps(selections: MigrationUiSelections): Map = mapOf( + "providers" to selections.providers.size.toString(), + "mcpServers" to selections.mcpServers.size.toString(), + "customModes" to selections.customModes.size.toString(), + "sessions" to selections.sessions.size.toString(), + "defaultModel" to selections.defaultModel.toString(), + "keepLegacySettingsFile" to selections.keepLegacySettingsFile.toString(), + ) + + private fun telemetry(event: String, props: Map) { + Telemetry.send(event, props) + } + private fun buildInitialProgress( selections: MigrationUiSelections, detection: ai.kilocode.rpc.dto.LegacyMigrationDetectionDto, diff --git a/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/SessionUi.kt b/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/SessionUi.kt index 133af6862f3..861ff21c044 100644 --- a/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/SessionUi.kt +++ b/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/SessionUi.kt @@ -32,6 +32,7 @@ import ai.kilocode.client.session.views.LoginRequiredView import ai.kilocode.client.session.views.permission.PermissionView import ai.kilocode.client.session.views.question.QuestionView import ai.kilocode.client.settings.profile.UserProfileConfigurable +import ai.kilocode.client.telemetry.Telemetry import ai.kilocode.client.ui.layout.Stack import ai.kilocode.log.ChatLogSummary import com.intellij.util.ui.JBUI @@ -285,7 +286,13 @@ class SessionUi( } prompt.setAutoApprove(controller.autoApprove) prompt.model.favorites = { app.favorites.value } - prompt.model.onFavoriteToggle = { item -> app.toggleModelFavorite(item.provider, item.id) } + prompt.model.onFavoriteToggle = { item -> + Telemetry.send( + "JetBrains Model Favorite Toggled", + mapOf("provider" to item.provider, "modelId" to item.id), + ) + app.toggleModelFavorite(item.provider, item.id) + } controller.addListener(this) { event -> when (event) { diff --git a/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/controller/SessionController.kt b/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/controller/SessionController.kt index 9ca8d56807f..95a613b418d 100644 --- a/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/controller/SessionController.kt +++ b/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/controller/SessionController.kt @@ -20,6 +20,7 @@ import ai.kilocode.client.session.model.QuestionOption import ai.kilocode.client.session.model.ToolCallRef import ai.kilocode.client.plugin.KiloPluginSettings import ai.kilocode.client.session.SessionRef +import ai.kilocode.client.telemetry.Telemetry import ai.kilocode.rpc.dto.ChatEventDto import ai.kilocode.rpc.dto.ConfigWarningDto import ai.kilocode.rpc.dto.ConfigUpdateDto @@ -83,6 +84,7 @@ class SessionController( private val afterUpdate: (Boolean) -> Unit = {}, private val loaded: (Boolean) -> Unit = {}, private val openProfileAction: () -> Unit = {}, + private val telemetry: (String, Map) -> Unit = { event, props -> Telemetry.send(event, props) }, ) : Disposable { private data class OrganizationTarget(val org: String?) @@ -189,6 +191,7 @@ class SessionController( fun prompt(text: String) { assertEdt() val start = sid ?: ref?.key ?: "pending" + val exists = sid != null val dto = promptDto(text) LOG.debug { "${ChatLogSummary.sid(start)} ${ChatLogSummary.prompt(text)} ${ChatLogSummary.dir(directory)}" } showSession() @@ -207,12 +210,15 @@ class SessionController( if (disposed) return@launch val meta = if (LOG.isDebugEnabled) ChatLogSummary.dir(directory) else "kind=session" LOG.info("${ChatLogSummary.sid(session.id)} kind=session $meta created=true") + capture("Task Created", sessionProps(session.id) + mapOf("source" to "jetbrains")) subscribeEvents() session.id } sessions.prompt(id, directory, dto) + capture("Conversation Message", sessionProps(id) + mapOf("source" to "user", "hasExistingSession" to exists.toString())) LOG.debug { "${ChatLogSummary.sid(id)} kind=prompt dispatched=true" } } catch (e: Exception) { + capture("JetBrains Session Error", sessionProps(sid ?: ref?.key ?: start) + mapOf("context" to "prompt", "errorClass" to e::class.java.name)) LOG.warn("${ChatLogSummary.sid(sid ?: ref?.key ?: start)} kind=prompt dir=${ChatLogSummary.dir(directory)} failed message=${e.message}", e) edt { if (disposed) return@edt @@ -232,8 +238,10 @@ class SessionController( cs.launch { try { sessions.abort(id, directory) + capture("JetBrains Session Stopped", sessionProps(id)) LOG.debug { "${ChatLogSummary.sid(id)} kind=abort ok=true" } } catch (e: Exception) { + capture("JetBrains Session Error", sessionProps(id) + mapOf("context" to "abort", "errorClass" to e::class.java.name)) LOG.warn("${ChatLogSummary.sid(id)} kind=abort dir=${ChatLogSummary.dir(directory)} failed message=${e.message}", e) } } @@ -242,6 +250,7 @@ class SessionController( fun setAutoApprove(value: Boolean) { assertEdt() KiloPluginSettings.setAutoApprove(value) + capture("JetBrains Auto Approve Toggled", mapOf("enabled" to value.toString())) if (!value) { drainJob?.cancel() drainJob = null @@ -268,8 +277,10 @@ class SessionController( cs.launch { try { sessions.compact(id, directory, sel) + capture("Context Condensed", sessionProps(id) + mapOf("provider" to sel.providerID, "modelId" to sel.modelID)) LOG.debug { "${ChatLogSummary.sid(id)} kind=compact ok=true" } } catch (e: Exception) { + capture("JetBrains Session Error", sessionProps(id) + mapOf("context" to "compact", "errorClass" to e::class.java.name)) LOG.warn("${ChatLogSummary.sid(id)} kind=compact dir=${ChatLogSummary.dir(directory)} failed message=${e.message}", e) edt { updateModel { @@ -320,6 +331,7 @@ class SessionController( model.agent = name syncModelSelection() } + capture("Mode Switched", sessionProps() + mapOf("agent" to name)) } fun selectModel(provider: String, id: String) { @@ -334,6 +346,7 @@ class SessionController( app.selectModel(agent, provider, id) selectResolvedModel(key) model.modelOverride = model.defaultModel != model.model + capture("JetBrains Model Selected", sessionProps() + mapOf("agent" to agent, "provider" to provider, "modelId" to id, "isOverride" to "true")) } fun clearModelOverride() { @@ -344,6 +357,7 @@ class SessionController( val auto = configModel(agent) ?: providerModel(agent) selectResolvedModel(auto) model.modelOverride = false + capture("JetBrains Model Override Cleared", sessionProps() + mapOf("agent" to agent)) } fun selectVariant(value: String) { @@ -353,6 +367,7 @@ class SessionController( LOG.debug { "${ChatLogSummary.sid(sid ?: ref?.key ?: "pending")} kind=config variant=$key/$value" } app.selectVariant(key, value) model.variant = value + capture("JetBrains Reasoning Variant Selected", sessionProps() + mapOf("model" to key, "variant" to value)) } // ------ permission / question resolution ------ @@ -360,13 +375,23 @@ class SessionController( fun replyPermission(requestId: String, reply: PermissionReplyDto, rules: PermissionAlwaysRulesDto? = null) { assertEdt() LOG.debug { "${ChatLogSummary.sid(sid ?: ref?.key ?: "pending")} kind=permission rid=$requestId reply=${reply.reply}" } + val current = model.state as? SessionState.AwaitingPermission updatePermission(requestId, PermissionRequestState.RESPONDING) cs.launch { try { if (rules != null) sessions.savePermissionRules(requestId, directory, rules) sessions.replyPermission(requestId, directory, reply) + capture("Ask Approval", sessionProps() + mapOf( + "requestId" to requestId, + "tool" to (current?.permission?.name ?: "unknown"), + "reply" to reply.reply, + "hasRules" to (rules != null).toString(), + "hasDiffs" to (current?.permission?.meta?.fileDiffs?.isNotEmpty() == true).toString(), + "diffCount" to (current?.permission?.meta?.fileDiffs?.size ?: 0).toString(), + )) LOG.debug { "${ChatLogSummary.sid(sid ?: ref?.key ?: "pending")} kind=permission rid=$requestId ok=true" } } catch (e: Exception) { + capture("JetBrains Session Error", sessionProps() + mapOf("context" to "permission", "errorClass" to e::class.java.name)) LOG.warn("${ChatLogSummary.sid(sid ?: ref?.key ?: "pending")} kind=permission rid=$requestId reply=${reply.reply} dir=${ChatLogSummary.dir(directory)} failed message=${e.message}", e) edt { updatePermission( @@ -404,6 +429,7 @@ class SessionController( model.setState(SessionState.Busy(KiloBundle.message("session.status.considering"))) } sessions.replyPermission(id, directory, PermissionReplyDto("once")) + capture("JetBrains Permission Auto Approved", sessionProps() + mapOf("tool" to restore().name, "source" to "single")) LOG.debug { "${ChatLogSummary.sid(sid ?: ref?.key ?: "pending")} kind=permission-auto rid=$id ok=true" } } catch (e: Exception) { LOG.warn("${ChatLogSummary.sid(sid ?: ref?.key ?: "pending")} kind=permission-auto rid=$id dir=${ChatLogSummary.dir(directory)} failed message=${e.message}", e) @@ -445,6 +471,7 @@ class SessionController( for (request in permissions) { if (!autoApprove) return count sessions.replyPermission(request.id, directory, PermissionReplyDto("once")) + capture("JetBrains Permission Auto Approved", sessionProps(request.sessionID) + mapOf("tool" to request.permission, "source" to "drain")) count++ } return count @@ -472,9 +499,15 @@ class SessionController( ) { Followup(directory, System.currentTimeMillis()) } else null + val follow = followup != null cs.launch { try { sessions.replyQuestion(requestId, directory, answers) + capture("JetBrains Question Answered", sessionProps() + mapOf( + "requestId" to requestId, + "answerCount" to answers.answers.size.toString(), + "hasFollowupNewSession" to follow.toString(), + )) LOG.debug { "${ChatLogSummary.sid(sid ?: ref?.key ?: "pending")} kind=question rid=$requestId ok=true" } } catch (e: Exception) { edt { followup = null } @@ -490,6 +523,7 @@ class SessionController( cs.launch { try { sessions.rejectQuestion(requestId, directory) + capture("JetBrains Question Rejected", sessionProps() + mapOf("requestId" to requestId)) LOG.debug { "${ChatLogSummary.sid(sid ?: ref?.key ?: "pending")} kind=question rid=$requestId ok=true" } } catch (e: Exception) { LOG.warn("${ChatLogSummary.sid(sid ?: ref?.key ?: "pending")} kind=question rid=$requestId rejected=true dir=${ChatLogSummary.dir(directory)} failed message=${e.message}", e) @@ -879,12 +913,16 @@ class SessionController( || current is SessionState.Busy || current is SessionState.Retry || current is SessionState.Offline - if (clobberOk) model.setState(SessionState.Idle) + if (clobberOk) { + if (event.reason == "completed") capture("Task Completed", sessionProps(event.sessionID)) + model.setState(SessionState.Idle) + } } is ChatEventDto.SessionCreated -> adoptFollowup(event.info) is ChatEventDto.Error -> { + capture("JetBrains Session Error", sessionProps(event.sessionID) + mapOf("context" to "event", "errorClass" to (event.error?.type ?: "unknown"))) error(event, true) } @@ -922,7 +960,10 @@ class SessionController( idle() } - is ChatEventDto.SessionCompacted -> model.markCompacted() + is ChatEventDto.SessionCompacted -> { + capture("Context Condensed", sessionProps(event.sessionID)) + model.markCompacted() + } is ChatEventDto.SessionDiffChanged -> model.setDiff(event.diff) is ChatEventDto.TodoUpdated -> model.setTodos(event.todos) } @@ -1279,7 +1320,9 @@ class SessionController( cs.launch { try { app.setOrganization(org) + capture("JetBrains Organization Switched", mapOf("target" to if (org == null) "personal" else "organization")) } catch (e: Exception) { + capture("JetBrains Account Connect Failed", mapOf("stage" to "organization", "errorClass" to e::class.java.name)) LOG.warn("account switch failed org=$org message=${e.message}", e) edt { if (disposed) return@edt @@ -1292,9 +1335,22 @@ class SessionController( fun openProfile() { assertEdt() + capture("JetBrains Profile Settings Opened", mapOf("surface" to "session_overlay")) openProfileAction() } + private fun capture(event: String, props: Map = emptyMap()) { + telemetry(event, props) + } + + private fun sessionProps(id: String? = sid): Map = buildMap { + id?.let { put("sessionId", it) } + if (ApplicationManager.getApplication().isDispatchThread) { + model.agent?.let { put("agent", it) } + model.model?.let { put("model", it) } + } + } + fun dismissLoginRequired() { assertEdt() loginRetry = null diff --git a/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/settings/profile/ProfileUi.kt b/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/settings/profile/ProfileUi.kt index 82337f20b08..4084ccbc242 100644 --- a/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/settings/profile/ProfileUi.kt +++ b/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/settings/profile/ProfileUi.kt @@ -2,6 +2,7 @@ package ai.kilocode.client.settings.profile import ai.kilocode.client.app.KiloAppService import ai.kilocode.client.plugin.KiloBundle +import ai.kilocode.client.telemetry.Telemetry import ai.kilocode.rpc.dto.KiloAppStateDto import ai.kilocode.rpc.dto.KiloAppStatusDto import ai.kilocode.rpc.dto.ProfileDto @@ -53,7 +54,10 @@ internal class ProfileUi( browse = browse, ) private val account = LoggedInProfileUi( - dashboard = { browse(DASHBOARD_URL) }, + dashboard = { + telemetry("JetBrains Dashboard Opened", mapOf("surface" to "settings")) + browse(DASHBOARD_URL) + }, logout = ::logout, organization = ::organization, refresh = ::refreshProfile, @@ -180,6 +184,7 @@ internal class ProfileUi( private fun start() { val id = ++attempt login = LoginState.Initiating + telemetry("Account Connect Clicked", mapOf("surface" to "settings")) sync() cs.launch { try { @@ -191,6 +196,7 @@ internal class ProfileUi( browse(next.verificationUrl) } val profile = app.completeLogin() + telemetry("Account Connect Success", mapOf("surface" to "settings", "hasOrganizations" to ((profile?.organizations?.isNotEmpty()) == true).toString())) val state = app.state.value withContext(edt) { if (id != attempt) return@withContext @@ -200,6 +206,7 @@ internal class ProfileUi( } catch (e: CancellationException) { throw e } catch (e: Exception) { + telemetry("JetBrains Account Connect Failed", mapOf("stage" to "complete", "errorClass" to e::class.java.name)) withContext(edt) { if (id != attempt) return@withContext login = LoginState.Error(compactLoginError(e)) @@ -212,14 +219,17 @@ internal class ProfileUi( private fun cancel() { attempt++ login = LoginState.Idle + telemetry("JetBrains Account Connect Failed", mapOf("stage" to "cancel", "errorClass" to "cancelled")) sync() } private fun logout() { + telemetry("Account Logout Clicked", mapOf("surface" to "settings")) cs.launch { try { val ok = app.logout() if (!ok) return@launch + telemetry("Account Logout Success", mapOf("surface" to "settings")) withContext(edt) { login = LoginState.Idle applyState() @@ -238,6 +248,7 @@ internal class ProfileUi( cs.launch { try { val profile = app.setOrganization(org) + telemetry("JetBrains Organization Switched", mapOf("target" to if (org == null) "personal" else "organization")) val state = app.state.value withContext(edt) { update(profile ?: state.profile, state.status) @@ -271,6 +282,10 @@ internal class ProfileUi( } } } + + private fun telemetry(event: String, props: Map) { + Telemetry.send(event, props) + } } private val HTML_MARKERS = listOf(" = emptyMap()) { + KiloTelemetryService.getInstance().send(event, properties) + } +} + +@Service(Service.Level.APP) +class KiloTelemetryService internal constructor( + private val cs: CoroutineScope, + private val rpc: KiloAppRpcApi?, +) { + constructor(cs: CoroutineScope) : this(cs, null) + + companion object { + private val LOG = KiloLog.create(KiloTelemetryService::class.java) + + fun getInstance(): KiloTelemetryService = service() + } + + fun capture(event: String, properties: Map = emptyMap()) { + send(event, properties) + } + + fun send(event: String, properties: Map = emptyMap()) { + cs.launch { + try { + val dto = TelemetryCaptureDto(event, properties) + val api = rpc + if (api != null) api.captureTelemetry(dto) + else durable { KiloAppRpcApi.getInstance().captureTelemetry(dto) } + } catch (e: Exception) { + LOG.warn("telemetry capture failed: ${e.message}", e) + } + } + } +} diff --git a/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/testing/FakeAppRpcApi.kt b/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/testing/FakeAppRpcApi.kt index 6ebca7de308..8a001addf95 100644 --- a/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/testing/FakeAppRpcApi.kt +++ b/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/testing/FakeAppRpcApi.kt @@ -11,6 +11,7 @@ import ai.kilocode.rpc.dto.ModelSelectionUpdateDto import ai.kilocode.rpc.dto.ModelStateDto import ai.kilocode.rpc.dto.ModelVariantUpdateDto import ai.kilocode.rpc.dto.ProfileDto +import ai.kilocode.rpc.dto.TelemetryCaptureDto import kotlinx.coroutines.CompletableDeferred import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.MutableStateFlow @@ -116,6 +117,7 @@ class FakeAppRpcApi : KiloAppRpcApi { var fakeDeviceAuth = DeviceAuthDto(code = "TEST-1234", verificationUrl = "https://auth.kilo.ai/device") val orgProfiles = mutableMapOf() val orgSelections = mutableListOf() + val telemetry = mutableListOf() /** When set, [completeLogin] will await this deferred before returning. */ var completeGate: CompletableDeferred? = null @@ -186,4 +188,9 @@ class FakeAppRpcApi : KiloAppRpcApi { if (orgProfiles.containsKey(organizationId)) fakeProfile = orgProfiles[organizationId] return fakeProfile } + + override suspend fun captureTelemetry(capture: TelemetryCaptureDto) { + assertNotEdt("captureTelemetry") + telemetry.add(capture) + } } diff --git a/packages/kilo-jetbrains/shared/src/main/kotlin/ai/kilocode/rpc/KiloAppRpcApi.kt b/packages/kilo-jetbrains/shared/src/main/kotlin/ai/kilocode/rpc/KiloAppRpcApi.kt index df83a017ac7..9411fefc880 100644 --- a/packages/kilo-jetbrains/shared/src/main/kotlin/ai/kilocode/rpc/KiloAppRpcApi.kt +++ b/packages/kilo-jetbrains/shared/src/main/kotlin/ai/kilocode/rpc/KiloAppRpcApi.kt @@ -8,6 +8,7 @@ import ai.kilocode.rpc.dto.ModelSelectionUpdateDto import ai.kilocode.rpc.dto.ModelStateDto import ai.kilocode.rpc.dto.ModelVariantUpdateDto import ai.kilocode.rpc.dto.ProfileDto +import ai.kilocode.rpc.dto.TelemetryCaptureDto import com.intellij.platform.rpc.RemoteApiProviderService import fleet.rpc.RemoteApi import fleet.rpc.Rpc @@ -85,4 +86,7 @@ interface KiloAppRpcApi : RemoteApi { * Returns the updated profile, or null if not logged in. */ suspend fun setOrganization(organizationId: String?): ProfileDto? + + /** Fire-and-forget behavior telemetry routed through the CLI server. */ + suspend fun captureTelemetry(capture: TelemetryCaptureDto) } diff --git a/packages/kilo-jetbrains/shared/src/main/kotlin/ai/kilocode/rpc/dto/TelemetryDto.kt b/packages/kilo-jetbrains/shared/src/main/kotlin/ai/kilocode/rpc/dto/TelemetryDto.kt new file mode 100644 index 00000000000..7bfffc64555 --- /dev/null +++ b/packages/kilo-jetbrains/shared/src/main/kotlin/ai/kilocode/rpc/dto/TelemetryDto.kt @@ -0,0 +1,9 @@ +package ai.kilocode.rpc.dto + +import kotlinx.serialization.Serializable + +@Serializable +data class TelemetryCaptureDto( + val event: String, + val properties: Map = emptyMap(), +) From 6d76e3a3d7fb042e33c0abbd9096f7aaf794d982 Mon Sep 17 00:00:00 2001 From: kirillk Date: Fri, 29 May 2026 14:28:51 -0400 Subject: [PATCH 02/16] fix(jetbrains): remove telemetry event prefix --- .../backend/app/KiloBackendAppService.kt | 2 +- .../backend/telemetry/KiloBackendTelemetry.kt | 1 - .../telemetry/KiloCliStartupTelemetry.kt | 3 +- .../telemetry/KiloBackendTelemetryTest.kt | 5 +- .../kilocode/client/KiloToolWindowFactory.kt | 6 +-- .../kilocode/client/actions/HistoryAction.kt | 2 +- .../client/actions/KiloSettingsAction.kt | 2 +- .../client/actions/NewSessionAction.kt | 2 +- .../client/actions/ReinstallKiloAction.kt | 2 +- .../client/actions/RestartKiloAction.kt | 2 +- .../client/actions/ShowProfileAction.kt | 2 +- .../client/migration/KiloMigrationService.kt | 16 +++---- .../client/session/SessionSidePanelManager.kt | 2 + .../ai/kilocode/client/session/SessionUi.kt | 2 +- .../session/controller/SessionController.kt | 48 ++++++++++++------- .../session/history/HistoryController.kt | 7 ++- .../client/settings/profile/ProfileUi.kt | 8 ++-- 17 files changed, 66 insertions(+), 46 deletions(-) diff --git a/packages/kilo-jetbrains/backend/src/main/kotlin/ai/kilocode/backend/app/KiloBackendAppService.kt b/packages/kilo-jetbrains/backend/src/main/kotlin/ai/kilocode/backend/app/KiloBackendAppService.kt index bed5b95e0f8..39fb3ad513a 100644 --- a/packages/kilo-jetbrains/backend/src/main/kotlin/ai/kilocode/backend/app/KiloBackendAppService.kt +++ b/packages/kilo-jetbrains/backend/src/main/kotlin/ai/kilocode/backend/app/KiloBackendAppService.kt @@ -345,7 +345,7 @@ class KiloBackendAppService private constructor( chat.start(connection.apiClient!!, connection.port, connection.events) workspaces.start(connection.api!!, connection.apiClient!!, connection.port, connection.events) service().setEnabled(connection.apiClient, connection.port, true) - service().capture(connection.apiClient, connection.port, "JetBrains Backend Connected", mapOf("portKnown" to "true")) + service().capture(connection.apiClient, connection.port, "Backend Connected", mapOf("portKnown" to "true")) setAppReady( AppData( profile = prof, diff --git a/packages/kilo-jetbrains/backend/src/main/kotlin/ai/kilocode/backend/telemetry/KiloBackendTelemetry.kt b/packages/kilo-jetbrains/backend/src/main/kotlin/ai/kilocode/backend/telemetry/KiloBackendTelemetry.kt index 88648b4a6a3..835f3984edf 100644 --- a/packages/kilo-jetbrains/backend/src/main/kotlin/ai/kilocode/backend/telemetry/KiloBackendTelemetry.kt +++ b/packages/kilo-jetbrains/backend/src/main/kotlin/ai/kilocode/backend/telemetry/KiloBackendTelemetry.kt @@ -63,7 +63,6 @@ class KiloBackendTelemetry( ).toString() private fun base(): Map = buildMap { - put("appName", JsonPrimitive("kilo-code")) put("platform", JsonPrimitive("jetbrains")) put("client", JsonPrimitive("jetbrains")) put("feature", JsonPrimitive("jetbrains-plugin")) diff --git a/packages/kilo-jetbrains/backend/src/main/kotlin/ai/kilocode/backend/telemetry/KiloCliStartupTelemetry.kt b/packages/kilo-jetbrains/backend/src/main/kotlin/ai/kilocode/backend/telemetry/KiloCliStartupTelemetry.kt index 459debbb05d..5cdfe44efea 100644 --- a/packages/kilo-jetbrains/backend/src/main/kotlin/ai/kilocode/backend/telemetry/KiloCliStartupTelemetry.kt +++ b/packages/kilo-jetbrains/backend/src/main/kotlin/ai/kilocode/backend/telemetry/KiloCliStartupTelemetry.kt @@ -35,7 +35,7 @@ class KiloCliStartupTelemetry( val body = JsonObject( mapOf( "api_key" to JsonPrimitive("phc_GK2Pxl0HPj5ZPfwhLRjXrtdz8eD7e9MKnXiFrOqnB6z"), - "event" to JsonPrimitive("JetBrains CLI Startup Failed"), + "event" to JsonPrimitive("CLI Startup Failed"), "distinct_id" to JsonPrimitive(props["machineId"] ?: "jetbrains-unknown"), "properties" to JsonObject(props.mapValues { JsonPrimitive(it.value) }), ), @@ -59,7 +59,6 @@ class KiloCliStartupTelemetry( } private fun base(): Map = buildMap { - put("appName", "kilo-code") put("platform", "jetbrains") put("client", "jetbrains") put("feature", "jetbrains-plugin") diff --git a/packages/kilo-jetbrains/backend/src/test/kotlin/ai/kilocode/backend/telemetry/KiloBackendTelemetryTest.kt b/packages/kilo-jetbrains/backend/src/test/kotlin/ai/kilocode/backend/telemetry/KiloBackendTelemetryTest.kt index 2c75e66158f..1a9a5be5a07 100644 --- a/packages/kilo-jetbrains/backend/src/test/kotlin/ai/kilocode/backend/telemetry/KiloBackendTelemetryTest.kt +++ b/packages/kilo-jetbrains/backend/src/test/kotlin/ai/kilocode/backend/telemetry/KiloBackendTelemetryTest.kt @@ -23,7 +23,10 @@ class KiloBackendTelemetryTest { assertEquals("/telemetry/capture", req.path) assertTrue(req.getHeader("Authorization")?.startsWith("Basic ") == true) val body = req.body.readUtf8() - assertTrue(body.contains("Test Event")) + assertTrue(body.contains("\"event\":\"Test Event\"")) + assertTrue(!body.contains("JetBrains")) + assertTrue(body.contains("\"platform\":\"jetbrains\"")) + assertTrue(!body.contains("appName")) assertTrue(body.contains("source")) } finally { KiloBackendHttpClients.shutdown(http) diff --git a/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/KiloToolWindowFactory.kt b/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/KiloToolWindowFactory.kt index c4541a3548c..787b829769d 100644 --- a/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/KiloToolWindowFactory.kt +++ b/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/KiloToolWindowFactory.kt @@ -44,10 +44,10 @@ class KiloToolWindowFactory : ToolWindowFactory, DumbAware { withContext(Dispatchers.Main) { setup(project, toolWindow, workspace) } - Telemetry.send("JetBrains Tool Window Opened", mapOf("projectResolved" to dir.isNotBlank().toString())) + Telemetry.send("Tool Window Opened", mapOf("projectResolved" to dir.isNotBlank().toString())) } } catch (e: Exception) { - Telemetry.send("JetBrains Tool Window Setup Failed", mapOf("stage" to "create", "errorClass" to e::class.java.name)) + Telemetry.send("Tool Window Setup Failed", mapOf("stage" to "create", "errorClass" to e::class.java.name)) LOG.error("Failed to create Kilo tool window content", e) } } @@ -74,7 +74,7 @@ class KiloToolWindowFactory : ToolWindowFactory, DumbAware { ) toolWindow.setTitleActions(actions) } catch (e: Exception) { - Telemetry.send("JetBrains Tool Window Setup Failed", mapOf("stage" to "setup", "errorClass" to e::class.java.name)) + Telemetry.send("Tool Window Setup Failed", mapOf("stage" to "setup", "errorClass" to e::class.java.name)) LOG.error("Failed to set up Kilo tool window content", e) } } diff --git a/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/actions/HistoryAction.kt b/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/actions/HistoryAction.kt index 88945b29d7e..9a82e36a1ed 100644 --- a/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/actions/HistoryAction.kt +++ b/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/actions/HistoryAction.kt @@ -14,7 +14,7 @@ class HistoryAction : AnAction( AllIcons.Vcs.History, ), DumbAware { override fun actionPerformed(e: AnActionEvent) { - Telemetry.send("JetBrains History Opened", mapOf("surface" to "tool_window")) + Telemetry.send("History Opened", mapOf("surface" to "tool_window")) e.getData(SessionManager.KEY)?.showHistory() } diff --git a/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/actions/KiloSettingsAction.kt b/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/actions/KiloSettingsAction.kt index 50078e42200..d3f12fb4089 100644 --- a/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/actions/KiloSettingsAction.kt +++ b/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/actions/KiloSettingsAction.kt @@ -23,7 +23,7 @@ class KiloSettingsAction : AnAction() { override fun actionPerformed(e: AnActionEvent) { val component = e.inputEvent?.component ?: return val group = ActionManager.getInstance().getAction(GROUP_ID) as? ActionGroup ?: return - Telemetry.send("JetBrains Settings Opened", mapOf("surface" to "tool_window")) + Telemetry.send("Settings Opened", mapOf("surface" to "tool_window")) JBPopupFactory.getInstance() .createActionGroupPopup( diff --git a/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/actions/NewSessionAction.kt b/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/actions/NewSessionAction.kt index a6c0e923cc0..385f45e45a7 100644 --- a/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/actions/NewSessionAction.kt +++ b/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/actions/NewSessionAction.kt @@ -14,7 +14,7 @@ class NewSessionAction : AnAction( AllIcons.General.Add, ), DumbAware { override fun actionPerformed(e: AnActionEvent) { - Telemetry.send("JetBrains New Session Clicked", mapOf("surface" to "tool_window")) + Telemetry.send("New Session Clicked", mapOf("surface" to "tool_window")) e.getData(SessionManager.KEY)?.newSession() } diff --git a/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/actions/ReinstallKiloAction.kt b/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/actions/ReinstallKiloAction.kt index 8b38efd08db..e6d982347ac 100644 --- a/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/actions/ReinstallKiloAction.kt +++ b/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/actions/ReinstallKiloAction.kt @@ -9,7 +9,7 @@ import com.intellij.openapi.project.DumbAware class ReinstallKiloAction : AnAction(), DumbAware { override fun actionPerformed(e: AnActionEvent) { - Telemetry.send("JetBrains CLI Reinstall Clicked", mapOf("surface" to "settings")) + Telemetry.send("CLI Reinstall Clicked", mapOf("surface" to "settings")) service().reinstallAsync() } diff --git a/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/actions/RestartKiloAction.kt b/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/actions/RestartKiloAction.kt index 752d0eb95f1..93d356c78d6 100644 --- a/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/actions/RestartKiloAction.kt +++ b/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/actions/RestartKiloAction.kt @@ -9,7 +9,7 @@ import com.intellij.openapi.project.DumbAware class RestartKiloAction : AnAction(), DumbAware { override fun actionPerformed(e: AnActionEvent) { - Telemetry.send("JetBrains CLI Restart Clicked", mapOf("surface" to "settings")) + Telemetry.send("CLI Restart Clicked", mapOf("surface" to "settings")) service().restartAsync() } diff --git a/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/actions/ShowProfileAction.kt b/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/actions/ShowProfileAction.kt index 1236e6cbb18..d7c82f3cb16 100644 --- a/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/actions/ShowProfileAction.kt +++ b/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/actions/ShowProfileAction.kt @@ -25,7 +25,7 @@ class ShowProfileAction : DumbAwareAction( ) { override fun actionPerformed(e: AnActionEvent) { - Telemetry.send("JetBrains Profile Settings Opened", mapOf("surface" to "tool_window")) + Telemetry.send("Profile Settings Opened", mapOf("surface" to "tool_window")) ShowSettingsUtil.getInstance().showSettingsDialog( e.project, Predicate { cfg: Configurable -> diff --git a/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/migration/KiloMigrationService.kt b/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/migration/KiloMigrationService.kt index c0bcef1a016..0ebe5e89320 100644 --- a/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/migration/KiloMigrationService.kt +++ b/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/migration/KiloMigrationService.kt @@ -106,7 +106,7 @@ class KiloMigrationService internal constructor( return } LOG.info("Migration wizard: user started migration ${selectionSummary(selections)}") - telemetry("JetBrains Migration Started", selectionProps(selections)) + telemetry("Migration Started", selectionProps(selections)) lastSelections.set(selections) val dto = MigrationSelectionBuilder.toDto(selections) @@ -127,7 +127,7 @@ class KiloMigrationService internal constructor( val flow = try { call { migrate(dto) } } catch (e: Exception) { - telemetry("JetBrains Migration Failed", mapOf("itemCount" to initialProgress.size.toString(), "errorCount" to "1", "stage" to "start")) + telemetry("Migration Failed", mapOf("itemCount" to initialProgress.size.toString(), "errorCount" to "1", "stage" to "start")) LOG.warn("migration start failed", e) finishWithError(e.message ?: "Migration failed") return@launch @@ -146,7 +146,7 @@ class KiloMigrationService internal constructor( override fun skip() { LOG.info("Migration wizard: user chose skip") val current = _state.value as? MigrationUiState.Needed - if (current != null) telemetry("JetBrains Migration Skipped", detectionProps(current.detection)) + if (current != null) telemetry("Migration Skipped", detectionProps(current.detection)) cs.launch { try { call { skip() } @@ -170,7 +170,7 @@ class KiloMigrationService internal constructor( val status = if (hasErrors) LegacyMigrationStatusDto.completed_with_errors else LegacyMigrationStatusDto.completed val selections = lastSelections.get() LOG.info("Migration wizard: user finished migration status=$status results=${current.results.size} errors=${current.results.count { it.status == MigrationItemStatusDto.error }}") - telemetry("JetBrains Migration Finished", mapOf("status" to status.name, "cleanupRequested" to (selections?.keepLegacySettingsFile == false).toString())) + telemetry("Migration Finished", mapOf("status" to status.name, "cleanupRequested" to (selections?.keepLegacySettingsFile == false).toString())) cs.launch { try { call { finalize(status) } @@ -232,9 +232,9 @@ class KiloMigrationService internal constructor( val progress = if (hasErrors) current.progress else finishSilentProgress(current.progress) LOG.info("Migration wizard: migration complete phase=$phase items=${items.size} errors=${items.count { it.status == MigrationItemStatusDto.error }}") if (hasErrors) { - telemetry("JetBrains Migration Failed", mapOf("itemCount" to items.size.toString(), "errorCount" to items.count { it.status == MigrationItemStatusDto.error }.toString(), "stage" to "complete")) + telemetry("Migration Failed", mapOf("itemCount" to items.size.toString(), "errorCount" to items.count { it.status == MigrationItemStatusDto.error }.toString(), "stage" to "complete")) } else { - telemetry("JetBrains Migration Completed", mapOf("itemCount" to items.size.toString(), "sessionImportedCount" to current.sessionSummary.imported.size.toString())) + telemetry("Migration Completed", mapOf("itemCount" to items.size.toString(), "sessionImportedCount" to current.sessionSummary.imported.size.toString())) } _state.value = current.copy( running = false, @@ -245,7 +245,7 @@ class KiloMigrationService internal constructor( } is LegacyMigrationEventDto.Error -> { LOG.warn("Migration wizard: migration error message=${event.message}") - telemetry("JetBrains Migration Failed", mapOf("itemCount" to current.progress.size.toString(), "errorCount" to "1", "stage" to "event")) + telemetry("Migration Failed", mapOf("itemCount" to current.progress.size.toString(), "errorCount" to "1", "stage" to "event")) finishWithError(event.message) } } @@ -257,7 +257,7 @@ class KiloMigrationService internal constructor( val current = _state.value if (current is MigrationUiState.Needed && current.detection == migration && current.phase != MigrationUiPhase.selecting) return LOG.info("Migration wizard: showing because backend requires migration ${detectionSummary(migration)}") - telemetry("JetBrains Migration Shown", detectionProps(migration)) + telemetry("Migration Shown", detectionProps(migration)) _state.value = MigrationUiState.Needed(migration) return } diff --git a/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/SessionSidePanelManager.kt b/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/SessionSidePanelManager.kt index 6d0cf70b7f0..b4cfcd3f340 100644 --- a/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/SessionSidePanelManager.kt +++ b/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/SessionSidePanelManager.kt @@ -5,6 +5,7 @@ import ai.kilocode.client.app.KiloWorkspaceService import ai.kilocode.client.app.Workspace import ai.kilocode.client.session.history.HistoryController import ai.kilocode.client.session.history.HistoryPanel +import ai.kilocode.client.telemetry.Telemetry import com.intellij.openapi.Disposable import com.intellij.openapi.actionSystem.DataProvider import com.intellij.openapi.application.ApplicationManager @@ -62,6 +63,7 @@ class SessionSidePanelManager( existing } else create(ref) } + Telemetry.send("Session Opened", mapOf("source" to ref.type.name.lowercase(), "sessionId" to ref.id)) show(ui) } diff --git a/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/SessionUi.kt b/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/SessionUi.kt index 861ff21c044..c9a0ae28459 100644 --- a/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/SessionUi.kt +++ b/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/SessionUi.kt @@ -288,7 +288,7 @@ class SessionUi( prompt.model.favorites = { app.favorites.value } prompt.model.onFavoriteToggle = { item -> Telemetry.send( - "JetBrains Model Favorite Toggled", + "Model Favorite Toggled", mapOf("provider" to item.provider, "modelId" to item.id), ) app.toggleModelFavorite(item.provider, item.id) diff --git a/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/controller/SessionController.kt b/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/controller/SessionController.kt index 95a613b418d..2efee5f011f 100644 --- a/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/controller/SessionController.kt +++ b/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/controller/SessionController.kt @@ -215,10 +215,10 @@ class SessionController( session.id } sessions.prompt(id, directory, dto) - capture("Conversation Message", sessionProps(id) + mapOf("source" to "user", "hasExistingSession" to exists.toString())) + capture("Conversation Message", sessionProps(id) + mapOf("source" to "user", "hasExistingSession" to exists.toString()) + promptProps()) LOG.debug { "${ChatLogSummary.sid(id)} kind=prompt dispatched=true" } } catch (e: Exception) { - capture("JetBrains Session Error", sessionProps(sid ?: ref?.key ?: start) + mapOf("context" to "prompt", "errorClass" to e::class.java.name)) + capture("Session Error", sessionProps(sid ?: ref?.key ?: start) + mapOf("context" to "prompt", "errorClass" to e::class.java.name)) LOG.warn("${ChatLogSummary.sid(sid ?: ref?.key ?: start)} kind=prompt dir=${ChatLogSummary.dir(directory)} failed message=${e.message}", e) edt { if (disposed) return@edt @@ -238,10 +238,10 @@ class SessionController( cs.launch { try { sessions.abort(id, directory) - capture("JetBrains Session Stopped", sessionProps(id)) + capture("Session Stopped", sessionProps(id)) LOG.debug { "${ChatLogSummary.sid(id)} kind=abort ok=true" } } catch (e: Exception) { - capture("JetBrains Session Error", sessionProps(id) + mapOf("context" to "abort", "errorClass" to e::class.java.name)) + capture("Session Error", sessionProps(id) + mapOf("context" to "abort", "errorClass" to e::class.java.name)) LOG.warn("${ChatLogSummary.sid(id)} kind=abort dir=${ChatLogSummary.dir(directory)} failed message=${e.message}", e) } } @@ -250,7 +250,7 @@ class SessionController( fun setAutoApprove(value: Boolean) { assertEdt() KiloPluginSettings.setAutoApprove(value) - capture("JetBrains Auto Approve Toggled", mapOf("enabled" to value.toString())) + capture("Auto Approve Toggled", mapOf("enabled" to value.toString())) if (!value) { drainJob?.cancel() drainJob = null @@ -280,7 +280,7 @@ class SessionController( capture("Context Condensed", sessionProps(id) + mapOf("provider" to sel.providerID, "modelId" to sel.modelID)) LOG.debug { "${ChatLogSummary.sid(id)} kind=compact ok=true" } } catch (e: Exception) { - capture("JetBrains Session Error", sessionProps(id) + mapOf("context" to "compact", "errorClass" to e::class.java.name)) + capture("Session Error", sessionProps(id) + mapOf("context" to "compact", "errorClass" to e::class.java.name)) LOG.warn("${ChatLogSummary.sid(id)} kind=compact dir=${ChatLogSummary.dir(directory)} failed message=${e.message}", e) edt { updateModel { @@ -346,7 +346,7 @@ class SessionController( app.selectModel(agent, provider, id) selectResolvedModel(key) model.modelOverride = model.defaultModel != model.model - capture("JetBrains Model Selected", sessionProps() + mapOf("agent" to agent, "provider" to provider, "modelId" to id, "isOverride" to "true")) + capture("Model Selected", sessionProps() + mapOf("agent" to agent, "provider" to provider, "modelId" to id, "isOverride" to "true")) } fun clearModelOverride() { @@ -357,7 +357,7 @@ class SessionController( val auto = configModel(agent) ?: providerModel(agent) selectResolvedModel(auto) model.modelOverride = false - capture("JetBrains Model Override Cleared", sessionProps() + mapOf("agent" to agent)) + capture("Model Override Cleared", sessionProps() + mapOf("agent" to agent)) } fun selectVariant(value: String) { @@ -367,7 +367,7 @@ class SessionController( LOG.debug { "${ChatLogSummary.sid(sid ?: ref?.key ?: "pending")} kind=config variant=$key/$value" } app.selectVariant(key, value) model.variant = value - capture("JetBrains Reasoning Variant Selected", sessionProps() + mapOf("model" to key, "variant" to value)) + capture("Reasoning Variant Selected", sessionProps() + mapOf("model" to key, "variant" to value)) } // ------ permission / question resolution ------ @@ -391,7 +391,7 @@ class SessionController( )) LOG.debug { "${ChatLogSummary.sid(sid ?: ref?.key ?: "pending")} kind=permission rid=$requestId ok=true" } } catch (e: Exception) { - capture("JetBrains Session Error", sessionProps() + mapOf("context" to "permission", "errorClass" to e::class.java.name)) + capture("Session Error", sessionProps() + mapOf("context" to "permission", "errorClass" to e::class.java.name)) LOG.warn("${ChatLogSummary.sid(sid ?: ref?.key ?: "pending")} kind=permission rid=$requestId reply=${reply.reply} dir=${ChatLogSummary.dir(directory)} failed message=${e.message}", e) edt { updatePermission( @@ -429,7 +429,7 @@ class SessionController( model.setState(SessionState.Busy(KiloBundle.message("session.status.considering"))) } sessions.replyPermission(id, directory, PermissionReplyDto("once")) - capture("JetBrains Permission Auto Approved", sessionProps() + mapOf("tool" to restore().name, "source" to "single")) + capture("Permission Auto Approved", sessionProps() + mapOf("tool" to restore().name, "source" to "single")) LOG.debug { "${ChatLogSummary.sid(sid ?: ref?.key ?: "pending")} kind=permission-auto rid=$id ok=true" } } catch (e: Exception) { LOG.warn("${ChatLogSummary.sid(sid ?: ref?.key ?: "pending")} kind=permission-auto rid=$id dir=${ChatLogSummary.dir(directory)} failed message=${e.message}", e) @@ -471,7 +471,7 @@ class SessionController( for (request in permissions) { if (!autoApprove) return count sessions.replyPermission(request.id, directory, PermissionReplyDto("once")) - capture("JetBrains Permission Auto Approved", sessionProps(request.sessionID) + mapOf("tool" to request.permission, "source" to "drain")) + capture("Permission Auto Approved", sessionProps(request.sessionID) + mapOf("tool" to request.permission, "source" to "drain")) count++ } return count @@ -503,7 +503,7 @@ class SessionController( cs.launch { try { sessions.replyQuestion(requestId, directory, answers) - capture("JetBrains Question Answered", sessionProps() + mapOf( + capture("Question Answered", sessionProps() + mapOf( "requestId" to requestId, "answerCount" to answers.answers.size.toString(), "hasFollowupNewSession" to follow.toString(), @@ -523,7 +523,7 @@ class SessionController( cs.launch { try { sessions.rejectQuestion(requestId, directory) - capture("JetBrains Question Rejected", sessionProps() + mapOf("requestId" to requestId)) + capture("Question Rejected", sessionProps() + mapOf("requestId" to requestId)) LOG.debug { "${ChatLogSummary.sid(sid ?: ref?.key ?: "pending")} kind=question rid=$requestId ok=true" } } catch (e: Exception) { LOG.warn("${ChatLogSummary.sid(sid ?: ref?.key ?: "pending")} kind=question rid=$requestId rejected=true dir=${ChatLogSummary.dir(directory)} failed message=${e.message}", e) @@ -922,7 +922,7 @@ class SessionController( is ChatEventDto.SessionCreated -> adoptFollowup(event.info) is ChatEventDto.Error -> { - capture("JetBrains Session Error", sessionProps(event.sessionID) + mapOf("context" to "event", "errorClass" to (event.error?.type ?: "unknown"))) + capture("Session Error", sessionProps(event.sessionID) + mapOf("context" to "event", "errorClass" to (event.error?.type ?: "unknown"))) error(event, true) } @@ -1320,9 +1320,9 @@ class SessionController( cs.launch { try { app.setOrganization(org) - capture("JetBrains Organization Switched", mapOf("target" to if (org == null) "personal" else "organization")) + capture("Organization Switched", mapOf("target" to if (org == null) "personal" else "organization")) } catch (e: Exception) { - capture("JetBrains Account Connect Failed", mapOf("stage" to "organization", "errorClass" to e::class.java.name)) + capture("Account Connect Failed", mapOf("stage" to "organization", "errorClass" to e::class.java.name)) LOG.warn("account switch failed org=$org message=${e.message}", e) edt { if (disposed) return@edt @@ -1335,7 +1335,7 @@ class SessionController( fun openProfile() { assertEdt() - capture("JetBrains Profile Settings Opened", mapOf("surface" to "session_overlay")) + capture("Profile Settings Opened", mapOf("surface" to "session_overlay")) openProfileAction() } @@ -1351,6 +1351,18 @@ class SessionController( } } + private fun promptProps(): Map = buildMap { + model.agent?.let { put("agent", it) } + model.model?.let { key -> + put("model", key) + parseModel(key)?.let { sel -> + put("provider", sel.first) + put("modelId", sel.second) + } + } + model.variant?.takeIf { it in model.variants }?.let { put("variant", it) } + } + fun dismissLoginRequired() { assertEdt() loginRetry = null diff --git a/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/history/HistoryController.kt b/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/history/HistoryController.kt index d790daf3901..246190ef340 100644 --- a/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/history/HistoryController.kt +++ b/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/history/HistoryController.kt @@ -4,6 +4,7 @@ import ai.kilocode.client.app.KiloSessionService import ai.kilocode.client.app.Workspace import ai.kilocode.client.plugin.KiloBundle import ai.kilocode.client.session.SessionRef +import ai.kilocode.client.telemetry.Telemetry import ai.kilocode.rpc.dto.CloudSessionDto import ai.kilocode.rpc.dto.SessionDto import com.intellij.openapi.application.ApplicationManager @@ -103,6 +104,7 @@ class HistoryController( edt { deleting.remove(item.id) local.remove(item.id) + Telemetry.send("History Session Deleted", mapOf("sessionId" to item.id)) deleted(item.id) } } catch (e: Exception) { @@ -120,7 +122,10 @@ class HistoryController( cs.launch { try { val updated = sessions.renameSession(item.id, dir, title) - edt { local.update(LocalHistoryItem(updated)) } + edt { + local.update(LocalHistoryItem(updated)) + Telemetry.send("History Session Renamed", mapOf("sessionId" to item.id)) + } } catch (e: Exception) { edt { local.fail(e.message ?: KiloBundle.message("history.error.local.rename")) } } diff --git a/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/settings/profile/ProfileUi.kt b/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/settings/profile/ProfileUi.kt index 4084ccbc242..110a695264c 100644 --- a/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/settings/profile/ProfileUi.kt +++ b/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/settings/profile/ProfileUi.kt @@ -55,7 +55,7 @@ internal class ProfileUi( ) private val account = LoggedInProfileUi( dashboard = { - telemetry("JetBrains Dashboard Opened", mapOf("surface" to "settings")) + telemetry("Dashboard Opened", mapOf("surface" to "settings")) browse(DASHBOARD_URL) }, logout = ::logout, @@ -206,7 +206,7 @@ internal class ProfileUi( } catch (e: CancellationException) { throw e } catch (e: Exception) { - telemetry("JetBrains Account Connect Failed", mapOf("stage" to "complete", "errorClass" to e::class.java.name)) + telemetry("Account Connect Failed", mapOf("stage" to "complete", "errorClass" to e::class.java.name)) withContext(edt) { if (id != attempt) return@withContext login = LoginState.Error(compactLoginError(e)) @@ -219,7 +219,7 @@ internal class ProfileUi( private fun cancel() { attempt++ login = LoginState.Idle - telemetry("JetBrains Account Connect Failed", mapOf("stage" to "cancel", "errorClass" to "cancelled")) + telemetry("Account Connect Failed", mapOf("stage" to "cancel", "errorClass" to "cancelled")) sync() } @@ -248,7 +248,7 @@ internal class ProfileUi( cs.launch { try { val profile = app.setOrganization(org) - telemetry("JetBrains Organization Switched", mapOf("target" to if (org == null) "personal" else "organization")) + telemetry("Organization Switched", mapOf("target" to if (org == null) "personal" else "organization")) val state = app.state.value withContext(edt) { update(profile ?: state.profile, state.status) From bfe3ae9e3b16a1ef196a88b46d938a10ef217a48 Mon Sep 17 00:00:00 2001 From: kirillk Date: Fri, 29 May 2026 14:49:32 -0400 Subject: [PATCH 03/16] feat(jetbrains): report cli connection failures --- .../backend/app/KiloBackendAppService.kt | 10 ++++-- .../app/KiloBackendConnectionService.kt | 34 +++++++++++++++++-- .../backend/cli/KiloBackendCliManager.kt | 21 ++++++++++++ .../kilocode/backend/rpc/KiloAppRpcApiImpl.kt | 4 +++ .../telemetry/KiloCliStartupTelemetry.kt | 14 ++++++-- .../session/controller/SessionController.kt | 9 +++++ .../controller/SessionControllerTestBase.kt | 2 ++ .../client/session/ui/ConnectionPanelTest.kt | 2 ++ 8 files changed, 89 insertions(+), 7 deletions(-) diff --git a/packages/kilo-jetbrains/backend/src/main/kotlin/ai/kilocode/backend/app/KiloBackendAppService.kt b/packages/kilo-jetbrains/backend/src/main/kotlin/ai/kilocode/backend/app/KiloBackendAppService.kt index 39fb3ad513a..97b1d7df4e3 100644 --- a/packages/kilo-jetbrains/backend/src/main/kotlin/ai/kilocode/backend/app/KiloBackendAppService.kt +++ b/packages/kilo-jetbrains/backend/src/main/kotlin/ai/kilocode/backend/app/KiloBackendAppService.kt @@ -5,6 +5,9 @@ import ai.kilocode.backend.cli.KiloBackendCliManager import ai.kilocode.backend.migration.KiloBackendLegacyMigrationStoreService import ai.kilocode.backend.migration.LegacyMigrationDetection import ai.kilocode.backend.telemetry.KiloBackendTelemetry +import ai.kilocode.backend.telemetry.CliStartupTelemetry +import ai.kilocode.backend.telemetry.KiloCliStartupTelemetry +import ai.kilocode.backend.telemetry.NoopCliStartupTelemetry import ai.kilocode.log.KiloLog import ai.kilocode.backend.workspace.KiloBackendWorkspaceManager import ai.kilocode.jetbrains.api.client.DefaultApi @@ -75,6 +78,7 @@ class KiloBackendAppService private constructor( private val server: CliServer, private val log: KiloLog, private val loadTimeoutMs: Long, + private val telemetry: CliStartupTelemetry, ) : Disposable { /** IntelliJ service injection entry point. */ @@ -83,6 +87,7 @@ class KiloBackendAppService private constructor( KiloBackendCliManager(), KiloLog.create(KiloBackendAppService::class.java), APP_LOAD_TIMEOUT_MS, + KiloCliStartupTelemetry(), ) companion object { @@ -96,13 +101,14 @@ class KiloBackendAppService private constructor( server: CliServer, log: KiloLog, loadTimeoutMs: Long = APP_LOAD_TIMEOUT_MS, - ) = KiloBackendAppService(cs, server, log, loadTimeoutMs) + telemetry: CliStartupTelemetry = NoopCliStartupTelemetry, + ) = KiloBackendAppService(cs, server, log, loadTimeoutMs, telemetry) } private val mutex = Mutex() private val connection = KiloConnectionService(cs, server, onReconnect = { cs.launch { reconnect() } - }, appLoadTimeoutMs = loadTimeoutMs, log = log) + }, appLoadTimeoutMs = loadTimeoutMs, log = log, telemetry = telemetry) private var watcher: Job? = null private var eventWatcher: Job? = null diff --git a/packages/kilo-jetbrains/backend/src/main/kotlin/ai/kilocode/backend/app/KiloBackendConnectionService.kt b/packages/kilo-jetbrains/backend/src/main/kotlin/ai/kilocode/backend/app/KiloBackendConnectionService.kt index 4eccd0b4c74..8a3b8e99aa9 100644 --- a/packages/kilo-jetbrains/backend/src/main/kotlin/ai/kilocode/backend/app/KiloBackendConnectionService.kt +++ b/packages/kilo-jetbrains/backend/src/main/kotlin/ai/kilocode/backend/app/KiloBackendConnectionService.kt @@ -3,6 +3,8 @@ package ai.kilocode.backend.app import ai.kilocode.backend.cli.KiloBackendHttpClients import ai.kilocode.backend.cli.KiloCliDataParser import ai.kilocode.backend.cli.CliServer +import ai.kilocode.backend.telemetry.CliStartupTelemetry +import ai.kilocode.backend.telemetry.NoopCliStartupTelemetry import ai.kilocode.log.ChatLogSummary import ai.kilocode.log.KiloLog import ai.kilocode.jetbrains.api.client.DefaultApi @@ -62,20 +64,29 @@ class KiloConnectionService( private val onReconnect: () -> Unit, private val log: KiloLog, private val appLoadTimeoutMs: Long, + private val telemetry: CliStartupTelemetry, ) { constructor( cs: CoroutineScope, server: CliServer, onReconnect: () -> Unit, - ) : this(cs, server, onReconnect, KiloLog.create(KiloConnectionService::class.java), 30_000L) + ) : this(cs, server, onReconnect, KiloLog.create(KiloConnectionService::class.java), 30_000L, NoopCliStartupTelemetry) constructor( cs: CoroutineScope, server: CliServer, onReconnect: () -> Unit, log: KiloLog, - ) : this(cs, server, onReconnect, log, 30_000L) + ) : this(cs, server, onReconnect, log, 30_000L, NoopCliStartupTelemetry) + + constructor( + cs: CoroutineScope, + server: CliServer, + onReconnect: () -> Unit, + log: KiloLog, + telemetry: CliStartupTelemetry, + ) : this(cs, server, onReconnect, log, 30_000L, telemetry) companion object { private const val HEARTBEAT_TIMEOUT_MS = 15_000L @@ -261,6 +272,12 @@ class KiloConnectionService( } else { log.warn("SSE: failure (HTTP ${response?.code}) — scheduling reconnect") } + report("sse", mapOf( + "message" to (t?.message ?: "SSE connection failed (HTTP ${response?.code})"), + "details" to detail.orEmpty(), + "httpStatus" to (response?.code?.toString() ?: ""), + "errorClass" to (t?.javaClass?.name ?: ""), + )) setState(ConnectionState.Error(t?.message ?: "SSE connection failed (HTTP ${response?.code})", detail)) scheduleReconnect() } @@ -302,6 +319,7 @@ class KiloConnectionService( val elapsed = System.currentTimeMillis() - lastEvent.get() if (elapsed > HEARTBEAT_TIMEOUT_MS) { log.warn("SSE: heartbeat timeout (${elapsed}ms) — forcing reconnect") + report("heartbeat", mapOf("elapsedMs" to elapsed.toString())) source.getAndSet(null)?.cancel() scheduleReconnect() } @@ -316,6 +334,7 @@ class KiloConnectionService( val ok = checkHealth() if (!ok && _state.value is ConnectionState.Connected) { log.warn("Health check failed — forcing SSE reconnect") + report("health", mapOf("message" to "Health check failed")) source.getAndSet(null)?.cancel() scheduleReconnect() } @@ -341,6 +360,7 @@ class KiloConnectionService( server.exited(proc) val code = proc.exitValue() log.warn("CLI process exited with code $code") + report("process", mapOf("exitCode" to code.toString(), "message" to "CLI process exited")) source.getAndSet(null)?.cancel() setState(ConnectionState.Error("CLI process exited with code $code")) scheduleReconnect() @@ -362,6 +382,16 @@ class KiloConnectionService( _state.value = next } + private fun report(stage: String, props: Map) { + cs.launch { + telemetry.report("CLI Connection Failed", base() + props + mapOf("stage" to stage)) + } + } + + private fun base(): Map = buildMap { + put("port", port.toString()) + } + fun dispose() { disposed = true source.getAndSet(null)?.cancel() diff --git a/packages/kilo-jetbrains/backend/src/main/kotlin/ai/kilocode/backend/cli/KiloBackendCliManager.kt b/packages/kilo-jetbrains/backend/src/main/kotlin/ai/kilocode/backend/cli/KiloBackendCliManager.kt index 56647cbb3d3..3a465edfcb5 100644 --- a/packages/kilo-jetbrains/backend/src/main/kotlin/ai/kilocode/backend/cli/KiloBackendCliManager.kt +++ b/packages/kilo-jetbrains/backend/src/main/kotlin/ai/kilocode/backend/cli/KiloBackendCliManager.kt @@ -50,9 +50,12 @@ class KiloBackendCliManager( @Volatile override var forceExtract = false + private var startupReported = false + override fun process(): Process? = process override suspend fun init(): CliServer.State { + startupReported = false return try { val path = extractCli() log.info("CLI binary path: ${path.absolutePath} (size=${path.length()} bytes)") @@ -61,6 +64,7 @@ class KiloBackendCliManager( } } catch (e: Exception) { log.warn("CLI startup failed", e) + if (!startupReported) reportStartupException(e) process?.let { proc -> log.info("Cleaning up orphaned CLI process (pid=${proc.pid()})") process = null @@ -257,6 +261,7 @@ class KiloBackendCliManager( details: String?, error: Exception?, ) { + startupReported = true startup.report(buildMap { if (code != null) put("exitCode", code.toString()) put("stdout", stdout) @@ -276,6 +281,22 @@ class KiloBackendCliManager( }) } + private suspend fun reportStartupException(error: Exception) { + startupReported = true + val env = ideEnv() + startup.report(buildMap { + put("details", error.stackTraceToString()) + put("errorClass", error::class.java.name) + put("message", error.message.orEmpty()) + runCatching { put("platform", platform()) } + put("arch", CpuArch.CURRENT.name) + env["KILO_APP_VERSION"]?.let { put("pluginVersion", it) } + env["KILO_APP_VERSION"]?.let { put("appVersion", it) } + env["KILO_EDITOR_NAME"]?.let { put("editorName", it) } + env["KILO_MACHINE_ID"]?.let { put("machineId", it) } + }) + } + override fun dispose() { val proc = process ?: return process = null diff --git a/packages/kilo-jetbrains/backend/src/main/kotlin/ai/kilocode/backend/rpc/KiloAppRpcApiImpl.kt b/packages/kilo-jetbrains/backend/src/main/kotlin/ai/kilocode/backend/rpc/KiloAppRpcApiImpl.kt index 668db20b290..5795c451205 100644 --- a/packages/kilo-jetbrains/backend/src/main/kotlin/ai/kilocode/backend/rpc/KiloAppRpcApiImpl.kt +++ b/packages/kilo-jetbrains/backend/src/main/kotlin/ai/kilocode/backend/rpc/KiloAppRpcApiImpl.kt @@ -5,6 +5,7 @@ package ai.kilocode.backend.rpc import ai.kilocode.backend.app.KiloAppState import ai.kilocode.backend.app.KiloBackendAppService import ai.kilocode.backend.telemetry.KiloBackendTelemetry +import ai.kilocode.backend.telemetry.KiloCliStartupTelemetry import ai.kilocode.backend.app.ConfigWarning import ai.kilocode.backend.app.LoadError import ai.kilocode.backend.app.LoadProgress @@ -98,6 +99,9 @@ class KiloAppRpcApiImpl : KiloAppRpcApi { override suspend fun captureTelemetry(capture: TelemetryCaptureDto) { service().capture(app.http, app.port, capture.event, capture.properties) + if ((app.http == null || app.port <= 0) && capture.event == "Connection Retry Clicked") { + KiloCliStartupTelemetry().report(capture.event, capture.properties) + } } private fun dto(state: KiloAppState): KiloAppStateDto = diff --git a/packages/kilo-jetbrains/backend/src/main/kotlin/ai/kilocode/backend/telemetry/KiloCliStartupTelemetry.kt b/packages/kilo-jetbrains/backend/src/main/kotlin/ai/kilocode/backend/telemetry/KiloCliStartupTelemetry.kt index 5cdfe44efea..f385f193148 100644 --- a/packages/kilo-jetbrains/backend/src/main/kotlin/ai/kilocode/backend/telemetry/KiloCliStartupTelemetry.kt +++ b/packages/kilo-jetbrains/backend/src/main/kotlin/ai/kilocode/backend/telemetry/KiloCliStartupTelemetry.kt @@ -18,7 +18,15 @@ import okhttp3.RequestBody.Companion.toRequestBody import java.util.concurrent.TimeUnit interface CliStartupTelemetry { - suspend fun report(properties: Map) + suspend fun report(properties: Map) { + report("CLI Startup Failed", properties) + } + + suspend fun report(event: String, properties: Map) +} + +object NoopCliStartupTelemetry : CliStartupTelemetry { + override suspend fun report(event: String, properties: Map) = Unit } class KiloCliStartupTelemetry( @@ -28,14 +36,14 @@ class KiloCliStartupTelemetry( private val url: String = "https://us.i.posthog.com/capture/", private val log: KiloLog = KiloLog.create(KiloCliStartupTelemetry::class.java), ) : CliStartupTelemetry { - override suspend fun report(properties: Map) { + override suspend fun report(event: String, properties: Map) { withContext(Dispatchers.IO) { try { val props = base() + properties val body = JsonObject( mapOf( "api_key" to JsonPrimitive("phc_GK2Pxl0HPj5ZPfwhLRjXrtdz8eD7e9MKnXiFrOqnB6z"), - "event" to JsonPrimitive("CLI Startup Failed"), + "event" to JsonPrimitive(event), "distinct_id" to JsonPrimitive(props["machineId"] ?: "jetbrains-unknown"), "properties" to JsonObject(props.mapValues { JsonPrimitive(it.value) }), ), diff --git a/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/controller/SessionController.kt b/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/controller/SessionController.kt index 2efee5f011f..c7ae050d32d 100644 --- a/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/controller/SessionController.kt +++ b/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/controller/SessionController.kt @@ -296,6 +296,7 @@ class SessionController( LOG.debug { "${ChatLogSummary.sid(sid ?: ref?.key ?: "pending")} kind=connection-retry app=${model.app.status} workspace=${model.workspace.status}" } + capture("Connection Retry Clicked", connectionProps()) setConnectionTargetState(SessionControllerEvent.ConnectionChanged.ShowConnecting) setVisibleConnectionState(SessionControllerEvent.ConnectionChanged.ShowConnecting) // App retry policy is backend-owned and may escalate from lightweight refresh to restart. @@ -1363,6 +1364,14 @@ class SessionController( model.variant?.takeIf { it in model.variants }?.let { put("variant", it) } } + private fun connectionProps(): Map = buildMap { + put("appStatus", model.app.status.name) + put("workspaceStatus", model.workspace.status.name) + model.app.error?.let { put("appError", it) } + model.workspace.error?.let { put("workspaceError", it) } + put("warningCount", model.app.warnings.size.toString()) + } + fun dismissLoginRequired() { assertEdt() loginRetry = null diff --git a/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/session/controller/SessionControllerTestBase.kt b/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/session/controller/SessionControllerTestBase.kt index 4b0a377c6d7..4149ad5313f 100644 --- a/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/session/controller/SessionControllerTestBase.kt +++ b/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/session/controller/SessionControllerTestBase.kt @@ -27,6 +27,7 @@ import ai.kilocode.rpc.dto.ProviderDto import ai.kilocode.rpc.dto.ProvidersDto import ai.kilocode.rpc.dto.SessionDto import ai.kilocode.rpc.dto.SessionTimeDto +import ai.kilocode.rpc.dto.TelemetryCaptureDto import com.intellij.openapi.Disposable import com.intellij.openapi.application.ApplicationManager import com.intellij.openapi.util.Disposer @@ -167,6 +168,7 @@ abstract class SessionControllerTestBase : BasePlatformTestCase() { open = open, beforeUpdate = beforeUpdate, afterUpdate = afterUpdate, + telemetry = { event, props -> appRpc.telemetry.add(TelemetryCaptureDto(event, props)) }, ) controllers.add(m) roots[m] = root diff --git a/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/session/ui/ConnectionPanelTest.kt b/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/session/ui/ConnectionPanelTest.kt index 16536b70276..7507db6943f 100644 --- a/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/session/ui/ConnectionPanelTest.kt +++ b/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/session/ui/ConnectionPanelTest.kt @@ -91,6 +91,8 @@ class ConnectionPanelTest : SessionControllerTestBase() { flush() assertEquals(1, appRpc.retries) + assertEquals("Connection Retry Clicked", appRpc.telemetry.last().event) + assertEquals("ERROR", appRpc.telemetry.last().properties["appStatus"]) } fun `test ready warnings show collapsed banner with retry`() { From d97348757ca4fc8e962f051b5d30da623575fa15 Mon Sep 17 00:00:00 2001 From: kirillk Date: Fri, 29 May 2026 15:13:38 -0400 Subject: [PATCH 04/16] fix(jetbrains): enable rc file logging --- .../backend/cli/KiloBackendCliManager.kt | 10 ++++----- .../backend/telemetry/KiloBackendTelemetry.kt | 5 ++--- .../telemetry/KiloCliStartupTelemetry.kt | 5 ++--- .../src/main/kotlin/ai/kilocode/KiloPlugin.kt | 22 +++++++++++++++++++ .../main/kotlin/ai/kilocode/log/KiloLog.kt | 16 +++++++++----- 5 files changed, 40 insertions(+), 18 deletions(-) create mode 100644 packages/kilo-jetbrains/shared/src/main/kotlin/ai/kilocode/KiloPlugin.kt diff --git a/packages/kilo-jetbrains/backend/src/main/kotlin/ai/kilocode/backend/cli/KiloBackendCliManager.kt b/packages/kilo-jetbrains/backend/src/main/kotlin/ai/kilocode/backend/cli/KiloBackendCliManager.kt index 3a465edfcb5..c1f5c3ee2a4 100644 --- a/packages/kilo-jetbrains/backend/src/main/kotlin/ai/kilocode/backend/cli/KiloBackendCliManager.kt +++ b/packages/kilo-jetbrains/backend/src/main/kotlin/ai/kilocode/backend/cli/KiloBackendCliManager.kt @@ -1,13 +1,12 @@ package ai.kilocode.backend.cli -import ai.kilocode.log.KiloLog +import ai.kilocode.KiloPlugin +import ai.kilocode.backend.dev.KiloDevMode import ai.kilocode.backend.telemetry.CliStartupTelemetry import ai.kilocode.backend.telemetry.KiloCliStartupTelemetry -import ai.kilocode.backend.dev.KiloDevMode -import com.intellij.ide.plugins.PluginManagerCore +import ai.kilocode.log.KiloLog import com.intellij.openapi.application.ApplicationInfo import com.intellij.openapi.application.PathManager -import com.intellij.openapi.extensions.PluginId import com.intellij.openapi.util.SystemInfo import com.intellij.util.system.CpuArch import kotlinx.coroutines.Dispatchers @@ -367,8 +366,7 @@ class KiloBackendCliManager( }.onFailure { log.info("Could not read ApplicationInfo: ${it.message}") } runCatching { - val version = PluginManagerCore - .getPlugin(PluginId.getId("ai.kilocode.jetbrains"))?.version + val version = KiloPlugin.version() if (version != null) put("KILO_APP_VERSION", version) }.onFailure { log.info("Could not read plugin version: ${it.message}") } diff --git a/packages/kilo-jetbrains/backend/src/main/kotlin/ai/kilocode/backend/telemetry/KiloBackendTelemetry.kt b/packages/kilo-jetbrains/backend/src/main/kotlin/ai/kilocode/backend/telemetry/KiloBackendTelemetry.kt index 835f3984edf..af9829ab3fd 100644 --- a/packages/kilo-jetbrains/backend/src/main/kotlin/ai/kilocode/backend/telemetry/KiloBackendTelemetry.kt +++ b/packages/kilo-jetbrains/backend/src/main/kotlin/ai/kilocode/backend/telemetry/KiloBackendTelemetry.kt @@ -1,11 +1,10 @@ package ai.kilocode.backend.telemetry +import ai.kilocode.KiloPlugin import ai.kilocode.backend.dev.KiloDevMode import ai.kilocode.log.KiloLog -import com.intellij.ide.plugins.PluginManagerCore import com.intellij.openapi.application.ApplicationInfo import com.intellij.openapi.components.Service -import com.intellij.openapi.extensions.PluginId import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.withContext import kotlinx.serialization.json.JsonObject @@ -72,7 +71,7 @@ class KiloBackendTelemetry( put("jetbrainsBuild", JsonPrimitive(info.build.asString())) }.onFailure { log.info("Could not read ApplicationInfo for telemetry: ${it.message}") } runCatching { - val version = PluginManagerCore.getPlugin(PluginId.getId("ai.kilocode.jetbrains"))?.version + val version = KiloPlugin.version() if (version != null) { put("pluginVersion", JsonPrimitive(version)) put("appVersion", JsonPrimitive(version)) diff --git a/packages/kilo-jetbrains/backend/src/main/kotlin/ai/kilocode/backend/telemetry/KiloCliStartupTelemetry.kt b/packages/kilo-jetbrains/backend/src/main/kotlin/ai/kilocode/backend/telemetry/KiloCliStartupTelemetry.kt index f385f193148..78596d95949 100644 --- a/packages/kilo-jetbrains/backend/src/main/kotlin/ai/kilocode/backend/telemetry/KiloCliStartupTelemetry.kt +++ b/packages/kilo-jetbrains/backend/src/main/kotlin/ai/kilocode/backend/telemetry/KiloCliStartupTelemetry.kt @@ -1,10 +1,9 @@ package ai.kilocode.backend.telemetry +import ai.kilocode.KiloPlugin import ai.kilocode.backend.dev.KiloDevMode import ai.kilocode.log.KiloLog -import com.intellij.ide.plugins.PluginManagerCore import com.intellij.openapi.application.ApplicationInfo -import com.intellij.openapi.extensions.PluginId import com.intellij.openapi.util.SystemInfo import com.intellij.util.system.CpuArch import kotlinx.coroutines.Dispatchers @@ -78,7 +77,7 @@ class KiloCliStartupTelemetry( put("jetbrainsBuild", info.build.asString()) }.onFailure { log.info("Could not read ApplicationInfo for startup telemetry: ${it.message}") } runCatching { - val version = PluginManagerCore.getPlugin(PluginId.getId("ai.kilocode.jetbrains"))?.version + val version = KiloPlugin.version() if (version != null) { put("pluginVersion", version) put("appVersion", version) diff --git a/packages/kilo-jetbrains/shared/src/main/kotlin/ai/kilocode/KiloPlugin.kt b/packages/kilo-jetbrains/shared/src/main/kotlin/ai/kilocode/KiloPlugin.kt new file mode 100644 index 00000000000..851c7a811d7 --- /dev/null +++ b/packages/kilo-jetbrains/shared/src/main/kotlin/ai/kilocode/KiloPlugin.kt @@ -0,0 +1,22 @@ +package ai.kilocode + +import com.intellij.ide.plugins.cl.PluginAwareClassLoader +import com.intellij.openapi.extensions.PluginDescriptor +import com.intellij.openapi.extensions.PluginId + +object KiloPlugin { + const val ID = "ai.kilocode.jetbrains" + + val id: PluginId = PluginId.getId(ID) + + fun descriptor(): PluginDescriptor? { + val loader = KiloPlugin::class.java.classLoader as? PluginAwareClassLoader ?: return null + val descriptor = loader.pluginDescriptor + if (descriptor.pluginId != id) return null + return descriptor + } + + fun version() = descriptor()?.version + + fun isRc() = version()?.contains("-rc.", ignoreCase = true) == true +} diff --git a/packages/kilo-jetbrains/shared/src/main/kotlin/ai/kilocode/log/KiloLog.kt b/packages/kilo-jetbrains/shared/src/main/kotlin/ai/kilocode/log/KiloLog.kt index d76dc9e9df5..ddefc89dafc 100644 --- a/packages/kilo-jetbrains/shared/src/main/kotlin/ai/kilocode/log/KiloLog.kt +++ b/packages/kilo-jetbrains/shared/src/main/kotlin/ai/kilocode/log/KiloLog.kt @@ -1,5 +1,6 @@ package ai.kilocode.log +import ai.kilocode.KiloPlugin import com.intellij.openapi.application.PathManager import com.intellij.openapi.diagnostic.Logger import java.io.PrintWriter @@ -22,9 +23,9 @@ import java.util.logging.LogRecord * which writes to the standard IDE log file. * * In sandbox mode (i.e. when running via `./gradlew runIde`, detected via the `idea.plugin.in.sandbox.mode` - * system property), output is additionally written to a `kilo-dev.log` file inside the sandbox log directory. - * This makes it easy to tail frontend and backend logs side-by-side during development without opening the - * IDE's own log viewer. + * system property), or in RC plugin builds, output is additionally written to a `kilo-dev.log` file inside the + * IDE log directory. This makes it easy to tail frontend and backend logs side-by-side during development or + * RC validation without opening the IDE's own log viewer. * * Usage: * ```kotlin @@ -46,13 +47,16 @@ interface KiloLog { fun error(msg: String, t: Throwable? = null) companion object { - private val sandbox = System.getProperty("idea.plugin.in.sandbox.mode", "false").toBoolean() - fun create(cls: Class<*>): KiloLog { val intellij = IntellijLog(cls) - if (!sandbox) return intellij + if (!fileEnabled()) return intellij return CompositeLog(intellij, FileLog(cls)) } + + private fun fileEnabled(): Boolean { + if (System.getProperty("idea.plugin.in.sandbox.mode", "false").toBoolean()) return true + return runCatching { KiloPlugin.isRc() }.getOrDefault(false) + } } } From 0aaff8956570e76f319abc14550dbdb82a32010a Mon Sep 17 00:00:00 2001 From: kirillk Date: Fri, 29 May 2026 15:23:08 -0400 Subject: [PATCH 05/16] docs(jetbrains): update agent guidance --- packages/kilo-jetbrains/AGENTS.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/packages/kilo-jetbrains/AGENTS.md b/packages/kilo-jetbrains/AGENTS.md index e96a2572711..ea07fc45eee 100644 --- a/packages/kilo-jetbrains/AGENTS.md +++ b/packages/kilo-jetbrains/AGENTS.md @@ -20,6 +20,8 @@ When looking for IntelliJ Platform API usage, implementation examples, extension points, services, actions, inspections, PSI/VFS/editor behavior, or plugin patterns, prefer real IntelliJ source code over Gradle caches, downloaded jars, generated parser artifacts, or decompiled classes. +Do not use IntelliJ Platform APIs marked as internal in the IntelliJ source repository. Find a public API alternative or keep the integration behind supported extension points. Experimental APIs are acceptable when needed, but warn the user that the integration relies on an experimental IntelliJ API. + Use this priority order: 1. Check whether `$INTELLIJ_REPO` is set and points to a readable IntelliJ Community checkout. @@ -226,6 +228,7 @@ Rules: ### Primary UI Rules - Use IntelliJ platform components instead of raw Swing where an equivalent exists (see [Platform Components](#platform-components-and-utilities) table below). +- When implementing a new user-facing action, consider adding metrics so usage can be tracked. - Do not set default Swing properties explicitly. Avoid `isOpaque = false` unless the component default differs or there is a documented rendering reason. - Avoid hardcoded dimensions, colors, and font sizes — use the platform style APIs described in [Theme-Derived Colors](#theme-derived-colors), [Theme-Derived Fonts](#theme-derived-fonts), and [Borders, Insets, and Spacing](#borders-insets-and-spacing). - Put user-visible strings in `*.properties` files. From f0200f9224d36d2701442d357dce026740ff6783 Mon Sep 17 00:00:00 2001 From: kirillk Date: Fri, 29 May 2026 15:58:43 -0400 Subject: [PATCH 06/16] feat(jetbrains): expand plugin telemetry coverage --- .../backend/app/KiloBackendAppService.kt | 48 ++++++++++++++++++- .../backend/cli/KiloBackendCliManager.kt | 11 ++++- .../kilocode/client/KiloToolWindowFactory.kt | 6 ++- .../client/actions/DeleteSessionAction.kt | 6 ++- .../client/actions/RenameSessionAction.kt | 17 ++++++- .../session/controller/SessionController.kt | 24 +++++++++- .../session/history/HistoryController.kt | 40 +++++++++++++++- .../session/controller/PromptLifecycleTest.kt | 9 ++++ .../session/controller/TurnLifecycleTest.kt | 6 +++ .../session/history/HistoryControllerTest.kt | 34 +++++++++++++ 10 files changed, 191 insertions(+), 10 deletions(-) diff --git a/packages/kilo-jetbrains/backend/src/main/kotlin/ai/kilocode/backend/app/KiloBackendAppService.kt b/packages/kilo-jetbrains/backend/src/main/kotlin/ai/kilocode/backend/app/KiloBackendAppService.kt index 97b1d7df4e3..94aea5c6836 100644 --- a/packages/kilo-jetbrains/backend/src/main/kotlin/ai/kilocode/backend/app/KiloBackendAppService.kt +++ b/packages/kilo-jetbrains/backend/src/main/kotlin/ai/kilocode/backend/app/KiloBackendAppService.kt @@ -272,12 +272,14 @@ class KiloBackendAppService private constructor( loader?.cancel() eventWatcher?.cancel() loader = cs.launch { + val start = System.currentTimeMillis() log.info("Application starting — loading config, profile, notifications") val progress = AtomicReference(LoadProgress()) _appState.value = KiloAppState.Loading(progress.get()) val migration = detectMigration() if (migration != null) { + captureLoad("Backend Migration Required", start, mapOf("migrationRequired" to "true")) stopRuntime() profile = null config = null @@ -350,8 +352,13 @@ class KiloBackendAppService private constructor( sessions.start(connection.api!!, connection.apiClient!!, connection.port, connection.events) chat.start(connection.apiClient!!, connection.port, connection.events) workspaces.start(connection.api!!, connection.apiClient!!, connection.port, connection.events) - service().setEnabled(connection.apiClient, connection.port, true) - service().capture(connection.apiClient, connection.port, "Backend Connected", mapOf("portKnown" to "true")) + setTelemetry(true) + captureBackend("Backend Connected", mapOf("portKnown" to "true")) + captureLoad("Backend Load Completed", start, mapOf( + "profileStatus" to if (prof != null) "loaded" else "not_logged_in", + "warningCount" to warns.size.toString(), + "migrationRequired" to "false", + )) setAppReady( AppData( profile = prof, @@ -368,6 +375,11 @@ class KiloBackendAppService private constructor( detail = "Timed out loading app data after ${loadTimeoutMs}ms", ) log.warn("Application start timed out after ${loadTimeoutMs}ms") + captureLoad("Backend Load Failed", start, mapOf( + "errorCount" to (errors.size + 1).toString(), + "resources" to (errors.map { it.resource } + err.resource).distinct().joinToString(","), + "reason" to "timeout", + )) setAppError( message = "Failed to load required data", errors = errors.toList() + err, @@ -376,6 +388,11 @@ class KiloBackendAppService private constructor( throw e } catch (e: Exception) { log.warn("Application start failed: ${e.message}") + captureLoad("Backend Load Failed", start, mapOf( + "errorCount" to errors.size.toString(), + "resources" to errors.map { it.resource }.distinct().joinToString(","), + "reason" to e::class.java.name, + )) setAppError( message = "Failed to load required data", errors = errors.toList(), @@ -385,6 +402,33 @@ class KiloBackendAppService private constructor( } } + private fun captureLoad(event: String, start: Long, props: Map) { + val http = connection.apiClient + val port = connection.port + cs.launch { + runCatching { + service().capture( + http, + port, + event, + props + mapOf("durationMs" to (System.currentTimeMillis() - start).toString()), + ) + }.onFailure { log.info("Skipping backend load telemetry: ${it.message}") } + } + } + + private suspend fun setTelemetry(enabled: Boolean) { + runCatching { + service().setEnabled(connection.apiClient, connection.port, enabled) + }.onFailure { log.info("Skipping telemetry setEnabled: ${it.message}") } + } + + private suspend fun captureBackend(event: String, props: Map) { + runCatching { + service().capture(connection.apiClient, connection.port, event, props) + }.onFailure { log.info("Skipping backend telemetry: ${it.message}") } + } + private suspend fun detectMigration(): LegacyMigrationDetection? = withContext(Dispatchers.IO) { val http = connection.apiClient ?: run { log.info("Migration check: skipped because CLI HTTP client is not connected") diff --git a/packages/kilo-jetbrains/backend/src/main/kotlin/ai/kilocode/backend/cli/KiloBackendCliManager.kt b/packages/kilo-jetbrains/backend/src/main/kotlin/ai/kilocode/backend/cli/KiloBackendCliManager.kt index c1f5c3ee2a4..11ed07bcea5 100644 --- a/packages/kilo-jetbrains/backend/src/main/kotlin/ai/kilocode/backend/cli/KiloBackendCliManager.kt +++ b/packages/kilo-jetbrains/backend/src/main/kotlin/ai/kilocode/backend/cli/KiloBackendCliManager.kt @@ -55,12 +55,21 @@ class KiloBackendCliManager( override suspend fun init(): CliServer.State { startupReported = false + val start = System.currentTimeMillis() return try { val path = extractCli() log.info("CLI binary path: ${path.absolutePath} (size=${path.length()} bytes)") - withTimeout(STARTUP_TIMEOUT_MS) { + val state = withTimeout(STARTUP_TIMEOUT_MS) { spawn(path) } + if (state is CliServer.State.Ready) { + startup.report("CLI Startup Succeeded", mapOf( + "durationMs" to (System.currentTimeMillis() - start).toString(), + "platform" to platform(), + "arch" to CpuArch.CURRENT.name, + )) + } + state } catch (e: Exception) { log.warn("CLI startup failed", e) if (!startupReported) reportStartupException(e) diff --git a/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/KiloToolWindowFactory.kt b/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/KiloToolWindowFactory.kt index 787b829769d..b3e083f0fb0 100644 --- a/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/KiloToolWindowFactory.kt +++ b/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/KiloToolWindowFactory.kt @@ -33,6 +33,7 @@ class KiloToolWindowFactory : ToolWindowFactory, DumbAware { } override fun createToolWindowContent(project: Project, toolWindow: ToolWindow) { + val start = System.currentTimeMillis() try { val workspaces = service() val cs = CoroutineScope(SupervisorJob()) @@ -44,7 +45,10 @@ class KiloToolWindowFactory : ToolWindowFactory, DumbAware { withContext(Dispatchers.Main) { setup(project, toolWindow, workspace) } - Telemetry.send("Tool Window Opened", mapOf("projectResolved" to dir.isNotBlank().toString())) + Telemetry.send("Tool Window Opened", mapOf( + "projectResolved" to dir.isNotBlank().toString(), + "durationMs" to (System.currentTimeMillis() - start).toString(), + )) } } catch (e: Exception) { Telemetry.send("Tool Window Setup Failed", mapOf("stage" to "create", "errorClass" to e::class.java.name)) diff --git a/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/actions/DeleteSessionAction.kt b/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/actions/DeleteSessionAction.kt index 3db258f8cce..0cee2523d55 100644 --- a/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/actions/DeleteSessionAction.kt +++ b/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/actions/DeleteSessionAction.kt @@ -41,7 +41,11 @@ class DeleteSessionAction : AnAction() { else KiloBundle.message("history.delete.confirm.message.multiple", items.size) - if (!confirm(e.project, msg)) return + controller.requestDelete(items.size) + if (!confirm(e.project, msg)) { + controller.cancelDelete(items.size) + return + } items.forEach { controller.delete(it) } } } diff --git a/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/actions/RenameSessionAction.kt b/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/actions/RenameSessionAction.kt index aa40c15fb41..dd2a617b219 100644 --- a/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/actions/RenameSessionAction.kt +++ b/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/actions/RenameSessionAction.kt @@ -39,9 +39,22 @@ class RenameSessionAction : AnAction() { val item = selection.selectedLocal.singleOrNull() ?: return val current = title(item) - val newTitle = input(e.project, current)?.trim() ?: return + controller.requestRename() + val raw = input(e.project, current) + if (raw == null) { + controller.cancelRename("cancelled") + return + } + val newTitle = raw.trim() - if (newTitle.isBlank() || newTitle == current) return + if (newTitle.isBlank()) { + controller.cancelRename("blank") + return + } + if (newTitle == current) { + controller.cancelRename("unchanged") + return + } controller.rename(item, newTitle) } } diff --git a/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/controller/SessionController.kt b/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/controller/SessionController.kt index c7ae050d32d..d4b8432dba9 100644 --- a/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/controller/SessionController.kt +++ b/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/controller/SessionController.kt @@ -194,6 +194,11 @@ class SessionController( val exists = sid != null val dto = promptDto(text) LOG.debug { "${ChatLogSummary.sid(start)} ${ChatLogSummary.prompt(text)} ${ChatLogSummary.dir(directory)}" } + capture("Conversation Send Clicked", sessionProps(sid ?: ref?.key) + mapOf( + "source" to "user", + "hasExistingSession" to exists.toString(), + "textLength" to bucket(text), + ) + promptProps()) showSession() cs.launch { try { @@ -235,6 +240,7 @@ class SessionController( assertEdt() LOG.debug { "${ChatLogSummary.sid(sid ?: ref?.key ?: "pending")} kind=abort" } val id = sid ?: return + capture("Session Stop Clicked", sessionProps(id)) cs.launch { try { sessions.abort(id, directory) @@ -1011,6 +1017,10 @@ class SessionController( if (isPaidModelAuthRequired(event.error)) { loginRetry = retryPrompt() if (reveal) showSession() + capture("Account Overlay Shown", sessionProps(event.sessionID) + mapOf( + "surface" to "session", + "reason" to "paid_model_auth", + )) model.setState(SessionState.LoginRequired(KiloBundle.message("session.login.required.description"))) return } @@ -1364,6 +1374,13 @@ class SessionController( model.variant?.takeIf { it in model.variants }?.let { put("variant", it) } } + private fun bucket(text: String): String = when (text.length) { + 0 -> "empty" + in 1..80 -> "short" + in 81..500 -> "medium" + else -> "long" + } + private fun connectionProps(): Map = buildMap { put("appStatus", model.app.status.name) put("workspaceStatus", model.workspace.status.name) @@ -1374,8 +1391,13 @@ class SessionController( fun dismissLoginRequired() { assertEdt() + val active = model.state is SessionState.LoginRequired loginRetry = null - if (model.state is SessionState.LoginRequired) { + if (active) { + capture("Account Overlay Dismissed", sessionProps() + mapOf( + "surface" to "session", + "reason" to "paid_model_auth", + )) updateModel { model.setState(SessionState.Idle) } } } diff --git a/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/history/HistoryController.kt b/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/history/HistoryController.kt index 246190ef340..dabdf4067d4 100644 --- a/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/history/HistoryController.kt +++ b/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/history/HistoryController.kt @@ -22,6 +22,7 @@ class HistoryController( open: (SessionRef) -> Unit = {}, private val deleted: (String) -> Unit = {}, private val gitUrlProvider: () -> String? = { resolveGitRemoteUrl(workspace.directory) }, + private val telemetry: (String, Map) -> Unit = { event, props -> Telemetry.send(event, props) }, ) { companion object { const val CLOUD_LIMIT = 50 @@ -71,6 +72,7 @@ class HistoryController( val items = result.sessions.map(::localItem) edt { local.replace(items) } } catch (e: Exception) { + capture("History Load Failed", mapOf("source" to "local", "errorClass" to e::class.java.name)) edt { local.fail(e.message ?: KiloBundle.message("history.error.local")) } } } @@ -82,14 +84,35 @@ class HistoryController( fun loadMoreCloud() { if (cloud.cursor == null || cloud.loading) return + capture("History Cloud Load More Clicked") loadCloud(reset = false) } fun applyRepoOnly(value: Boolean) { updateRepoOnly(value) + capture("History Cloud Repo Filter Toggled", mapOf( + "enabled" to value.toString(), + "hasGitUrl" to (gitUrl != null).toString(), + )) edt { reloadCloud() } } + fun requestDelete(count: Int) { + capture("History Session Delete Clicked", mapOf("count" to count.toString())) + } + + fun cancelDelete(count: Int) { + capture("History Session Delete Cancelled", mapOf("count" to count.toString())) + } + + fun requestRename() { + capture("History Session Rename Clicked") + } + + fun cancelRename(reason: String) { + capture("History Session Rename Cancelled", mapOf("reason" to reason)) + } + fun delete(item: LocalHistoryItem) { edt { if (item.id in deleting) return@edt @@ -104,7 +127,7 @@ class HistoryController( edt { deleting.remove(item.id) local.remove(item.id) - Telemetry.send("History Session Deleted", mapOf("sessionId" to item.id)) + capture("History Session Deleted", mapOf("sessionId" to item.id)) deleted(item.id) } } catch (e: Exception) { @@ -124,7 +147,7 @@ class HistoryController( val updated = sessions.renameSession(item.id, dir, title) edt { local.update(LocalHistoryItem(updated)) - Telemetry.send("History Session Renamed", mapOf("sessionId" to item.id)) + capture("History Session Renamed", mapOf("sessionId" to item.id)) } } catch (e: Exception) { edt { local.fail(e.message ?: KiloBundle.message("history.error.local.rename")) } @@ -135,10 +158,12 @@ class HistoryController( fun deleting(item: LocalHistoryItem): Boolean = item.id in deleting fun open(item: LocalHistoryItem) { + capture("History Session Opened", mapOf("source" to "local")) edt { opener(SessionRef.Local(item.session)) } } fun open(item: CloudHistoryItem) { + capture("History Session Opened", mapOf("source" to "cloud")) edt { opener(SessionRef.Cloud(item.session)) } } @@ -151,16 +176,27 @@ class HistoryController( try { val result = sessions.cloudSessions(workspace.directory, cursor, CLOUD_LIMIT, filter) val items = result.sessions.map(::cloudItem) + capture("History Cloud Page Loaded", mapOf( + "reset" to reset.toString(), + "count" to items.size.toString(), + "hasNextCursor" to (result.nextCursor != null).toString(), + "repoOnly" to (filter != null).toString(), + )) edt { if (reset) cloud.replace(items, result.nextCursor) else cloud.append(items, result.nextCursor) } } catch (e: Exception) { + capture("History Load Failed", mapOf("source" to "cloud", "errorClass" to e::class.java.name)) edt { cloud.fail(e.message ?: KiloBundle.message("history.error.cloud")) } } } } + private fun capture(event: String, props: Map = emptyMap()) { + telemetry(event, props) + } + /** * Resolves [gitUrl] on first cloud load. Subsequent calls return the cached value. * Also enables [repoOnly] by default when a URL is found the first time. diff --git a/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/session/controller/PromptLifecycleTest.kt b/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/session/controller/PromptLifecycleTest.kt index acf202509de..04aeadf6630 100644 --- a/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/session/controller/PromptLifecycleTest.kt +++ b/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/session/controller/PromptLifecycleTest.kt @@ -35,6 +35,15 @@ class PromptLifecycleTest : SessionControllerTestBase() { } } + fun `test prompt records send intent telemetry`() { + prompted() + + val event = appRpc.telemetry.single { it.event == "Conversation Send Clicked" } + assertEquals("user", event.properties["source"]) + assertEquals("false", event.properties["hasExistingSession"]) + assertEquals("short", event.properties["textLength"]) + } + fun `test PermissionAsked moves state to AwaitingPermission`() { val (m, _, _) = prompted() diff --git a/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/session/controller/TurnLifecycleTest.kt b/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/session/controller/TurnLifecycleTest.kt index 0151333baab..002d7509c65 100644 --- a/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/session/controller/TurnLifecycleTest.kt +++ b/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/session/controller/TurnLifecycleTest.kt @@ -180,6 +180,9 @@ class TurnLifecycleTest : SessionControllerTestBase() { )) assertTrue(m.model.state is SessionState.LoginRequired) + assertTrue(appRpc.telemetry.any { + it.event == "Account Overlay Shown" && it.properties["reason"] == "paid_model_auth" + }) assertSession( """ [code] [kilo/gpt-5] [login-required] [Go to User Profile settings to sign in, then continue this session.] @@ -346,6 +349,9 @@ class TurnLifecycleTest : SessionControllerTestBase() { edt { m.dismissLoginRequired() } flush() + assertTrue(appRpc.telemetry.any { + it.event == "Account Overlay Dismissed" && it.properties["reason"] == "paid_model_auth" + }) assertSession( """ [code] [kilo/gpt-5] [idle] diff --git a/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/session/history/HistoryControllerTest.kt b/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/session/history/HistoryControllerTest.kt index cc014f97ee7..3ccfb9de9a7 100644 --- a/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/session/history/HistoryControllerTest.kt +++ b/packages/kilo-jetbrains/frontend/src/test/kotlin/ai/kilocode/client/session/history/HistoryControllerTest.kt @@ -104,6 +104,21 @@ class HistoryControllerTest : BasePlatformTestCase() { assertEquals(FakeSessionRpcApi.CloudCall("/test", "next_1", 50, null), rpc.cloudCalls[1]) } + fun `test cloud load records page telemetry`() { + rpc.cloud += cloud("cloud_1", "Cloud One") + rpc.cloudCursor = "next_1" + val events = mutableListOf>>() + val controller = telemetryController(events) + + controller.reloadCloud() + flush() + + val event = events.single { it.first == "History Cloud Page Loaded" } + assertEquals("true", event.second["reset"]) + assertEquals("1", event.second["count"]) + assertEquals("true", event.second["hasNextCursor"]) + } + fun `test local delete calls rpc and removes item`() { rpc.listed += session("ses_1", "Local One") val controller = controller() @@ -117,6 +132,18 @@ class HistoryControllerTest : BasePlatformTestCase() { assertTrue(controller.local.items.isEmpty()) } + fun `test opening history items records source telemetry`() { + val events = mutableListOf>>() + val controller = telemetryController(events) + + controller.open(LocalHistoryItem(session("ses_1", "Local One"))) + controller.open(CloudHistoryItem(cloud("cloud_1", "Cloud One"))) + flush() + + val opened = events.filter { it.first == "History Session Opened" } + assertEquals(listOf("local", "cloud"), opened.map { it.second["source"] }) + } + fun `test activity returns typed items`() { rpc.statuses.value = mapOf( "ses_busy" to SessionStatusDto("busy"), @@ -799,6 +826,13 @@ class HistoryControllerTest : BasePlatformTestCase() { private fun controller() = HistoryController(sessions, workspace, scope) + private fun telemetryController(events: MutableList>>) = HistoryController( + sessions, + workspace, + scope, + telemetry = { event, props -> events.add(event to props) }, + ) + private fun controllerWithGit(url: String?) = HistoryController( sessions, workspace, From 846e6f656e25eae67c6b71a494f184640b608f48 Mon Sep 17 00:00:00 2001 From: kirillk Date: Fri, 29 May 2026 16:26:43 -0400 Subject: [PATCH 07/16] fix(jetbrains): remove direct startup telemetry --- .../backend/app/KiloBackendAppService.kt | 10 +- .../app/KiloBackendConnectionService.kt | 34 +------ .../backend/cli/KiloBackendCliManager.kt | 84 +---------------- .../kilocode/backend/rpc/KiloAppRpcApiImpl.kt | 4 - .../backend/telemetry/KiloBackendTelemetry.kt | 5 +- .../telemetry/KiloCliStartupTelemetry.kt | 94 ------------------- .../session/controller/SessionController.kt | 2 +- .../client/telemetry/KiloTelemetryService.kt | 4 - 8 files changed, 11 insertions(+), 226 deletions(-) delete mode 100644 packages/kilo-jetbrains/backend/src/main/kotlin/ai/kilocode/backend/telemetry/KiloCliStartupTelemetry.kt diff --git a/packages/kilo-jetbrains/backend/src/main/kotlin/ai/kilocode/backend/app/KiloBackendAppService.kt b/packages/kilo-jetbrains/backend/src/main/kotlin/ai/kilocode/backend/app/KiloBackendAppService.kt index 94aea5c6836..1839adbc8f2 100644 --- a/packages/kilo-jetbrains/backend/src/main/kotlin/ai/kilocode/backend/app/KiloBackendAppService.kt +++ b/packages/kilo-jetbrains/backend/src/main/kotlin/ai/kilocode/backend/app/KiloBackendAppService.kt @@ -5,9 +5,6 @@ import ai.kilocode.backend.cli.KiloBackendCliManager import ai.kilocode.backend.migration.KiloBackendLegacyMigrationStoreService import ai.kilocode.backend.migration.LegacyMigrationDetection import ai.kilocode.backend.telemetry.KiloBackendTelemetry -import ai.kilocode.backend.telemetry.CliStartupTelemetry -import ai.kilocode.backend.telemetry.KiloCliStartupTelemetry -import ai.kilocode.backend.telemetry.NoopCliStartupTelemetry import ai.kilocode.log.KiloLog import ai.kilocode.backend.workspace.KiloBackendWorkspaceManager import ai.kilocode.jetbrains.api.client.DefaultApi @@ -78,7 +75,6 @@ class KiloBackendAppService private constructor( private val server: CliServer, private val log: KiloLog, private val loadTimeoutMs: Long, - private val telemetry: CliStartupTelemetry, ) : Disposable { /** IntelliJ service injection entry point. */ @@ -87,7 +83,6 @@ class KiloBackendAppService private constructor( KiloBackendCliManager(), KiloLog.create(KiloBackendAppService::class.java), APP_LOAD_TIMEOUT_MS, - KiloCliStartupTelemetry(), ) companion object { @@ -101,14 +96,13 @@ class KiloBackendAppService private constructor( server: CliServer, log: KiloLog, loadTimeoutMs: Long = APP_LOAD_TIMEOUT_MS, - telemetry: CliStartupTelemetry = NoopCliStartupTelemetry, - ) = KiloBackendAppService(cs, server, log, loadTimeoutMs, telemetry) + ) = KiloBackendAppService(cs, server, log, loadTimeoutMs) } private val mutex = Mutex() private val connection = KiloConnectionService(cs, server, onReconnect = { cs.launch { reconnect() } - }, appLoadTimeoutMs = loadTimeoutMs, log = log, telemetry = telemetry) + }, appLoadTimeoutMs = loadTimeoutMs, log = log) private var watcher: Job? = null private var eventWatcher: Job? = null diff --git a/packages/kilo-jetbrains/backend/src/main/kotlin/ai/kilocode/backend/app/KiloBackendConnectionService.kt b/packages/kilo-jetbrains/backend/src/main/kotlin/ai/kilocode/backend/app/KiloBackendConnectionService.kt index 8a3b8e99aa9..4eccd0b4c74 100644 --- a/packages/kilo-jetbrains/backend/src/main/kotlin/ai/kilocode/backend/app/KiloBackendConnectionService.kt +++ b/packages/kilo-jetbrains/backend/src/main/kotlin/ai/kilocode/backend/app/KiloBackendConnectionService.kt @@ -3,8 +3,6 @@ package ai.kilocode.backend.app import ai.kilocode.backend.cli.KiloBackendHttpClients import ai.kilocode.backend.cli.KiloCliDataParser import ai.kilocode.backend.cli.CliServer -import ai.kilocode.backend.telemetry.CliStartupTelemetry -import ai.kilocode.backend.telemetry.NoopCliStartupTelemetry import ai.kilocode.log.ChatLogSummary import ai.kilocode.log.KiloLog import ai.kilocode.jetbrains.api.client.DefaultApi @@ -64,29 +62,20 @@ class KiloConnectionService( private val onReconnect: () -> Unit, private val log: KiloLog, private val appLoadTimeoutMs: Long, - private val telemetry: CliStartupTelemetry, ) { constructor( cs: CoroutineScope, server: CliServer, onReconnect: () -> Unit, - ) : this(cs, server, onReconnect, KiloLog.create(KiloConnectionService::class.java), 30_000L, NoopCliStartupTelemetry) + ) : this(cs, server, onReconnect, KiloLog.create(KiloConnectionService::class.java), 30_000L) constructor( cs: CoroutineScope, server: CliServer, onReconnect: () -> Unit, log: KiloLog, - ) : this(cs, server, onReconnect, log, 30_000L, NoopCliStartupTelemetry) - - constructor( - cs: CoroutineScope, - server: CliServer, - onReconnect: () -> Unit, - log: KiloLog, - telemetry: CliStartupTelemetry, - ) : this(cs, server, onReconnect, log, 30_000L, telemetry) + ) : this(cs, server, onReconnect, log, 30_000L) companion object { private const val HEARTBEAT_TIMEOUT_MS = 15_000L @@ -272,12 +261,6 @@ class KiloConnectionService( } else { log.warn("SSE: failure (HTTP ${response?.code}) — scheduling reconnect") } - report("sse", mapOf( - "message" to (t?.message ?: "SSE connection failed (HTTP ${response?.code})"), - "details" to detail.orEmpty(), - "httpStatus" to (response?.code?.toString() ?: ""), - "errorClass" to (t?.javaClass?.name ?: ""), - )) setState(ConnectionState.Error(t?.message ?: "SSE connection failed (HTTP ${response?.code})", detail)) scheduleReconnect() } @@ -319,7 +302,6 @@ class KiloConnectionService( val elapsed = System.currentTimeMillis() - lastEvent.get() if (elapsed > HEARTBEAT_TIMEOUT_MS) { log.warn("SSE: heartbeat timeout (${elapsed}ms) — forcing reconnect") - report("heartbeat", mapOf("elapsedMs" to elapsed.toString())) source.getAndSet(null)?.cancel() scheduleReconnect() } @@ -334,7 +316,6 @@ class KiloConnectionService( val ok = checkHealth() if (!ok && _state.value is ConnectionState.Connected) { log.warn("Health check failed — forcing SSE reconnect") - report("health", mapOf("message" to "Health check failed")) source.getAndSet(null)?.cancel() scheduleReconnect() } @@ -360,7 +341,6 @@ class KiloConnectionService( server.exited(proc) val code = proc.exitValue() log.warn("CLI process exited with code $code") - report("process", mapOf("exitCode" to code.toString(), "message" to "CLI process exited")) source.getAndSet(null)?.cancel() setState(ConnectionState.Error("CLI process exited with code $code")) scheduleReconnect() @@ -382,16 +362,6 @@ class KiloConnectionService( _state.value = next } - private fun report(stage: String, props: Map) { - cs.launch { - telemetry.report("CLI Connection Failed", base() + props + mapOf("stage" to stage)) - } - } - - private fun base(): Map = buildMap { - put("port", port.toString()) - } - fun dispose() { disposed = true source.getAndSet(null)?.cancel() diff --git a/packages/kilo-jetbrains/backend/src/main/kotlin/ai/kilocode/backend/cli/KiloBackendCliManager.kt b/packages/kilo-jetbrains/backend/src/main/kotlin/ai/kilocode/backend/cli/KiloBackendCliManager.kt index 11ed07bcea5..2cc443d8d8b 100644 --- a/packages/kilo-jetbrains/backend/src/main/kotlin/ai/kilocode/backend/cli/KiloBackendCliManager.kt +++ b/packages/kilo-jetbrains/backend/src/main/kotlin/ai/kilocode/backend/cli/KiloBackendCliManager.kt @@ -2,8 +2,6 @@ package ai.kilocode.backend.cli import ai.kilocode.KiloPlugin import ai.kilocode.backend.dev.KiloDevMode -import ai.kilocode.backend.telemetry.CliStartupTelemetry -import ai.kilocode.backend.telemetry.KiloCliStartupTelemetry import ai.kilocode.log.KiloLog import com.intellij.openapi.application.ApplicationInfo import com.intellij.openapi.application.PathManager @@ -32,7 +30,6 @@ import java.util.concurrent.TimeUnit */ class KiloBackendCliManager( private val log: KiloLog = KiloLog.create(KiloBackendCliManager::class.java), - private val startup: CliStartupTelemetry = KiloCliStartupTelemetry(), ) : CliServer { companion object { @@ -49,30 +46,17 @@ class KiloBackendCliManager( @Volatile override var forceExtract = false - private var startupReported = false - override fun process(): Process? = process override suspend fun init(): CliServer.State { - startupReported = false - val start = System.currentTimeMillis() return try { val path = extractCli() log.info("CLI binary path: ${path.absolutePath} (size=${path.length()} bytes)") - val state = withTimeout(STARTUP_TIMEOUT_MS) { + withTimeout(STARTUP_TIMEOUT_MS) { spawn(path) } - if (state is CliServer.State.Ready) { - startup.report("CLI Startup Succeeded", mapOf( - "durationMs" to (System.currentTimeMillis() - start).toString(), - "platform" to platform(), - "arch" to CpuArch.CURRENT.name, - )) - } - state } catch (e: Exception) { log.warn("CLI startup failed", e) - if (!startupReported) reportStartupException(e) process?.let { proc -> log.info("Cleaning up orphaned CLI process (pid=${proc.pid()})") process = null @@ -198,15 +182,7 @@ class KiloBackendCliManager( val proc = try { builder.start() } catch (e: Exception) { - reportStartupFailure( - env = env, - cmd = cmd, - stdout = "", - stderr = "", - code = null, - details = e.message, - error = e, - ) + log.warn("CLI process failed to start: ${e.message}", e) throw e } log.info("CLI process started (pid=${proc.pid()})") @@ -242,69 +218,15 @@ class KiloBackendCliManager( val code = proc.waitFor() val details = synchronized(stderr) { stderr.toString().trim() } - val out = synchronized(stdout) { stdout.toString().trim() } process = null uninstall() - reportStartupFailure( - env = env, - cmd = cmd, - stdout = out, - stderr = details, - code = code, - details = details, - error = null, - ) + log.warn("CLI process exited with code $code before announcing a port: $details") CliServer.State.Error( message = "CLI process exited with code $code before announcing a port", details = details.ifEmpty { null }, ) } - private suspend fun reportStartupFailure( - env: Map, - cmd: List, - stdout: String, - stderr: String, - code: Int?, - details: String?, - error: Exception?, - ) { - startupReported = true - startup.report(buildMap { - if (code != null) put("exitCode", code.toString()) - put("stdout", stdout) - put("stderr", stderr) - put("details", details.orEmpty()) - put("command", cmd.joinToString(" ")) - put("platform", platform()) - put("arch", CpuArch.CURRENT.name) - env["KILO_APP_VERSION"]?.let { put("pluginVersion", it) } - env["KILO_APP_VERSION"]?.let { put("appVersion", it) } - env["KILO_EDITOR_NAME"]?.let { put("editorName", it) } - env["KILO_MACHINE_ID"]?.let { put("machineId", it) } - error?.let { - put("errorClass", it::class.java.name) - put("message", it.message.orEmpty()) - } - }) - } - - private suspend fun reportStartupException(error: Exception) { - startupReported = true - val env = ideEnv() - startup.report(buildMap { - put("details", error.stackTraceToString()) - put("errorClass", error::class.java.name) - put("message", error.message.orEmpty()) - runCatching { put("platform", platform()) } - put("arch", CpuArch.CURRENT.name) - env["KILO_APP_VERSION"]?.let { put("pluginVersion", it) } - env["KILO_APP_VERSION"]?.let { put("appVersion", it) } - env["KILO_EDITOR_NAME"]?.let { put("editorName", it) } - env["KILO_MACHINE_ID"]?.let { put("machineId", it) } - }) - } - override fun dispose() { val proc = process ?: return process = null diff --git a/packages/kilo-jetbrains/backend/src/main/kotlin/ai/kilocode/backend/rpc/KiloAppRpcApiImpl.kt b/packages/kilo-jetbrains/backend/src/main/kotlin/ai/kilocode/backend/rpc/KiloAppRpcApiImpl.kt index 5795c451205..668db20b290 100644 --- a/packages/kilo-jetbrains/backend/src/main/kotlin/ai/kilocode/backend/rpc/KiloAppRpcApiImpl.kt +++ b/packages/kilo-jetbrains/backend/src/main/kotlin/ai/kilocode/backend/rpc/KiloAppRpcApiImpl.kt @@ -5,7 +5,6 @@ package ai.kilocode.backend.rpc import ai.kilocode.backend.app.KiloAppState import ai.kilocode.backend.app.KiloBackendAppService import ai.kilocode.backend.telemetry.KiloBackendTelemetry -import ai.kilocode.backend.telemetry.KiloCliStartupTelemetry import ai.kilocode.backend.app.ConfigWarning import ai.kilocode.backend.app.LoadError import ai.kilocode.backend.app.LoadProgress @@ -99,9 +98,6 @@ class KiloAppRpcApiImpl : KiloAppRpcApi { override suspend fun captureTelemetry(capture: TelemetryCaptureDto) { service().capture(app.http, app.port, capture.event, capture.properties) - if ((app.http == null || app.port <= 0) && capture.event == "Connection Retry Clicked") { - KiloCliStartupTelemetry().report(capture.event, capture.properties) - } } private fun dto(state: KiloAppState): KiloAppStateDto = diff --git a/packages/kilo-jetbrains/backend/src/main/kotlin/ai/kilocode/backend/telemetry/KiloBackendTelemetry.kt b/packages/kilo-jetbrains/backend/src/main/kotlin/ai/kilocode/backend/telemetry/KiloBackendTelemetry.kt index af9829ab3fd..5f90aa377cf 100644 --- a/packages/kilo-jetbrains/backend/src/main/kotlin/ai/kilocode/backend/telemetry/KiloBackendTelemetry.kt +++ b/packages/kilo-jetbrains/backend/src/main/kotlin/ai/kilocode/backend/telemetry/KiloBackendTelemetry.kt @@ -24,6 +24,7 @@ class KiloBackendTelemetry( log.debug { "telemetry capture dev-mode: $body" } return } + if (http == null || port <= 0) return post(http, port, "telemetry/capture", body) } @@ -33,11 +34,11 @@ class KiloBackendTelemetry( log.debug { "telemetry setEnabled dev-mode: $body" } return } + if (http == null || port <= 0) return post(http, port, "telemetry/setEnabled", body) } - private suspend fun post(http: OkHttpClient?, port: Int, path: String, body: String) { - if (http == null || port <= 0) return + private suspend fun post(http: OkHttpClient, port: Int, path: String, body: String) { withContext(Dispatchers.IO) { try { val req = Request.Builder() diff --git a/packages/kilo-jetbrains/backend/src/main/kotlin/ai/kilocode/backend/telemetry/KiloCliStartupTelemetry.kt b/packages/kilo-jetbrains/backend/src/main/kotlin/ai/kilocode/backend/telemetry/KiloCliStartupTelemetry.kt deleted file mode 100644 index 78596d95949..00000000000 --- a/packages/kilo-jetbrains/backend/src/main/kotlin/ai/kilocode/backend/telemetry/KiloCliStartupTelemetry.kt +++ /dev/null @@ -1,94 +0,0 @@ -package ai.kilocode.backend.telemetry - -import ai.kilocode.KiloPlugin -import ai.kilocode.backend.dev.KiloDevMode -import ai.kilocode.log.KiloLog -import com.intellij.openapi.application.ApplicationInfo -import com.intellij.openapi.util.SystemInfo -import com.intellij.util.system.CpuArch -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.withContext -import kotlinx.serialization.json.JsonObject -import kotlinx.serialization.json.JsonPrimitive -import okhttp3.MediaType.Companion.toMediaType -import okhttp3.OkHttpClient -import okhttp3.Request -import okhttp3.RequestBody.Companion.toRequestBody -import java.util.concurrent.TimeUnit - -interface CliStartupTelemetry { - suspend fun report(properties: Map) { - report("CLI Startup Failed", properties) - } - - suspend fun report(event: String, properties: Map) -} - -object NoopCliStartupTelemetry : CliStartupTelemetry { - override suspend fun report(event: String, properties: Map) = Unit -} - -class KiloCliStartupTelemetry( - private val http: OkHttpClient = OkHttpClient.Builder() - .callTimeout(5, TimeUnit.SECONDS) - .build(), - private val url: String = "https://us.i.posthog.com/capture/", - private val log: KiloLog = KiloLog.create(KiloCliStartupTelemetry::class.java), -) : CliStartupTelemetry { - override suspend fun report(event: String, properties: Map) { - withContext(Dispatchers.IO) { - try { - val props = base() + properties - val body = JsonObject( - mapOf( - "api_key" to JsonPrimitive("phc_GK2Pxl0HPj5ZPfwhLRjXrtdz8eD7e9MKnXiFrOqnB6z"), - "event" to JsonPrimitive(event), - "distinct_id" to JsonPrimitive(props["machineId"] ?: "jetbrains-unknown"), - "properties" to JsonObject(props.mapValues { JsonPrimitive(it.value) }), - ), - ).toString() - if (KiloDevMode.enabled()) { - log.debug { "startup telemetry dev-mode: $body" } - return@withContext - } - val req = Request.Builder() - .url(url) - .header("Accept", "application/json") - .post(body.toRequestBody("application/json".toMediaType())) - .build() - http.newCall(req).execute().use { res -> - if (!res.isSuccessful) log.warn("startup telemetry failed: HTTP ${res.code}") - } - } catch (e: Exception) { - log.warn("startup telemetry failed: ${e.message}", e) - } - } - } - - private fun base(): Map = buildMap { - put("platform", "jetbrains") - put("client", "jetbrains") - put("feature", "jetbrains-plugin") - put("os", os()) - put("arch", CpuArch.CURRENT.name) - runCatching { - val info = ApplicationInfo.getInstance() - put("editorName", info.fullApplicationName) - put("jetbrainsBuild", info.build.asString()) - }.onFailure { log.info("Could not read ApplicationInfo for startup telemetry: ${it.message}") } - runCatching { - val version = KiloPlugin.version() - if (version != null) { - put("pluginVersion", version) - put("appVersion", version) - } - }.onFailure { log.info("Could not read plugin version for startup telemetry: ${it.message}") } - } - - private fun os(): String = when { - SystemInfo.isMac -> "darwin" - SystemInfo.isLinux -> "linux" - SystemInfo.isWindows -> "windows" - else -> "unknown" - } -} diff --git a/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/controller/SessionController.kt b/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/controller/SessionController.kt index d4b8432dba9..00c51c833ed 100644 --- a/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/controller/SessionController.kt +++ b/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/controller/SessionController.kt @@ -388,7 +388,7 @@ class SessionController( try { if (rules != null) sessions.savePermissionRules(requestId, directory, rules) sessions.replyPermission(requestId, directory, reply) - capture("Ask Approval", sessionProps() + mapOf( + capture("Approval Answered", sessionProps() + mapOf( "requestId" to requestId, "tool" to (current?.permission?.name ?: "unknown"), "reply" to reply.reply, diff --git a/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/telemetry/KiloTelemetryService.kt b/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/telemetry/KiloTelemetryService.kt index ca277e5056a..d299aeda4cb 100644 --- a/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/telemetry/KiloTelemetryService.kt +++ b/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/telemetry/KiloTelemetryService.kt @@ -30,10 +30,6 @@ class KiloTelemetryService internal constructor( fun getInstance(): KiloTelemetryService = service() } - fun capture(event: String, properties: Map = emptyMap()) { - send(event, properties) - } - fun send(event: String, properties: Map = emptyMap()) { cs.launch { try { From 98646038537a7d74d697f66edc5eecc828b888f0 Mon Sep 17 00:00:00 2001 From: kirillk Date: Sat, 30 May 2026 11:48:13 -0400 Subject: [PATCH 08/16] fix(jetbrains): skip root instrumentation in CI --- packages/kilo-jetbrains/build.gradle.kts | 5 +++++ packages/kilo-jetbrains/script/test-ci.ts | 4 ++-- 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/packages/kilo-jetbrains/build.gradle.kts b/packages/kilo-jetbrains/build.gradle.kts index 06e65495e9d..fd4c091eede 100644 --- a/packages/kilo-jetbrains/build.gradle.kts +++ b/packages/kilo-jetbrains/build.gradle.kts @@ -1,6 +1,7 @@ import org.jetbrains.changelog.Changelog import org.jetbrains.intellij.platform.gradle.IntelliJPlatformType import org.jetbrains.intellij.platform.gradle.TestFrameworkType +import org.jetbrains.intellij.platform.gradle.tasks.InstrumentCodeTask import org.jetbrains.intellij.platform.gradle.tasks.RunIdeTask import org.jetbrains.intellij.platform.gradle.tasks.aware.SplitModeAware.SplitModeTarget import java.time.LocalDate @@ -170,6 +171,10 @@ intellijPlatform { } tasks { + withType { + enabled = false + } + runIdeBackend { splitModeServerPort.set(splitPort) dependsOn(":backend:prepareLocalCli") diff --git a/packages/kilo-jetbrains/script/test-ci.ts b/packages/kilo-jetbrains/script/test-ci.ts index 4e9a3801bb0..782f476d80d 100644 --- a/packages/kilo-jetbrains/script/test-ci.ts +++ b/packages/kilo-jetbrains/script/test-ci.ts @@ -3,7 +3,7 @@ /** * CI test runner for the JetBrains plugin. * - * Runs ./gradlew test --continue --no-build-cache so all modules run even when some fail, + * Runs ./gradlew clean test --continue --no-build-cache so all modules run even when some fail, * then collects per-module JUnit XML results into .artifacts/unit/junit.xml * so mikepenz/action-junit-report can find them at the standard path. * The generated OpenAPI client can otherwise restore stale compile outputs @@ -20,7 +20,7 @@ import { mkdirSync, readdirSync, readFileSync, writeFileSync, existsSync } from const root = join(import.meta.dir, "..") const gradlew = process.platform === "win32" ? "gradlew.bat" : "./gradlew" -const args = ["test", "--continue", "--no-build-cache"] +const args = ["clean", "test", "--continue", "--no-build-cache"] const cmd = process.platform === "win32" ? ["cmd.exe", "/c", gradlew, ...args] : [gradlew, ...args] const fallback = 45 * 60 * 1000 const parsed = Number(process.env.KILO_JETBRAINS_TEST_TIMEOUT_MS ?? fallback) From 8aa109fff178a86d4354623a6f3496e48668c059 Mon Sep 17 00:00:00 2001 From: kirillk Date: Sat, 30 May 2026 17:53:17 -0400 Subject: [PATCH 09/16] fix(jetbrains): address telemetry review feedback --- .../ai/kilocode/backend/cli/KiloBackendCliManager.kt | 2 -- .../shared/src/main/kotlin/ai/kilocode/KiloPlugin.kt | 9 ++------- 2 files changed, 2 insertions(+), 9 deletions(-) diff --git a/packages/kilo-jetbrains/backend/src/main/kotlin/ai/kilocode/backend/cli/KiloBackendCliManager.kt b/packages/kilo-jetbrains/backend/src/main/kotlin/ai/kilocode/backend/cli/KiloBackendCliManager.kt index 2cc443d8d8b..564d342590e 100644 --- a/packages/kilo-jetbrains/backend/src/main/kotlin/ai/kilocode/backend/cli/KiloBackendCliManager.kt +++ b/packages/kilo-jetbrains/backend/src/main/kotlin/ai/kilocode/backend/cli/KiloBackendCliManager.kt @@ -190,7 +190,6 @@ class KiloBackendCliManager( install(proc) val stderr = StringBuilder() - val stdout = StringBuilder() Thread({ BufferedReader(InputStreamReader(proc.errorStream)).use { reader -> @@ -204,7 +203,6 @@ class KiloBackendCliManager( BufferedReader(InputStreamReader(proc.inputStream)).use { reader -> for (line in reader.lineSequence()) { log.info("CLI stdout: $line") - synchronized(stdout) { stdout.appendLine(line) } val match = PORT_REGEX.find(line) if (match != null) { val p = match.groupValues[1].toInt() diff --git a/packages/kilo-jetbrains/shared/src/main/kotlin/ai/kilocode/KiloPlugin.kt b/packages/kilo-jetbrains/shared/src/main/kotlin/ai/kilocode/KiloPlugin.kt index 851c7a811d7..5dfd3f5ee62 100644 --- a/packages/kilo-jetbrains/shared/src/main/kotlin/ai/kilocode/KiloPlugin.kt +++ b/packages/kilo-jetbrains/shared/src/main/kotlin/ai/kilocode/KiloPlugin.kt @@ -1,6 +1,6 @@ package ai.kilocode -import com.intellij.ide.plugins.cl.PluginAwareClassLoader +import com.intellij.ide.plugins.PluginManagerCore import com.intellij.openapi.extensions.PluginDescriptor import com.intellij.openapi.extensions.PluginId @@ -9,12 +9,7 @@ object KiloPlugin { val id: PluginId = PluginId.getId(ID) - fun descriptor(): PluginDescriptor? { - val loader = KiloPlugin::class.java.classLoader as? PluginAwareClassLoader ?: return null - val descriptor = loader.pluginDescriptor - if (descriptor.pluginId != id) return null - return descriptor - } + fun descriptor(): PluginDescriptor? = PluginManagerCore.getPlugin(id) fun version() = descriptor()?.version From 0d9ea23ec3ae2957d96fe4228143047df6db9bcf Mon Sep 17 00:00:00 2001 From: kirillk Date: Sun, 31 May 2026 10:11:09 -0400 Subject: [PATCH 10/16] fix(jetbrains): align logging environment payload --- .../backend/telemetry/KiloBackendTelemetry.kt | 22 +++--------------- .../main/kotlin/ai/kilocode/log/KiloLog.kt | 23 +++++++++++++++++++ 2 files changed, 26 insertions(+), 19 deletions(-) diff --git a/packages/kilo-jetbrains/backend/src/main/kotlin/ai/kilocode/backend/telemetry/KiloBackendTelemetry.kt b/packages/kilo-jetbrains/backend/src/main/kotlin/ai/kilocode/backend/telemetry/KiloBackendTelemetry.kt index 5f90aa377cf..dd0b1571e3d 100644 --- a/packages/kilo-jetbrains/backend/src/main/kotlin/ai/kilocode/backend/telemetry/KiloBackendTelemetry.kt +++ b/packages/kilo-jetbrains/backend/src/main/kotlin/ai/kilocode/backend/telemetry/KiloBackendTelemetry.kt @@ -1,9 +1,8 @@ package ai.kilocode.backend.telemetry -import ai.kilocode.KiloPlugin import ai.kilocode.backend.dev.KiloDevMode +import ai.kilocode.log.KiloEnvironment import ai.kilocode.log.KiloLog -import com.intellij.openapi.application.ApplicationInfo import com.intellij.openapi.components.Service import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.withContext @@ -62,21 +61,6 @@ class KiloBackendTelemetry( ), ).toString() - private fun base(): Map = buildMap { - put("platform", JsonPrimitive("jetbrains")) - put("client", JsonPrimitive("jetbrains")) - put("feature", JsonPrimitive("jetbrains-plugin")) - runCatching { - val info = ApplicationInfo.getInstance() - put("editorName", JsonPrimitive(info.fullApplicationName)) - put("jetbrainsBuild", JsonPrimitive(info.build.asString())) - }.onFailure { log.info("Could not read ApplicationInfo for telemetry: ${it.message}") } - runCatching { - val version = KiloPlugin.version() - if (version != null) { - put("pluginVersion", JsonPrimitive(version)) - put("appVersion", JsonPrimitive(version)) - } - }.onFailure { log.info("Could not read plugin version for telemetry: ${it.message}") } - } + private fun base(): Map = + KiloEnvironment.payload(log).mapValues { JsonPrimitive(it.value) } } diff --git a/packages/kilo-jetbrains/shared/src/main/kotlin/ai/kilocode/log/KiloLog.kt b/packages/kilo-jetbrains/shared/src/main/kotlin/ai/kilocode/log/KiloLog.kt index ddefc89dafc..3e89376c359 100644 --- a/packages/kilo-jetbrains/shared/src/main/kotlin/ai/kilocode/log/KiloLog.kt +++ b/packages/kilo-jetbrains/shared/src/main/kotlin/ai/kilocode/log/KiloLog.kt @@ -1,6 +1,7 @@ package ai.kilocode.log import ai.kilocode.KiloPlugin +import com.intellij.openapi.application.ApplicationInfo import com.intellij.openapi.application.PathManager import com.intellij.openapi.diagnostic.Logger import java.io.PrintWriter @@ -60,6 +61,26 @@ interface KiloLog { } } +object KiloEnvironment { + fun payload(log: KiloLog? = null): Map = buildMap { + put("platform", "jetbrains") + put("client", "jetbrains") + put("feature", "jetbrains-plugin") + runCatching { + val info = ApplicationInfo.getInstance() + put("editorName", info.fullApplicationName) + put("jetbrainsBuild", info.build.asString()) + }.onFailure { log?.info("Could not read ApplicationInfo for environment payload: ${it.message}") } + runCatching { + val version = KiloPlugin.version() + if (version != null) { + put("pluginVersion", version) + put("appVersion", version) + } + }.onFailure { log?.info("Could not read plugin version for environment payload: ${it.message}") } + } +} + internal class IntellijLog(cls: Class<*>) : KiloLog { private val delegate = Logger.getInstance(cls) override val isDebugEnabled: Boolean @@ -84,9 +105,11 @@ internal class FileLog(cls: Class<*>) : KiloLog { private val root: java.util.logging.Logger by lazy { val logger = java.util.logging.Logger.getLogger("ai.kilocode") + val payload = KiloEnvironment.payload().entries.joinToString(" ") { "${it.key}=${it.value}" } logger.addHandler(handler) logger.useParentHandlers = false logger.level = level + logger.log(Level.INFO, "environment payload: $payload") logger } From 2e2f3d924f369bddd5481c86ed6ea2620c61d326 Mon Sep 17 00:00:00 2001 From: kirillk Date: Sun, 31 May 2026 12:22:41 -0400 Subject: [PATCH 11/16] fix(jetbrains): keep sandbox telemetry local --- .../kotlin/ai/kilocode/backend/dev/KiloDevMode.kt | 4 +++- .../backend/telemetry/KiloBackendTelemetry.kt | 4 ++-- .../client/telemetry/KiloTelemetryService.kt | 6 ++++++ .../src/main/kotlin/ai/kilocode/log/KiloLog.kt | 15 ++++++--------- 4 files changed, 17 insertions(+), 12 deletions(-) diff --git a/packages/kilo-jetbrains/backend/src/main/kotlin/ai/kilocode/backend/dev/KiloDevMode.kt b/packages/kilo-jetbrains/backend/src/main/kotlin/ai/kilocode/backend/dev/KiloDevMode.kt index 80f042bfc05..fedc21b82f8 100644 --- a/packages/kilo-jetbrains/backend/src/main/kotlin/ai/kilocode/backend/dev/KiloDevMode.kt +++ b/packages/kilo-jetbrains/backend/src/main/kotlin/ai/kilocode/backend/dev/KiloDevMode.kt @@ -1,5 +1,7 @@ package ai.kilocode.backend.dev +import ai.kilocode.log.KiloEnvironment + object KiloDevMode { - fun enabled(): Boolean = System.getProperty("idea.plugin.in.sandbox.mode", "false").toBoolean() + fun enabled(): Boolean = KiloEnvironment.sandbox() } diff --git a/packages/kilo-jetbrains/backend/src/main/kotlin/ai/kilocode/backend/telemetry/KiloBackendTelemetry.kt b/packages/kilo-jetbrains/backend/src/main/kotlin/ai/kilocode/backend/telemetry/KiloBackendTelemetry.kt index dd0b1571e3d..1d4f0d61a27 100644 --- a/packages/kilo-jetbrains/backend/src/main/kotlin/ai/kilocode/backend/telemetry/KiloBackendTelemetry.kt +++ b/packages/kilo-jetbrains/backend/src/main/kotlin/ai/kilocode/backend/telemetry/KiloBackendTelemetry.kt @@ -20,7 +20,7 @@ class KiloBackendTelemetry( suspend fun capture(http: OkHttpClient?, port: Int, event: String, properties: Map) { val body = payload(event, properties) if (KiloDevMode.enabled()) { - log.debug { "telemetry capture dev-mode: $body" } + log.info("telemetry capture sandbox: $body") return } if (http == null || port <= 0) return @@ -30,7 +30,7 @@ class KiloBackendTelemetry( suspend fun setEnabled(http: OkHttpClient?, port: Int, enabled: Boolean) { val body = JsonObject(mapOf("enabled" to JsonPrimitive(enabled))).toString() if (KiloDevMode.enabled()) { - log.debug { "telemetry setEnabled dev-mode: $body" } + log.info("telemetry setEnabled sandbox: $body") return } if (http == null || port <= 0) return diff --git a/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/telemetry/KiloTelemetryService.kt b/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/telemetry/KiloTelemetryService.kt index d299aeda4cb..b4264327892 100644 --- a/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/telemetry/KiloTelemetryService.kt +++ b/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/telemetry/KiloTelemetryService.kt @@ -2,6 +2,7 @@ package ai.kilocode.client.telemetry +import ai.kilocode.log.KiloEnvironment import ai.kilocode.log.KiloLog import ai.kilocode.rpc.KiloAppRpcApi import ai.kilocode.rpc.dto.TelemetryCaptureDto @@ -31,6 +32,11 @@ class KiloTelemetryService internal constructor( } fun send(event: String, properties: Map = emptyMap()) { + if (KiloEnvironment.sandbox()) { + val payload = KiloEnvironment.payload(LOG) + properties + LOG.info("telemetry capture sandbox: event=$event ${payload.entries.joinToString(" ") { "${it.key}=${it.value}" }}") + return + } cs.launch { try { val dto = TelemetryCaptureDto(event, properties) diff --git a/packages/kilo-jetbrains/shared/src/main/kotlin/ai/kilocode/log/KiloLog.kt b/packages/kilo-jetbrains/shared/src/main/kotlin/ai/kilocode/log/KiloLog.kt index 3e89376c359..3477a5adc2b 100644 --- a/packages/kilo-jetbrains/shared/src/main/kotlin/ai/kilocode/log/KiloLog.kt +++ b/packages/kilo-jetbrains/shared/src/main/kotlin/ai/kilocode/log/KiloLog.kt @@ -24,9 +24,8 @@ import java.util.logging.LogRecord * which writes to the standard IDE log file. * * In sandbox mode (i.e. when running via `./gradlew runIde`, detected via the `idea.plugin.in.sandbox.mode` - * system property), or in RC plugin builds, output is additionally written to a `kilo-dev.log` file inside the - * IDE log directory. This makes it easy to tail frontend and backend logs side-by-side during development or - * RC validation without opening the IDE's own log viewer. + * system property), output is written only to a `kilo-dev.log` file inside the IDE log directory. RC plugin builds + * write to both IntelliJ's log and `kilo-dev.log`. * * Usage: * ```kotlin @@ -49,19 +48,17 @@ interface KiloLog { companion object { fun create(cls: Class<*>): KiloLog { + if (KiloEnvironment.sandbox()) return FileLog(cls) val intellij = IntellijLog(cls) - if (!fileEnabled()) return intellij + if (!runCatching { KiloPlugin.isRc() }.getOrDefault(false)) return intellij return CompositeLog(intellij, FileLog(cls)) } - - private fun fileEnabled(): Boolean { - if (System.getProperty("idea.plugin.in.sandbox.mode", "false").toBoolean()) return true - return runCatching { KiloPlugin.isRc() }.getOrDefault(false) - } } } object KiloEnvironment { + fun sandbox(): Boolean = System.getProperty("idea.plugin.in.sandbox.mode", "false").toBoolean() + fun payload(log: KiloLog? = null): Map = buildMap { put("platform", "jetbrains") put("client", "jetbrains") From f755792baa508096ffeb79bee99c5417086b0a74 Mon Sep 17 00:00:00 2001 From: kirillk Date: Sun, 31 May 2026 13:51:29 -0400 Subject: [PATCH 12/16] fix(jetbrains): keep telemetry payload on logger --- .../ai/kilocode/backend/dev/KiloDevMode.kt | 4 +- .../backend/telemetry/KiloBackendTelemetry.kt | 3 +- .../client/telemetry/KiloTelemetryService.kt | 5 +-- .../main/kotlin/ai/kilocode/log/KiloLog.kt | 42 +++++++++---------- 4 files changed, 25 insertions(+), 29 deletions(-) diff --git a/packages/kilo-jetbrains/backend/src/main/kotlin/ai/kilocode/backend/dev/KiloDevMode.kt b/packages/kilo-jetbrains/backend/src/main/kotlin/ai/kilocode/backend/dev/KiloDevMode.kt index fedc21b82f8..1118cb96dd8 100644 --- a/packages/kilo-jetbrains/backend/src/main/kotlin/ai/kilocode/backend/dev/KiloDevMode.kt +++ b/packages/kilo-jetbrains/backend/src/main/kotlin/ai/kilocode/backend/dev/KiloDevMode.kt @@ -1,7 +1,7 @@ package ai.kilocode.backend.dev -import ai.kilocode.log.KiloEnvironment +import ai.kilocode.log.KiloLog object KiloDevMode { - fun enabled(): Boolean = KiloEnvironment.sandbox() + fun enabled(): Boolean = KiloLog.sandbox() } diff --git a/packages/kilo-jetbrains/backend/src/main/kotlin/ai/kilocode/backend/telemetry/KiloBackendTelemetry.kt b/packages/kilo-jetbrains/backend/src/main/kotlin/ai/kilocode/backend/telemetry/KiloBackendTelemetry.kt index 1d4f0d61a27..6a3fade0d25 100644 --- a/packages/kilo-jetbrains/backend/src/main/kotlin/ai/kilocode/backend/telemetry/KiloBackendTelemetry.kt +++ b/packages/kilo-jetbrains/backend/src/main/kotlin/ai/kilocode/backend/telemetry/KiloBackendTelemetry.kt @@ -1,7 +1,6 @@ package ai.kilocode.backend.telemetry import ai.kilocode.backend.dev.KiloDevMode -import ai.kilocode.log.KiloEnvironment import ai.kilocode.log.KiloLog import com.intellij.openapi.components.Service import kotlinx.coroutines.Dispatchers @@ -62,5 +61,5 @@ class KiloBackendTelemetry( ).toString() private fun base(): Map = - KiloEnvironment.payload(log).mapValues { JsonPrimitive(it.value) } + KiloLog.payload(log).mapValues { JsonPrimitive(it.value) } } diff --git a/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/telemetry/KiloTelemetryService.kt b/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/telemetry/KiloTelemetryService.kt index b4264327892..7dbe80eb65d 100644 --- a/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/telemetry/KiloTelemetryService.kt +++ b/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/telemetry/KiloTelemetryService.kt @@ -2,7 +2,6 @@ package ai.kilocode.client.telemetry -import ai.kilocode.log.KiloEnvironment import ai.kilocode.log.KiloLog import ai.kilocode.rpc.KiloAppRpcApi import ai.kilocode.rpc.dto.TelemetryCaptureDto @@ -32,8 +31,8 @@ class KiloTelemetryService internal constructor( } fun send(event: String, properties: Map = emptyMap()) { - if (KiloEnvironment.sandbox()) { - val payload = KiloEnvironment.payload(LOG) + properties + if (KiloLog.sandbox()) { + val payload = KiloLog.payload(LOG) + properties LOG.info("telemetry capture sandbox: event=$event ${payload.entries.joinToString(" ") { "${it.key}=${it.value}" }}") return } diff --git a/packages/kilo-jetbrains/shared/src/main/kotlin/ai/kilocode/log/KiloLog.kt b/packages/kilo-jetbrains/shared/src/main/kotlin/ai/kilocode/log/KiloLog.kt index 3477a5adc2b..12763663cf3 100644 --- a/packages/kilo-jetbrains/shared/src/main/kotlin/ai/kilocode/log/KiloLog.kt +++ b/packages/kilo-jetbrains/shared/src/main/kotlin/ai/kilocode/log/KiloLog.kt @@ -48,33 +48,31 @@ interface KiloLog { companion object { fun create(cls: Class<*>): KiloLog { - if (KiloEnvironment.sandbox()) return FileLog(cls) + if (sandbox()) return FileLog(cls) val intellij = IntellijLog(cls) if (!runCatching { KiloPlugin.isRc() }.getOrDefault(false)) return intellij return CompositeLog(intellij, FileLog(cls)) } - } -} -object KiloEnvironment { - fun sandbox(): Boolean = System.getProperty("idea.plugin.in.sandbox.mode", "false").toBoolean() + fun sandbox(): Boolean = System.getProperty("idea.plugin.in.sandbox.mode", "false").toBoolean() - fun payload(log: KiloLog? = null): Map = buildMap { - put("platform", "jetbrains") - put("client", "jetbrains") - put("feature", "jetbrains-plugin") - runCatching { - val info = ApplicationInfo.getInstance() - put("editorName", info.fullApplicationName) - put("jetbrainsBuild", info.build.asString()) - }.onFailure { log?.info("Could not read ApplicationInfo for environment payload: ${it.message}") } - runCatching { - val version = KiloPlugin.version() - if (version != null) { - put("pluginVersion", version) - put("appVersion", version) - } - }.onFailure { log?.info("Could not read plugin version for environment payload: ${it.message}") } + fun payload(log: KiloLog? = null): Map = buildMap { + put("platform", "jetbrains") + put("client", "jetbrains") + put("feature", "jetbrains-plugin") + runCatching { + val info = ApplicationInfo.getInstance() + put("editorName", info.fullApplicationName) + put("jetbrainsBuild", info.build.asString()) + }.onFailure { log?.info("Could not read ApplicationInfo for environment payload: ${it.message}") } + runCatching { + val version = KiloPlugin.version() + if (version != null) { + put("pluginVersion", version) + put("appVersion", version) + } + }.onFailure { log?.info("Could not read plugin version for environment payload: ${it.message}") } + } } } @@ -102,7 +100,7 @@ internal class FileLog(cls: Class<*>) : KiloLog { private val root: java.util.logging.Logger by lazy { val logger = java.util.logging.Logger.getLogger("ai.kilocode") - val payload = KiloEnvironment.payload().entries.joinToString(" ") { "${it.key}=${it.value}" } + val payload = KiloLog.payload().entries.joinToString(" ") { "${it.key}=${it.value}" } logger.addHandler(handler) logger.useParentHandlers = false logger.level = level From f087379f8976c31c03b604d1ea1c7c0a7bff0d6b Mon Sep 17 00:00:00 2001 From: kirillk Date: Sun, 31 May 2026 15:24:48 -0400 Subject: [PATCH 13/16] fix(jetbrains): remove sandbox telemetry prefix --- .../ai/kilocode/backend/telemetry/KiloBackendTelemetry.kt | 4 ++-- .../ai/kilocode/client/telemetry/KiloTelemetryService.kt | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/kilo-jetbrains/backend/src/main/kotlin/ai/kilocode/backend/telemetry/KiloBackendTelemetry.kt b/packages/kilo-jetbrains/backend/src/main/kotlin/ai/kilocode/backend/telemetry/KiloBackendTelemetry.kt index 6a3fade0d25..ed47d39771d 100644 --- a/packages/kilo-jetbrains/backend/src/main/kotlin/ai/kilocode/backend/telemetry/KiloBackendTelemetry.kt +++ b/packages/kilo-jetbrains/backend/src/main/kotlin/ai/kilocode/backend/telemetry/KiloBackendTelemetry.kt @@ -19,7 +19,7 @@ class KiloBackendTelemetry( suspend fun capture(http: OkHttpClient?, port: Int, event: String, properties: Map) { val body = payload(event, properties) if (KiloDevMode.enabled()) { - log.info("telemetry capture sandbox: $body") + log.info(body) return } if (http == null || port <= 0) return @@ -29,7 +29,7 @@ class KiloBackendTelemetry( suspend fun setEnabled(http: OkHttpClient?, port: Int, enabled: Boolean) { val body = JsonObject(mapOf("enabled" to JsonPrimitive(enabled))).toString() if (KiloDevMode.enabled()) { - log.info("telemetry setEnabled sandbox: $body") + log.info(body) return } if (http == null || port <= 0) return diff --git a/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/telemetry/KiloTelemetryService.kt b/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/telemetry/KiloTelemetryService.kt index 7dbe80eb65d..42dfd0c6d19 100644 --- a/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/telemetry/KiloTelemetryService.kt +++ b/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/telemetry/KiloTelemetryService.kt @@ -33,7 +33,7 @@ class KiloTelemetryService internal constructor( fun send(event: String, properties: Map = emptyMap()) { if (KiloLog.sandbox()) { val payload = KiloLog.payload(LOG) + properties - LOG.info("telemetry capture sandbox: event=$event ${payload.entries.joinToString(" ") { "${it.key}=${it.value}" }}") + LOG.info("event=$event ${payload.entries.joinToString(" ") { "${it.key}=${it.value}" }}") return } cs.launch { From 099b85ae47949a2699674441d3137985d6f96048 Mon Sep 17 00:00:00 2001 From: kirillk Date: Sun, 31 May 2026 15:28:18 -0400 Subject: [PATCH 14/16] fix(jetbrains): avoid duplicate session open telemetry --- .../kotlin/ai/kilocode/client/session/SessionSidePanelManager.kt | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/SessionSidePanelManager.kt b/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/SessionSidePanelManager.kt index b4cfcd3f340..2a1a7e1f10d 100644 --- a/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/SessionSidePanelManager.kt +++ b/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/SessionSidePanelManager.kt @@ -63,6 +63,7 @@ class SessionSidePanelManager( existing } else create(ref) } + if (current === ui) return Telemetry.send("Session Opened", mapOf("source" to ref.type.name.lowercase(), "sessionId" to ref.id)) show(ui) } From c545c32a18911afbeb90749f6d1ff7d85d498de3 Mon Sep 17 00:00:00 2001 From: kirillk Date: Sun, 31 May 2026 16:09:09 -0400 Subject: [PATCH 15/16] fix(jetbrains): harden telemetry threading --- .../backend/app/KiloBackendAppService.kt | 24 +++++++++----- .../backend/telemetry/KiloBackendTelemetry.kt | 11 ++++++- packages/kilo-jetbrains/build.gradle.kts | 5 --- .../session/controller/SessionController.kt | 18 ++++++++--- .../client/telemetry/KiloTelemetryService.kt | 31 +++++++++++++++---- 5 files changed, 65 insertions(+), 24 deletions(-) diff --git a/packages/kilo-jetbrains/backend/src/main/kotlin/ai/kilocode/backend/app/KiloBackendAppService.kt b/packages/kilo-jetbrains/backend/src/main/kotlin/ai/kilocode/backend/app/KiloBackendAppService.kt index a2dda3a6793..240d562cdc9 100644 --- a/packages/kilo-jetbrains/backend/src/main/kotlin/ai/kilocode/backend/app/KiloBackendAppService.kt +++ b/packages/kilo-jetbrains/backend/src/main/kotlin/ai/kilocode/backend/app/KiloBackendAppService.kt @@ -411,16 +411,24 @@ class KiloBackendAppService private constructor( } } - private suspend fun setTelemetry(enabled: Boolean) { - runCatching { - service().setEnabled(connection.apiClient, connection.port, enabled) - }.onFailure { log.info("Skipping telemetry setEnabled: ${it.message}") } + private fun setTelemetry(enabled: Boolean) { + val http = connection.apiClient + val port = connection.port + cs.launch { + runCatching { + service().setEnabled(http, port, enabled) + }.onFailure { log.info("Skipping telemetry setEnabled: ${it.message}") } + } } - private suspend fun captureBackend(event: String, props: Map) { - runCatching { - service().capture(connection.apiClient, connection.port, event, props) - }.onFailure { log.info("Skipping backend telemetry: ${it.message}") } + private fun captureBackend(event: String, props: Map) { + val http = connection.apiClient + val port = connection.port + cs.launch { + runCatching { + service().capture(http, port, event, props) + }.onFailure { log.info("Skipping backend telemetry: ${it.message}") } + } } private suspend fun detectMigration(): LegacyMigrationDetection? = withContext(Dispatchers.IO) { diff --git a/packages/kilo-jetbrains/backend/src/main/kotlin/ai/kilocode/backend/telemetry/KiloBackendTelemetry.kt b/packages/kilo-jetbrains/backend/src/main/kotlin/ai/kilocode/backend/telemetry/KiloBackendTelemetry.kt index ed47d39771d..9f0ee195da1 100644 --- a/packages/kilo-jetbrains/backend/src/main/kotlin/ai/kilocode/backend/telemetry/KiloBackendTelemetry.kt +++ b/packages/kilo-jetbrains/backend/src/main/kotlin/ai/kilocode/backend/telemetry/KiloBackendTelemetry.kt @@ -11,11 +11,16 @@ import okhttp3.MediaType.Companion.toMediaType import okhttp3.OkHttpClient import okhttp3.Request import okhttp3.RequestBody.Companion.toRequestBody +import java.util.concurrent.TimeUnit @Service(Service.Level.APP) class KiloBackendTelemetry( private val log: KiloLog = KiloLog.create(KiloBackendTelemetry::class.java), ) { + companion object { + private const val TIMEOUT_MS = 5_000L + } + suspend fun capture(http: OkHttpClient?, port: Int, event: String, properties: Map) { val body = payload(event, properties) if (KiloDevMode.enabled()) { @@ -39,12 +44,16 @@ class KiloBackendTelemetry( private suspend fun post(http: OkHttpClient, port: Int, path: String, body: String) { withContext(Dispatchers.IO) { try { + val client = http.newBuilder() + .callTimeout(TIMEOUT_MS, TimeUnit.MILLISECONDS) + .readTimeout(TIMEOUT_MS, TimeUnit.MILLISECONDS) + .build() val req = Request.Builder() .url("http://127.0.0.1:$port/$path") .header("Accept", "application/json") .post(body.toRequestBody("application/json".toMediaType())) .build() - http.newCall(req).execute().use { res -> + client.newCall(req).execute().use { res -> if (!res.isSuccessful) log.warn("telemetry $path failed: HTTP ${res.code}") } } catch (e: Exception) { diff --git a/packages/kilo-jetbrains/build.gradle.kts b/packages/kilo-jetbrains/build.gradle.kts index fd4c091eede..06e65495e9d 100644 --- a/packages/kilo-jetbrains/build.gradle.kts +++ b/packages/kilo-jetbrains/build.gradle.kts @@ -1,7 +1,6 @@ import org.jetbrains.changelog.Changelog import org.jetbrains.intellij.platform.gradle.IntelliJPlatformType import org.jetbrains.intellij.platform.gradle.TestFrameworkType -import org.jetbrains.intellij.platform.gradle.tasks.InstrumentCodeTask import org.jetbrains.intellij.platform.gradle.tasks.RunIdeTask import org.jetbrains.intellij.platform.gradle.tasks.aware.SplitModeAware.SplitModeTarget import java.time.LocalDate @@ -171,10 +170,6 @@ intellijPlatform { } tasks { - withType { - enabled = false - } - runIdeBackend { splitModeServerPort.set(splitPort) dependsOn(":backend:prepareLocalCli") diff --git a/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/controller/SessionController.kt b/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/controller/SessionController.kt index 00c51c833ed..bf1a3513555 100644 --- a/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/controller/SessionController.kt +++ b/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/session/controller/SessionController.kt @@ -193,12 +193,13 @@ class SessionController( val start = sid ?: ref?.key ?: "pending" val exists = sid != null val dto = promptDto(text) + val props = promptProps() LOG.debug { "${ChatLogSummary.sid(start)} ${ChatLogSummary.prompt(text)} ${ChatLogSummary.dir(directory)}" } capture("Conversation Send Clicked", sessionProps(sid ?: ref?.key) + mapOf( "source" to "user", "hasExistingSession" to exists.toString(), "textLength" to bucket(text), - ) + promptProps()) + ) + props) showSession() cs.launch { try { @@ -220,7 +221,7 @@ class SessionController( session.id } sessions.prompt(id, directory, dto) - capture("Conversation Message", sessionProps(id) + mapOf("source" to "user", "hasExistingSession" to exists.toString()) + promptProps()) + capture("Conversation Message", sessionProps(id) + mapOf("source" to "user", "hasExistingSession" to exists.toString()) + props) LOG.debug { "${ChatLogSummary.sid(id)} kind=prompt dispatched=true" } } catch (e: Exception) { capture("Session Error", sessionProps(sid ?: ref?.key ?: start) + mapOf("context" to "prompt", "errorClass" to e::class.java.name)) @@ -1384,11 +1385,20 @@ class SessionController( private fun connectionProps(): Map = buildMap { put("appStatus", model.app.status.name) put("workspaceStatus", model.workspace.status.name) - model.app.error?.let { put("appError", it) } - model.workspace.error?.let { put("workspaceError", it) } + model.app.error?.let { put("appError", bucketError(it)) } + model.workspace.error?.let { put("workspaceError", bucketError(it)) } put("warningCount", model.app.warnings.size.toString()) } + private fun bucketError(text: String): String = when { + text.isBlank() -> "empty" + text.contains("timed out", ignoreCase = true) -> "timeout" + text.contains("not connected", ignoreCase = true) -> "not_connected" + text.contains("connection", ignoreCase = true) -> "connection" + text.contains("http", ignoreCase = true) -> "http" + else -> "other" + } + fun dismissLoginRequired() { assertEdt() val active = model.state is SessionState.LoginRequired diff --git a/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/telemetry/KiloTelemetryService.kt b/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/telemetry/KiloTelemetryService.kt index 42dfd0c6d19..0559b03a331 100644 --- a/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/telemetry/KiloTelemetryService.kt +++ b/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/telemetry/KiloTelemetryService.kt @@ -8,8 +8,11 @@ import ai.kilocode.rpc.dto.TelemetryCaptureDto import com.intellij.openapi.components.Service import com.intellij.openapi.components.service import fleet.rpc.client.durable +import kotlinx.coroutines.CancellationException import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.withTimeoutOrNull import kotlinx.coroutines.launch +import java.util.concurrent.atomic.AtomicInteger object Telemetry { fun send(event: String, properties: Map = emptyMap()) { @@ -26,24 +29,40 @@ class KiloTelemetryService internal constructor( companion object { private val LOG = KiloLog.create(KiloTelemetryService::class.java) + private const val MAX_PENDING = 64 + private const val TIMEOUT_MS = 5_000L fun getInstance(): KiloTelemetryService = service() } + private val pending = AtomicInteger() + fun send(event: String, properties: Map = emptyMap()) { - if (KiloLog.sandbox()) { - val payload = KiloLog.payload(LOG) + properties - LOG.info("event=$event ${payload.entries.joinToString(" ") { "${it.key}=${it.value}" }}") + if (pending.incrementAndGet() > MAX_PENDING) { + pending.decrementAndGet() return } cs.launch { try { + if (KiloLog.sandbox()) { + val payload = KiloLog.payload(LOG) + properties + LOG.info("event=$event ${payload.entries.joinToString(" ") { "${it.key}=${it.value}" }}") + return@launch + } val dto = TelemetryCaptureDto(event, properties) - val api = rpc - if (api != null) api.captureTelemetry(dto) - else durable { KiloAppRpcApi.getInstance().captureTelemetry(dto) } + val sent = withTimeoutOrNull(TIMEOUT_MS) { + val api = rpc + if (api != null) api.captureTelemetry(dto) + else durable { KiloAppRpcApi.getInstance().captureTelemetry(dto) } + true + } + if (sent != true) LOG.warn("telemetry capture timed out: $event") + } catch (e: CancellationException) { + throw e } catch (e: Exception) { LOG.warn("telemetry capture failed: ${e.message}", e) + } finally { + pending.decrementAndGet() } } } From cb066d2f3cf1e2ff864a1f377843815f31e50e37 Mon Sep 17 00:00:00 2001 From: kirillk Date: Sun, 31 May 2026 16:59:31 -0400 Subject: [PATCH 16/16] fix(jetbrains): log telemetry backpressure --- .../ai/kilocode/client/telemetry/KiloTelemetryService.kt | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/telemetry/KiloTelemetryService.kt b/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/telemetry/KiloTelemetryService.kt index 0559b03a331..5346fcface1 100644 --- a/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/telemetry/KiloTelemetryService.kt +++ b/packages/kilo-jetbrains/frontend/src/main/kotlin/ai/kilocode/client/telemetry/KiloTelemetryService.kt @@ -12,6 +12,7 @@ import kotlinx.coroutines.CancellationException import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.withTimeoutOrNull import kotlinx.coroutines.launch +import java.util.concurrent.atomic.AtomicBoolean import java.util.concurrent.atomic.AtomicInteger object Telemetry { @@ -36,10 +37,14 @@ class KiloTelemetryService internal constructor( } private val pending = AtomicInteger() + private val warned = AtomicBoolean() fun send(event: String, properties: Map = emptyMap()) { if (pending.incrementAndGet() > MAX_PENDING) { pending.decrementAndGet() + if (warned.compareAndSet(false, true)) { + LOG.warn("telemetry backpressure: dropping events with more than $MAX_PENDING pending") + } return } cs.launch {