Skip to content

Commit 2720828

Browse files
authored
telemetry(amazonq): Add changed IDE diagnostics after user acceptance (#5613)
When user accepts suggestion from inline completion or chat, there can be a change in the current open editor's IDE diagnostics, this can be used as a measure for code suggestion quality. Ref aws/aws-toolkit-vscode#7130. This change is part of the server side workspace context. To reduce the risk of large blast radius, this change is only applied for users who are in the experiment of server side project context (both treatment and control), which is the Amzn idc users. Verified backend request id 6faef702-938e-42eb-894a-957d9757186e
1 parent 3668c5f commit 2720828

File tree

11 files changed

+267
-2
lines changed

11 files changed

+267
-2
lines changed

plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/cwc/controller/ChatController.kt

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ import com.intellij.openapi.fileEditor.FileEditorManager
2121
import com.intellij.openapi.options.ShowSettingsUtil
2222
import com.intellij.psi.PsiDocumentManager
2323
import kotlinx.coroutines.cancelChildren
24+
import kotlinx.coroutines.delay
2425
import kotlinx.coroutines.flow.catch
2526
import kotlinx.coroutines.flow.filter
2627
import kotlinx.coroutines.flow.first
@@ -36,6 +37,7 @@ import software.aws.toolkits.core.utils.getLogger
3637
import software.aws.toolkits.core.utils.info
3738
import software.aws.toolkits.core.utils.warn
3839
import software.aws.toolkits.jetbrains.core.coroutines.EDT
40+
import software.aws.toolkits.jetbrains.core.credentials.sono.isInternalUser
3941
import software.aws.toolkits.jetbrains.services.amazonq.apps.AmazonQAppInitContext
4042
import software.aws.toolkits.jetbrains.services.amazonq.auth.AuthController
4143
import software.aws.toolkits.jetbrains.services.amazonq.auth.AuthNeededState
@@ -49,6 +51,8 @@ import software.aws.toolkits.jetbrains.services.codewhisperer.settings.CodeWhisp
4951
import software.aws.toolkits.jetbrains.services.codewhisperer.telemetry.CodeWhispererUserModificationTracker
5052
import software.aws.toolkits.jetbrains.services.codewhisperer.telemetry.QFeatureEvent
5153
import software.aws.toolkits.jetbrains.services.codewhisperer.telemetry.broadcastQEvent
54+
import software.aws.toolkits.jetbrains.services.codewhisperer.util.getDiagnosticDifferences
55+
import software.aws.toolkits.jetbrains.services.codewhisperer.util.getDocumentDiagnostics
5256
import software.aws.toolkits.jetbrains.services.cwc.InboundAppMessagesHandler
5357
import software.aws.toolkits.jetbrains.services.cwc.clients.chat.exceptions.ChatApiException
5458
import software.aws.toolkits.jetbrains.services.cwc.clients.chat.model.ChatRequestData
@@ -214,6 +218,7 @@ class ChatController private constructor(
214218
val caret: Caret = editor.caretModel.primaryCaret
215219
val offset: Int = caret.offset
216220

221+
val oldDiagnostics = getDocumentDiagnostics(editor.document, context.project)
217222
ApplicationManager.getApplication().runWriteAction {
218223
WriteCommandAction.runWriteCommandAction(context.project) {
219224
if (caret.hasSelection()) {
@@ -236,6 +241,12 @@ class ChatController private constructor(
236241
)
237242
}
238243
}
244+
if (isInternalUser(getStartUrl(context.project))) {
245+
// wait for the IDE itself to update its diagnostics for current file
246+
delay(500)
247+
val newDiagnostics = getDocumentDiagnostics(editor.document, context.project)
248+
message.diagnosticsDifferences = getDiagnosticDifferences(oldDiagnostics, newDiagnostics)
249+
}
239250
}
240251
telemetryHelper.recordInteractWithMessage(message)
241252

plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/cwc/controller/chat/telemetry/TelemetryHelper.kt

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -270,6 +270,8 @@ class TelemetryHelper(private val project: Project, private val sessionStorage:
270270
acceptedCharacterCount(message.code.length)
271271
acceptedLineCount(message.code.lines().size)
272272
hasProjectLevelContext(getMessageHasProjectContext(message.messageId))
273+
addedIdeDiagnostics(message.diagnosticsDifferences?.added)
274+
removedIdeDiagnostics(message.diagnosticsDifferences?.removed)
273275
}.build()
274276
}
275277

plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/cwc/messages/CwcMessage.kt

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ import software.aws.toolkits.jetbrains.services.amazonq.auth.AuthFollowUpType
1818
import software.aws.toolkits.jetbrains.services.amazonq.messages.AmazonQMessage
1919
import software.aws.toolkits.jetbrains.services.amazonq.onboarding.OnboardingPageInteractionType
2020
import software.aws.toolkits.jetbrains.services.amazonq.util.HighlightCommand
21+
import software.aws.toolkits.jetbrains.services.codewhisperer.util.DiagnosticDifferences
2122
import software.aws.toolkits.jetbrains.services.cwc.clients.chat.model.FollowUpType
2223
import java.time.Instant
2324

@@ -95,6 +96,7 @@ sealed interface IncomingCwcMessage : CwcMessage {
9596
val codeBlockIndex: Int?,
9697
val totalCodeBlocks: Int?,
9798
val codeBlockLanguage: String?,
99+
var diagnosticsDifferences: DiagnosticDifferences?,
98100
) : IncomingCwcMessage, TabId, MessageId
99101

100102
data class TriggerTabIdReceived(

plugins/amazonq/chat/jetbrains-community/tst/software/aws/toolkits/jetbrains/services/amazonq/TelemetryHelperTest.kt

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,7 @@ import software.aws.toolkits.jetbrains.services.amazonq.apps.AmazonQAppInitConte
4444
import software.aws.toolkits.jetbrains.services.codewhisperer.credentials.CodeWhispererClientAdaptor
4545
import software.aws.toolkits.jetbrains.services.codewhisperer.customization.CodeWhispererCustomization
4646
import software.aws.toolkits.jetbrains.services.codewhisperer.customization.CodeWhispererModelConfigurator
47+
import software.aws.toolkits.jetbrains.services.codewhisperer.util.DiagnosticDifferences
4748
import software.aws.toolkits.jetbrains.services.cwc.clients.chat.ChatSession
4849
import software.aws.toolkits.jetbrains.services.cwc.clients.chat.model.ChatRequestData
4950
import software.aws.toolkits.jetbrains.services.cwc.clients.chat.model.CodeNamesImpl
@@ -475,6 +476,7 @@ class TelemetryHelperTest {
475476
val inserTionTargetType = "insertionTargetType"
476477
val eventId = "eventId"
477478
val code = "println()"
479+
val diagnosticDifferences = DiagnosticDifferences(emptyList(), emptyList())
478480

479481
sut.recordInteractWithMessage(
480482
IncomingCwcMessage.InsertCodeAtCursorPosition(
@@ -487,7 +489,8 @@ class TelemetryHelperTest {
487489
eventId,
488490
codeBlockIndex,
489491
totalCodeBlocks,
490-
lang
492+
lang,
493+
diagnosticDifferences
491494
)
492495
)
493496

@@ -503,6 +506,8 @@ class TelemetryHelperTest {
503506
acceptedLineCount(code.lines().size)
504507
customizationArn(customizationArn)
505508
hasProjectLevelContext(false)
509+
addedIdeDiagnostics(emptyList())
510+
removedIdeDiagnostics(emptyList())
506511
}.build()
507512
)
508513
)

plugins/amazonq/codewhisperer/jetbrains-community/src/software/aws/toolkits/jetbrains/services/codewhisperer/credentials/CodeWhispererClientAdaptor.kt

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@ import software.amazon.awssdk.services.codewhispererruntime.model.TargetCode
3737
import software.amazon.awssdk.services.codewhispererruntime.model.UserIntent
3838
import software.aws.toolkits.core.utils.debug
3939
import software.aws.toolkits.core.utils.getLogger
40+
import software.aws.toolkits.jetbrains.core.credentials.sono.isInternalUser
4041
import software.aws.toolkits.jetbrains.services.amazonq.codeWhispererUserContext
4142
import software.aws.toolkits.jetbrains.services.amazonq.profile.QRegionProfileManager
4243
import software.aws.toolkits.jetbrains.services.codewhisperer.customization.CodeWhispererCustomization
@@ -47,6 +48,10 @@ import software.aws.toolkits.jetbrains.services.codewhisperer.service.RequestCon
4748
import software.aws.toolkits.jetbrains.services.codewhisperer.service.ResponseContext
4849
import software.aws.toolkits.jetbrains.services.codewhisperer.util.CodeWhispererConstants
4950
import software.aws.toolkits.jetbrains.services.codewhisperer.util.CodeWhispererUtil.getTelemetryOptOutPreference
51+
import software.aws.toolkits.jetbrains.services.codewhisperer.util.DiagnosticDifferences
52+
import software.aws.toolkits.jetbrains.services.codewhisperer.util.getDiagnosticDifferences
53+
import software.aws.toolkits.jetbrains.services.codewhisperer.util.getDocumentDiagnostics
54+
import software.aws.toolkits.jetbrains.services.cwc.controller.chat.telemetry.getStartUrl
5055
import software.aws.toolkits.telemetry.CodewhispererCompletionType
5156
import software.aws.toolkits.telemetry.CodewhispererSuggestionState
5257
import java.time.Instant
@@ -340,7 +345,17 @@ open class CodeWhispererClientAdaptorImpl(override val project: Project) : CodeW
340345
) {
341346
e2eLatency = 0.0
342347
}
343-
348+
var diffDiagnostics = DiagnosticDifferences(
349+
added = emptyList(),
350+
removed = emptyList()
351+
)
352+
if (suggestionState == CodewhispererSuggestionState.Accept && isInternalUser(getStartUrl(project))) {
353+
val oldDiagnostics = requestContext.diagnostics.orEmpty()
354+
// wait for the IDE itself to update its diagnostics for current file
355+
Thread.sleep(500)
356+
val newDiagnostics = getDocumentDiagnostics(requestContext.editor.document, project)
357+
diffDiagnostics = getDiagnosticDifferences(oldDiagnostics, newDiagnostics)
358+
}
344359
return bearerClient().sendTelemetryEvent { requestBuilder ->
345360
requestBuilder.telemetryEvent { telemetryEventBuilder ->
346361
telemetryEventBuilder.userTriggerDecisionEvent {
@@ -358,6 +373,8 @@ open class CodeWhispererClientAdaptorImpl(override val project: Project) : CodeW
358373
it.customizationArn(requestContext.customizationArn.nullize(nullizeSpaces = true))
359374
it.numberOfRecommendations(numberOfRecommendations)
360375
it.acceptedCharacterCount(acceptedCharCount)
376+
it.addedIdeDiagnostics(diffDiagnostics.added)
377+
it.removedIdeDiagnostics(diffDiagnostics.removed)
361378
}
362379
}
363380
requestBuilder.optOutPreference(getTelemetryOptOutPreference())

plugins/amazonq/codewhisperer/jetbrains-community/src/software/aws/toolkits/jetbrains/services/codewhisperer/service/CodeWhispererService.kt

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@ import software.amazon.awssdk.services.codewhispererruntime.model.Completion
3636
import software.amazon.awssdk.services.codewhispererruntime.model.FileContext
3737
import software.amazon.awssdk.services.codewhispererruntime.model.GenerateCompletionsRequest
3838
import software.amazon.awssdk.services.codewhispererruntime.model.GenerateCompletionsResponse
39+
import software.amazon.awssdk.services.codewhispererruntime.model.IdeDiagnostic
3940
import software.amazon.awssdk.services.codewhispererruntime.model.ProgrammingLanguage
4041
import software.amazon.awssdk.services.codewhispererruntime.model.RecommendationsWithReferencesPreference
4142
import software.amazon.awssdk.services.codewhispererruntime.model.ResourceNotFoundException
@@ -87,6 +88,7 @@ import software.aws.toolkits.jetbrains.services.codewhisperer.util.CodeWhisperer
8788
import software.aws.toolkits.jetbrains.services.codewhisperer.util.CodeWhispererUtil.notifyErrorCodeWhispererUsageLimit
8889
import software.aws.toolkits.jetbrains.services.codewhisperer.util.CodeWhispererUtil.promptReAuth
8990
import software.aws.toolkits.jetbrains.services.codewhisperer.util.FileContextProvider
91+
import software.aws.toolkits.jetbrains.services.codewhisperer.util.getDocumentDiagnostics
9092
import software.aws.toolkits.jetbrains.settings.CodeWhispererSettings
9193
import software.aws.toolkits.jetbrains.utils.isInjectedText
9294
import software.aws.toolkits.jetbrains.utils.isQExpired
@@ -691,6 +693,7 @@ class CodeWhispererService(private val cs: CoroutineScope) : Disposable {
691693
} catch (e: Exception) {
692694
LOG.warn { "Cannot get workspaceId from LSP'$e'" }
693695
}
696+
val diagnostics = getDocumentDiagnostics(editor.document, project)
694697
return RequestContext(
695698
project,
696699
editor,
@@ -703,6 +706,7 @@ class CodeWhispererService(private val cs: CoroutineScope) : Disposable {
703706
customizationArn,
704707
profileArn,
705708
workspaceId,
709+
diagnostics
706710
)
707711
}
708712

@@ -895,6 +899,7 @@ data class RequestContext(
895899
val customizationArn: String?,
896900
val profileArn: String?,
897901
val workspaceId: String?,
902+
val diagnostics: List<IdeDiagnostic>?,
898903
) {
899904
// TODO: should make the entire getRequestContext() suspend function instead of making supplemental context only
900905
var supplementalContext: SupplementalContextInfo? = null

plugins/amazonq/codewhisperer/jetbrains-community/src/software/aws/toolkits/jetbrains/services/codewhisperer/util/CodeWhispererUtil.kt

Lines changed: 92 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,12 +3,16 @@
33

44
package software.aws.toolkits.jetbrains.services.codewhisperer.util
55

6+
import com.intellij.codeInsight.daemon.impl.HighlightInfo
67
import com.intellij.codeInsight.lookup.LookupManager
78
import com.intellij.ide.BrowserUtil
9+
import com.intellij.lang.annotation.HighlightSeverity
810
import com.intellij.notification.NotificationAction
911
import com.intellij.openapi.application.ApplicationManager
1012
import com.intellij.openapi.application.runInEdt
13+
import com.intellij.openapi.editor.Document
1114
import com.intellij.openapi.editor.Editor
15+
import com.intellij.openapi.editor.impl.DocumentMarkupModel
1216
import com.intellij.openapi.editor.impl.EditorImpl
1317
import com.intellij.openapi.project.Project
1418
import com.intellij.openapi.vfs.VfsUtil
@@ -22,7 +26,10 @@ import kotlinx.coroutines.delay
2226
import kotlinx.coroutines.launch
2327
import kotlinx.coroutines.yield
2428
import software.amazon.awssdk.services.codewhispererruntime.model.Completion
29+
import software.amazon.awssdk.services.codewhispererruntime.model.IdeDiagnostic
2530
import software.amazon.awssdk.services.codewhispererruntime.model.OptOutPreference
31+
import software.amazon.awssdk.services.codewhispererruntime.model.Position
32+
import software.amazon.awssdk.services.codewhispererruntime.model.Range
2633
import software.aws.toolkits.core.utils.getLogger
2734
import software.aws.toolkits.core.utils.warn
2835
import software.aws.toolkits.jetbrains.core.credentials.AwsBearerTokenConnection
@@ -361,3 +368,88 @@ object CodeWhispererUtil {
361368
enum class CaretMovement {
362369
NO_CHANGE, MOVE_FORWARD, MOVE_BACKWARD
363370
}
371+
372+
fun getDiagnosticsType(message: String): String {
373+
val lowercaseMessage = message.lowercase()
374+
375+
val diagnosticPatterns = mapOf(
376+
"TYPE_ERROR" to listOf("type", "cast"),
377+
"SYNTAX_ERROR" to listOf("expected", "indent", "syntax"),
378+
"REFERENCE_ERROR" to listOf("undefined", "not defined", "undeclared", "reference", "symbol"),
379+
"BEST_PRACTICE" to listOf("deprecated", "unused", "uninitialized", "not initialized"),
380+
"SECURITY" to listOf("security", "vulnerability")
381+
)
382+
383+
return diagnosticPatterns
384+
.entries
385+
.firstOrNull { (_, keywords) ->
386+
keywords.any { lowercaseMessage.contains(it) }
387+
}
388+
?.key ?: "OTHER"
389+
}
390+
391+
fun convertSeverity(severity: HighlightSeverity): String = when {
392+
severity == HighlightSeverity.ERROR -> "ERROR"
393+
severity == HighlightSeverity.WARNING ||
394+
severity == HighlightSeverity.WEAK_WARNING -> "WARNING"
395+
severity == HighlightSeverity.INFORMATION -> "INFORMATION"
396+
severity.toString().contains("TEXT", ignoreCase = true) -> "HINT"
397+
severity == HighlightSeverity.INFO -> "INFORMATION"
398+
// For severities that might indicate performance issues
399+
severity.toString().contains("PERFORMANCE", ignoreCase = true) -> "WARNING"
400+
// For deprecation warnings
401+
severity.toString().contains("DEPRECATED", ignoreCase = true) -> "WARNING"
402+
// Default case
403+
else -> "INFORMATION"
404+
}
405+
406+
fun getDocumentDiagnostics(document: Document, project: Project): List<IdeDiagnostic> = runCatching {
407+
DocumentMarkupModel.forDocument(document, project, true)
408+
.allHighlighters
409+
.mapNotNull { it.errorStripeTooltip as? HighlightInfo }
410+
.filter { !it.description.isNullOrEmpty() }
411+
.map { info ->
412+
val startLine = document.getLineNumber(info.startOffset)
413+
val endLine = document.getLineNumber(info.endOffset)
414+
415+
IdeDiagnostic.builder()
416+
.ideDiagnosticType(getDiagnosticsType(info.description))
417+
.severity(convertSeverity(info.severity))
418+
.source(info.inspectionToolId)
419+
.range(
420+
Range.builder()
421+
.start(
422+
Position.builder()
423+
.line(startLine)
424+
.character(document.getLineStartOffset(startLine))
425+
.build()
426+
)
427+
.end(
428+
Position.builder()
429+
.line(endLine)
430+
.character(document.getLineStartOffset(endLine))
431+
.build()
432+
)
433+
.build()
434+
)
435+
.build()
436+
}
437+
}.getOrElse { e ->
438+
getLogger<CodeWhispererUtil>().warn { "Failed to get document diagnostics ${e.message}" }
439+
emptyList()
440+
}
441+
442+
data class DiagnosticDifferences(
443+
val added: List<IdeDiagnostic>,
444+
val removed: List<IdeDiagnostic>,
445+
)
446+
447+
fun serializeDiagnostics(diagnostic: IdeDiagnostic): String = "${diagnostic.source()}-${diagnostic.severity()}-${diagnostic.ideDiagnosticType()}"
448+
449+
fun getDiagnosticDifferences(oldDiagnostic: List<IdeDiagnostic>, newDiagnostic: List<IdeDiagnostic>): DiagnosticDifferences {
450+
val oldSet = oldDiagnostic.map { i -> serializeDiagnostics(i) }.toSet()
451+
val newSet = newDiagnostic.map { i -> serializeDiagnostics(i) }.toSet()
452+
val added = newDiagnostic.filter { i -> !oldSet.contains(serializeDiagnostics(i)) }.distinctBy { serializeDiagnostics(it) }
453+
val removed = oldDiagnostic.filter { i -> !newSet.contains(serializeDiagnostics(i)) }.distinctBy { serializeDiagnostics(it) }
454+
return DiagnosticDifferences(added, removed)
455+
}

plugins/amazonq/codewhisperer/jetbrains-community/tst/software/aws/toolkits/jetbrains/services/codewhisperer/CodeWhispererCodeCoverageTrackerTest.kt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -179,6 +179,7 @@ internal class CodeWhispererCodeCoverageTrackerTestPython : CodeWhispererCodeCov
179179
aString(),
180180
aString(),
181181
aString(),
182+
emptyList()
182183
)
183184
val responseContext = ResponseContext("sessionId")
184185
val recommendationContext = RecommendationContext(

plugins/amazonq/codewhisperer/jetbrains-community/tst/software/aws/toolkits/jetbrains/services/codewhisperer/CodeWhispererServiceTest.kt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -215,6 +215,7 @@ class CodeWhispererServiceTest {
215215
customizationArn = "fake-arn",
216216
profileArn = "fake-arn",
217217
workspaceId = null,
218+
diagnostics = emptyList()
218219
)
219220
)
220221

0 commit comments

Comments
 (0)