Merge pull request #11616 from Kilo-Org/jetbrains-prompt-usage-telemetry

feat(jetbrains): track prompt slash and mention usage
This commit is contained in:
Kirill Kalishev
2026-06-25 13:42:52 -06:00
committed by GitHub
4 changed files with 69 additions and 3 deletions
@@ -642,7 +642,10 @@ class SessionUi(
spec.name,
KiloBundle.message(spec.descriptionKey),
spec.hints,
action,
{
Telemetry.send("Slash Command Used", mapOf("slashCommandType" to "client", "command" to spec.name))
action()
},
)
private fun mentionActions(): List<MentionAction> = MentionAction.ALL.map(::bind)
@@ -268,18 +268,19 @@ class SessionController(
private fun dispatch(data: Dispatch, send: suspend (String) -> Unit) {
assertEdt()
val props = data.props + if (data.kind == "command") slashProps() else emptyMap()
capture("Conversation Send Clicked", sessionProps(sid ?: ref?.key) + mapOf(
"source" to data.source,
"hasExistingSession" to data.exists.toString(),
"textLength" to bucket(data.text),
) + data.props)
) + props)
showSession()
val pending = sid?.let { CompletableDeferred(it) } ?: session()
cs.launch {
try {
val id = pending.await() ?: return@launch
send(id)
capture("Conversation Message", sessionProps(id) + mapOf("source" to data.source, "hasExistingSession" to data.exists.toString()) + data.props)
capture("Conversation Message", sessionProps(id) + mapOf("source" to data.source, "hasExistingSession" to data.exists.toString()) + props)
LOG.debug { "${ChatLogSummary.sid(id)} kind=${data.kind} dispatched=true" }
} catch (e: Exception) {
capture("Session Error", sessionProps(sid ?: ref?.key ?: data.start) + mapOf("context" to data.kind, "errorClass" to e::class.java.name))
@@ -1531,8 +1532,18 @@ class SessionController(
put("attachmentCount", files.size.toString())
put("mediaAttachmentCount", files.count { it.mime?.startsWith("image/") == true || it.mime == "application/pdf" }.toString())
}
val mentions = files.filter { it.source?.text?.value?.startsWith("@") == true }
if (mentions.isNotEmpty()) {
val resources = mentions.count { it.source?.path == "git-changes" }
put("hasMentions", "true")
put("mentionCount", mentions.size.toString())
put("fileMentionCount", (mentions.size - resources).toString())
put("resourceMentionCount", resources.toString())
}
}
private fun slashProps() = mapOf("hasSlashCommand" to "true", "slashCommandType" to "server")
private fun bucket(text: String): String = when (text.length) {
0 -> "empty"
in 1..80 -> "short"
@@ -48,8 +48,12 @@ class CommandLifecycleTest : SessionControllerTestBase() {
val sent = appRpc.telemetry.single { it.event == "Conversation Send Clicked" }
assertEquals("command", sent.properties["source"])
assertEquals("true", sent.properties["hasSlashCommand"])
assertEquals("server", sent.properties["slashCommandType"])
val message = appRpc.telemetry.single { it.event == "Conversation Message" }
assertEquals("command", message.properties["source"])
assertEquals("true", message.properties["hasSlashCommand"])
assertEquals("server", message.properties["slashCommandType"])
}
fun `test command errors set state and telemetry`() {
@@ -9,11 +9,14 @@ import ai.kilocode.rpc.dto.AgentDto
import ai.kilocode.rpc.dto.ChatEventDto
import ai.kilocode.rpc.dto.ModelDto
import ai.kilocode.rpc.dto.PartDto
import ai.kilocode.rpc.dto.PartSourceDto
import ai.kilocode.rpc.dto.PartSourceTextDto
import ai.kilocode.rpc.dto.PermissionAlwaysRulesDto
import ai.kilocode.rpc.dto.PermissionFileDiffDto
import ai.kilocode.rpc.dto.PermissionReplyDto
import ai.kilocode.rpc.dto.PermissionRequestDto
import ai.kilocode.rpc.dto.ProviderDto
import ai.kilocode.rpc.dto.PromptPartDto
import ai.kilocode.rpc.dto.QuestionInfoDto
import ai.kilocode.rpc.dto.QuestionOptionDto
import ai.kilocode.rpc.dto.QuestionReplyDto
@@ -45,6 +48,51 @@ class PromptLifecycleTest : SessionControllerTestBase() {
assertEquals("short", event.properties["textLength"])
}
fun `test prompt records aggregate mention telemetry`() {
appRpc.state.value = ai.kilocode.rpc.dto.KiloAppStateDto(ai.kilocode.rpc.dto.KiloAppStatusDto.READY, config = ai.kilocode.rpc.dto.ConfigDto(model = "kilo/gpt-5"))
projectRpc.state.value = workspaceReady()
val m = controller()
val files = listOf(
PromptPartDto(
type = "file",
mime = "text/plain",
url = "file:///repo/src/A.kt",
source = PartSourceDto("file", PartSourceTextDto("@src/A.kt", 0.0, 9.0), path = "src/A.kt"),
),
PromptPartDto(
type = "file",
mime = "text/plain",
url = "file:///repo/src/B.kt",
source = PartSourceDto("file", PartSourceTextDto("@src/B.kt", 10.0, 19.0), path = "/repo/src/B.kt"),
),
PromptPartDto(
type = "text",
text = "diff",
source = PartSourceDto("resource", PartSourceTextDto("@git-changes", 20.0, 32.0), path = "git-changes"),
),
)
flush()
edt { m.prompt("review", files) }
flush()
val sent = appRpc.telemetry.single { it.event == "Conversation Send Clicked" }
assertEquals("true", sent.properties["hasMentions"])
assertEquals("3", sent.properties["mentionCount"])
assertEquals("2", sent.properties["fileMentionCount"])
assertEquals("1", sent.properties["resourceMentionCount"])
assertFalse(sent.properties.containsValue("@src/A.kt"))
assertFalse(sent.properties.containsValue("src/A.kt"))
assertFalse(sent.properties.containsValue("/repo/src/B.kt"))
val message = appRpc.telemetry.single { it.event == "Conversation Message" }
assertEquals("true", message.properties["hasMentions"])
assertEquals("3", message.properties["mentionCount"])
assertEquals("2", message.properties["fileMentionCount"])
assertEquals("1", message.properties["resourceMentionCount"])
assertFalse(message.properties.containsValue("@git-changes"))
assertFalse(message.properties.containsValue("git-changes"))
}
fun `test PermissionAsked moves state to AwaitingPermission`() {
val (m, _, _) = prompted()