Skip to content

Commit fef7450

Browse files
authored
fix(codewhisperer): Improve Code percentage reporting (#4120)
* update codeCoverage API with new fields * improve code percentage reporting of CW
1 parent 5be4b1c commit fef7450

File tree

8 files changed

+91
-48
lines changed

8 files changed

+91
-48
lines changed
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
{
2+
"type" : "bugfix",
3+
"description" : "CodeWhisperer: Improve CodePercentage telemetry reporting"
4+
}

jetbrains-core/src/software/aws/toolkits/jetbrains/services/codewhisperer/credentials/CodeWhispererClientAdaptor.kt

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -100,7 +100,8 @@ interface CodeWhispererClientAdaptor : Disposable {
100100
language: CodeWhispererProgrammingLanguage,
101101
customizationArn: String?,
102102
acceptedTokenCount: Int,
103-
totalTokenCount: Int
103+
totalTokenCount: Int,
104+
unmodifiedAcceptedTokenCount: Int?
104105
): SendTelemetryEventResponse
105106

106107
fun sendUserModificationTelemetry(
@@ -297,7 +298,8 @@ open class CodeWhispererClientAdaptorImpl(override val project: Project) : CodeW
297298
language: CodeWhispererProgrammingLanguage,
298299
customizationArn: String?,
299300
acceptedTokenCount: Int,
300-
totalTokenCount: Int
301+
totalTokenCount: Int,
302+
unmodifiedAcceptedTokenCount: Int?
301303
): SendTelemetryEventResponse = bearerClient().sendTelemetryEvent { requestBuilder ->
302304
requestBuilder.telemetryEvent { telemetryEventBuilder ->
303305
telemetryEventBuilder.codeCoverageEvent {
@@ -306,6 +308,7 @@ open class CodeWhispererClientAdaptorImpl(override val project: Project) : CodeW
306308
it.acceptedCharacterCount(acceptedTokenCount)
307309
it.totalCharacterCount(totalTokenCount)
308310
it.timestamp(Instant.now())
311+
it.unmodifiedAcceptedCharacterCount(unmodifiedAcceptedTokenCount)
309312
}
310313
}
311314
requestBuilder.optOutPreference(getTelemetryOptOutPreference())

jetbrains-core/src/software/aws/toolkits/jetbrains/services/codewhisperer/telemetry/CodeWhispererCodeCoverageTracker.kt

Lines changed: 24 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -85,6 +85,10 @@ abstract class CodeWhispererCodeCoverageTracker(
8585
rangeMarkers.add(rangeMarker)
8686
val originalRecommendation = extractRangeMarkerString(rangeMarker) ?: return
8787
rangeMarker.putUserData(KEY_REMAINING_RECOMMENDATION, originalRecommendation)
88+
runReadAction {
89+
// also increment total tokens because accepted tokens are part of it
90+
incrementTotalTokens(rangeMarker.document, originalRecommendation.length)
91+
}
8892
}
8993
}
9094
)
@@ -112,12 +116,16 @@ abstract class CodeWhispererCodeCoverageTracker(
112116
LOG.debug { "event with isWholeTextReplaced flag: $event" }
113117
if (event.oldTimeStamp == 0L) return
114118
}
115-
// This case capture IDE reformatting the document, which will be blank string
116-
if (isDocumentEventFromReformatting(event)) return
117-
118-
// Don't capture deletion events
119-
if (event.newLength <= event.oldLength) return
120-
incrementTotalTokens(event.document, event.newLength - event.oldLength)
119+
// only count total tokens when it is a user keystroke input
120+
// do not count doc changes from copy & paste of >=2 characters
121+
// do not count other changes from formatter, git command, etc
122+
// edge case: event can be from user hit enter with indentation where change is \n\t\t, count as 1 char increase in total chars
123+
// when event is auto closing [{(', there will be 2 separated events, both count as 1 char increase in total chars
124+
val text = event.newFragment.toString()
125+
if ((event.newLength == 1 && event.oldLength == 0) || (text.startsWith('\n') && text.trim().isEmpty())) {
126+
incrementTotalTokens(event.document, 1)
127+
return
128+
}
121129
}
122130

123131
internal fun extractRangeMarkerString(rangeMarker: RangeMarker): String? = runReadAction {
@@ -173,9 +181,6 @@ abstract class CodeWhispererCodeCoverageTracker(
173181
}
174182
}
175183

176-
private fun isDocumentEventFromReformatting(event: DocumentEvent): Boolean =
177-
(event.newFragment.toString().isBlank() && event.oldFragment.toString().isBlank()) && (event.oldLength == 0 || event.newLength == 0)
178-
179184
private fun reset() {
180185
startTime = Instant.now()
181186
rangeMarkers.clear()
@@ -184,8 +189,12 @@ abstract class CodeWhispererCodeCoverageTracker(
184189
}
185190

186191
internal fun emitCodeWhispererCodeContribution() {
187-
// If the user is inactive, don't emit the telemetry
192+
// If the user is inactive or did not invoke, don't emit the telemetry
188193
if (percentage == null) return
194+
if (myServiceInvocationCount.get() <= 0) return
195+
196+
// accepted char count without considering modification
197+
var rawAcceptedCharacterCount = 0
189198
rangeMarkers.forEach { rangeMarker ->
190199
if (!rangeMarker.isValid) return@forEach
191200
// if users add more code upon the recommendation generated from CodeWhisperer, we consider those added part as userToken but not CwsprTokens
@@ -199,6 +208,7 @@ abstract class CodeWhispererCodeCoverageTracker(
199208
}
200209
return@forEach
201210
}
211+
rawAcceptedCharacterCount += originalRecommendation.length
202212
val delta = getAcceptedTokensDelta(originalRecommendation, modifiedRecommendation)
203213
runReadAction {
204214
incrementAcceptedTokens(rangeMarker.document, delta)
@@ -207,12 +217,14 @@ abstract class CodeWhispererCodeCoverageTracker(
207217
val customizationArn: String? = CodeWhispererModelConfigurator.getInstance().activeCustomization(project)?.arn
208218

209219
runIfIdcConnectionOrTelemetryEnabled(project) {
220+
// here acceptedTokensSize is the count of accepted chars post user modification
210221
try {
211222
val response = CodeWhispererClientAdaptor.getInstance(project).sendCodePercentageTelemetry(
212223
language,
213224
customizationArn,
214-
acceptedTokensSize,
215-
totalTokensSize
225+
rawAcceptedCharacterCount,
226+
totalTokensSize,
227+
acceptedTokensSize
216228
)
217229
LOG.debug { "Successfully sent code percentage telemetry. RequestId: ${response.responseMetadata().requestId()}" }
218230
} catch (e: Exception) {

jetbrains-core/tst/software/aws/toolkits/jetbrains/services/codewhisperer/CodeWhispererClientAdaptorTest.kt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -387,7 +387,7 @@ class CodeWhispererClientAdaptorTest {
387387
@Test
388388
fun `sendTelemetryEvent for codePercentage respects telemetry optin status`() {
389389
sendTelemetryEventOptOutCheckHelper {
390-
sut.sendCodePercentageTelemetry(aProgrammingLanguage(), aString(), 0, 1)
390+
sut.sendCodePercentageTelemetry(aProgrammingLanguage(), aString(), 0, 1, 0)
391391
}
392392
}
393393

jetbrains-core/tst/software/aws/toolkits/jetbrains/services/codewhisperer/CodeWhispererCodeCoverageTrackerTest.kt

Lines changed: 33 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@ import software.aws.toolkits.core.telemetry.TelemetryBatcher
3737
import software.aws.toolkits.core.telemetry.TelemetryPublisher
3838
import software.aws.toolkits.core.utils.test.aString
3939
import software.aws.toolkits.jetbrains.core.MockClientManagerRule
40+
import software.aws.toolkits.jetbrains.services.codewhisperer.CodeWhispererTestUtil.keystrokeInput
4041
import software.aws.toolkits.jetbrains.services.codewhisperer.CodeWhispererTestUtil.pythonFileName
4142
import software.aws.toolkits.jetbrains.services.codewhisperer.CodeWhispererTestUtil.pythonResponse
4243
import software.aws.toolkits.jetbrains.services.codewhisperer.CodeWhispererTestUtil.pythonTestLeftContext
@@ -78,8 +79,9 @@ internal abstract class CodeWhispererCodeCoverageTrackerTestBase(myProjectRule:
7879
timeWindowInSec: Long,
7980
language: CodeWhispererProgrammingLanguage,
8081
rangeMarkers: MutableList<RangeMarker> = mutableListOf(),
81-
codeCoverageTokens: MutableMap<Document, CodeCoverageTokens> = mutableMapOf()
82-
) : CodeWhispererCodeCoverageTracker(project, timeWindowInSec, language, rangeMarkers, codeCoverageTokens, AtomicInteger(0))
82+
codeCoverageTokens: MutableMap<Document, CodeCoverageTokens> = mutableMapOf(),
83+
invocationCount: Int = 0,
84+
) : CodeWhispererCodeCoverageTracker(project, timeWindowInSec, language, rangeMarkers, codeCoverageTokens, AtomicInteger(invocationCount))
8385

8486
protected class TestTelemetryService(
8587
publisher: TelemetryPublisher = NoOpPublisher(),
@@ -230,6 +232,24 @@ internal class CodeWhispererCodeCoverageTrackerTestPython : CodeWhispererCodeCov
230232
CodeWhispererCodeCoverageTracker.getInstancesMap()[CodeWhispererPython.INSTANCE] = sut
231233
sut.activateTrackerIfNotActive()
232234

235+
fixture.configureByText(pythonFileName, "")
236+
runInEdtAndWait {
237+
WriteCommandAction.runWriteCommandAction(project) {
238+
fixture.editor.appendString(keystrokeInput)
239+
}
240+
}
241+
val captor = argumentCaptor<DocumentEvent>()
242+
verify(sut, Times(1)).documentChanged(captor.capture())
243+
assertThat(captor.firstValue.newFragment.toString()).isEqualTo(keystrokeInput)
244+
assertThat(sut.totalTokensSize).isEqualTo(keystrokeInput.length)
245+
}
246+
247+
@Test
248+
fun `test tracker is not listening to multi char input and will not increment totalTokens - add new code`() {
249+
sut = spy(TestCodePercentageTracker(project, TOTAL_SECONDS_IN_MINUTE, CodeWhispererPython.INSTANCE))
250+
CodeWhispererCodeCoverageTracker.getInstancesMap()[CodeWhispererPython.INSTANCE] = sut
251+
sut.activateTrackerIfNotActive()
252+
233253
fixture.configureByText(pythonFileName, "")
234254
runInEdtAndWait {
235255
WriteCommandAction.runWriteCommandAction(project) {
@@ -239,16 +259,15 @@ internal class CodeWhispererCodeCoverageTrackerTestPython : CodeWhispererCodeCov
239259
val captor = argumentCaptor<DocumentEvent>()
240260
verify(sut, Times(1)).documentChanged(captor.capture())
241261
assertThat(captor.firstValue.newFragment.toString()).isEqualTo(pythonTestLeftContext)
242-
val oldSize = sut.totalTokensSize
243-
assertThat(sut.totalTokensSize).isEqualTo(pythonTestLeftContext.length)
262+
assertThat(sut.totalTokensSize).isEqualTo(0)
244263

245264
val anotherCode = "(x, y):"
246265
runInEdtAndWait {
247266
WriteCommandAction.runWriteCommandAction(project) {
248267
fixture.editor.appendString(anotherCode)
249268
}
250269
}
251-
assertThat(sut.totalTokensSize).isEqualTo(oldSize + anotherCode.length)
270+
assertThat(sut.totalTokensSize).isEqualTo(0)
252271
}
253272

254273
@Test
@@ -275,7 +294,7 @@ internal class CodeWhispererCodeCoverageTrackerTestPython : CodeWhispererCodeCov
275294
}
276295

277296
@Test
278-
fun `test tracker documentChanged - will not increment tokens on blank string of length greater than 1`() {
297+
fun `test tracker documentChanged - will increment tokens on user input blank string`() {
279298
sut = TestCodePercentageTracker(
280299
project,
281300
TOTAL_SECONDS_IN_MINUTE,
@@ -292,7 +311,7 @@ internal class CodeWhispererCodeCoverageTrackerTestPython : CodeWhispererCodeCov
292311
}
293312
}
294313

295-
assertThat(sut.totalTokensSize).isEqualTo(pythonTestLeftContext.length)
314+
assertThat(sut.totalTokensSize).isEqualTo(pythonTestLeftContext.length + 1)
296315
}
297316

298317
@Test
@@ -394,20 +413,19 @@ internal class CodeWhispererCodeCoverageTrackerTestPython : CodeWhispererCodeCov
394413
on { getUserData(any<Key<String>>()) } doReturn "foo"
395414
on { document } doReturn fixture.editor.document
396415
}
397-
398416
sut = spy(
399417
TestCodePercentageTracker(
400418
project,
401419
TOTAL_SECONDS_IN_MINUTE,
402420
CodeWhispererPython.INSTANCE,
403421
mutableListOf(rangeMarkerMock1),
404-
mutableMapOf(fixture.editor.document to CodeCoverageTokens(totalTokens = 100, acceptedTokens = 0))
422+
mutableMapOf(fixture.editor.document to CodeCoverageTokens(totalTokens = 100, acceptedTokens = 0)),
423+
1
405424
)
406425
) {
407426
onGeneric { extractRangeMarkerString(any()) } doReturn "fou"
408-
onGeneric { getAcceptedTokensDelta(any(), any()) } doReturn 99
427+
onGeneric { getAcceptedTokensDelta(any(), any()) } doReturn 1
409428
}
410-
411429
sut.emitCodeWhispererCodeContribution()
412430

413431
val metricCaptor = argumentCaptor<MetricEvent>()
@@ -416,8 +434,8 @@ internal class CodeWhispererCodeCoverageTrackerTestPython : CodeWhispererCodeCov
416434
metricCaptor.allValues,
417435
CODE_PERCENTAGE,
418436
1,
419-
CWSPR_PERCENTAGE to "99",
420-
CWSPR_ACCEPTED_TOKENS to "99",
437+
CWSPR_PERCENTAGE to "1",
438+
CWSPR_ACCEPTED_TOKENS to "1",
421439
CWSPR_TOTAL_TOKENS to "100",
422440
"codewhispererUserGroup" to userGroup.name,
423441
)
@@ -508,7 +526,8 @@ internal class CodeWhispererCodeCoverageTrackerTestJava : CodeWhispererCodeCover
508526
class Answer {
509527
private int knapsack(int[] w, int[] v, int c) {
510528
int[][] dp = new int[w.length + 1][c + 1];
511-
for (int i = 0; i < w.length; i++) {for (int j = 0; j <= c; j++) {
529+
for (int i = 0; i < w.length; i++) {
530+
for (int j = 0; j <= c; j++) {
512531
if (j < w[i]) {
513532
dp[i + 1][j] = dp[i][j];
514533
}

jetbrains-core/tst/software/aws/toolkits/jetbrains/services/codewhisperer/CodeWhispererTelemetryTest.kt

Lines changed: 21 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,6 @@ package software.aws.toolkits.jetbrains.services.codewhisperer
55

66
import com.google.gson.Gson
77
import com.intellij.openapi.application.ApplicationManager
8-
import com.intellij.openapi.application.runInEdt
98
import com.intellij.openapi.command.WriteCommandAction
109
import com.intellij.openapi.editor.Editor
1110
import com.intellij.openapi.ui.popup.JBPopup
@@ -41,6 +40,7 @@ import software.aws.toolkits.core.telemetry.MetricEvent
4140
import software.aws.toolkits.core.telemetry.TelemetryBatcher
4241
import software.aws.toolkits.core.telemetry.TelemetryPublisher
4342
import software.aws.toolkits.jetbrains.services.codewhisperer.CodeWhispererTestUtil.emptyListResponse
43+
import software.aws.toolkits.jetbrains.services.codewhisperer.CodeWhispererTestUtil.keystrokeInput
4444
import software.aws.toolkits.jetbrains.services.codewhisperer.CodeWhispererTestUtil.listOfEmptyRecommendationResponse
4545
import software.aws.toolkits.jetbrains.services.codewhisperer.CodeWhispererTestUtil.listOfMixedEmptyAndNonEmptyRecommendationResponse
4646
import software.aws.toolkits.jetbrains.services.codewhisperer.CodeWhispererTestUtil.pythonResponse
@@ -508,11 +508,11 @@ class CodeWhispererTelemetryTest : CodeWhispererTestBase() {
508508
val fixture = projectRule.fixture
509509
val emptyFile = fixture.addFileToProject("/anotherFile.py", "")
510510
// simulate users typing behavior of the following
511-
// def addTwoNumbers(
511+
// user hit one key stroke
512512
runInEdtAndWait {
513513
fixture.openFileInEditor(emptyFile.virtualFile)
514514
WriteCommandAction.runWriteCommandAction(project) {
515-
fixture.editor.appendString(pythonTestLeftContext)
515+
fixture.editor.appendString(keystrokeInput)
516516
}
517517
}
518518
// simulate users accepting the recommendation and delete part of the recommendation
@@ -533,7 +533,7 @@ class CodeWhispererTelemetryTest : CodeWhispererTestBase() {
533533
CodeWhispererCodeCoverageTracker.getInstance(project, CodeWhispererPython.INSTANCE).dispose()
534534

535535
val acceptedTokensSize = pythonResponse.completions()[0].content().length - deletedTokenByUser
536-
val totalTokensSize = pythonTestLeftContext.length + pythonResponse.completions()[0].content().length
536+
val totalTokensSize = keystrokeInput.length + pythonResponse.completions()[0].content().length
537537

538538
val metricCaptor = argumentCaptor<MetricEvent>()
539539
verify(batcher, atLeastOnce()).enqueue(metricCaptor.capture())
@@ -554,24 +554,23 @@ class CodeWhispererTelemetryTest : CodeWhispererTestBase() {
554554
val project = projectRule.project
555555
val fixture = projectRule.fixture
556556
val emptyFile = fixture.addFileToProject("/anotherFile.py", "")
557-
// simulate users typing behavior of the following
558-
// def addTwoNumbers
557+
// simulate users typing behavior
559558
runInEdtAndWait {
560559
fixture.openFileInEditor(emptyFile.virtualFile)
561560
WriteCommandAction.runWriteCommandAction(project) {
562-
fixture.editor.appendString(pythonTestLeftContext)
561+
fixture.editor.appendString(keystrokeInput)
563562
}
564563
}
565564
// simulate users accepting the recommendation
566565
// (x, y):\n return x + y
567-
val anotherCodeSnippet = "\ndef functionWritenByMyself():\n\tpass()"
566+
val anotherKeyStrokeInput = "\n "
568567
withCodeWhispererServiceInvokedAndWait {
569568
popupManagerSpy.popupComponents.acceptButton.doClick()
570569
}
571570

572571
runInEdtAndWait {
573572
WriteCommandAction.runWriteCommandAction(project) {
574-
fixture.editor.appendString(anotherCodeSnippet)
573+
fixture.editor.appendString(anotherKeyStrokeInput)
575574
val currentOffset = fixture.editor.caretModel.offset
576575
// delete 1 char
577576
fixture.editor.document.deleteString(currentOffset - 1, currentOffset)
@@ -581,7 +580,7 @@ class CodeWhispererTelemetryTest : CodeWhispererTestBase() {
581580
}
582581

583582
val acceptedTokensSize = pythonResponse.completions()[0].content().length
584-
val totalTokensSize = pythonTestLeftContext.length + pythonResponse.completions()[0].content().length + anotherCodeSnippet.length
583+
val totalTokensSize = keystrokeInput.length + pythonResponse.completions()[0].content().length + 1
585584

586585
val metricCaptor = argumentCaptor<MetricEvent>()
587586
verify(batcher, atLeastOnce()).enqueue(metricCaptor.capture())
@@ -605,17 +604,21 @@ class CodeWhispererTelemetryTest : CodeWhispererTestBase() {
605604
val userGroup = CodeWhispererUserGroupSettings.getInstance().getUserGroup()
606605
val project = projectRule.project
607606
val fixture = projectRule.fixture
608-
fixture.configureByText("/file1.py", pythonTestLeftContext)
609-
runInEdt {
610-
fixture.editor.caretModel.moveToOffset(fixture.editor.document.textLength)
607+
val emptyFile = fixture.addFileToProject("/anotherFile.py", pythonTestLeftContext)
608+
// simulate users typing behavior
609+
runInEdtAndWait {
610+
fixture.openFileInEditor(emptyFile.virtualFile)
611+
WriteCommandAction.runWriteCommandAction(project) {
612+
fixture.editor.appendString(keystrokeInput)
613+
}
611614
}
612-
val file2 = fixture.addFileToProject("./file2.py", "Pre-existing string")
613615

614616
// accept recommendation in file1.py
615617
withCodeWhispererServiceInvokedAndWait {
616618
popupManagerSpy.popupComponents.acceptButton.doClick()
617619
}
618620

621+
val file2 = fixture.addFileToProject("./file2.py", "Pre-existing string")
619622
// switch to file2.py and delete code there
620623
runInEdtAndWait {
621624
fixture.openFileInEditor(file2.virtualFile)
@@ -634,8 +637,8 @@ class CodeWhispererTelemetryTest : CodeWhispererTestBase() {
634637
codePercentage,
635638
1,
636639
"codewhispererAcceptedTokens" to pythonResponse.completions()[0].content().length.toString(),
637-
"codewhispererTotalTokens" to pythonResponse.completions()[0].content().length.toString(),
638-
"codewhispererPercentage" to "100",
640+
"codewhispererTotalTokens" to (1 + pythonResponse.completions()[0].content().length).toString(),
641+
"codewhispererPercentage" to "96",
639642
"codewhispererUserGroup" to userGroup.name,
640643
)
641644
}
@@ -663,7 +666,7 @@ class CodeWhispererTelemetryTest : CodeWhispererTestBase() {
663666
runInEdtAndWait {
664667
fixture.openFileInEditor(emptyFile.virtualFile)
665668
WriteCommandAction.runWriteCommandAction(project) {
666-
fixture.editor.appendString("$pythonTestLeftContext(")
669+
fixture.editor.appendString("(")
667670
// add closing paren but not move the caret position, simulating IDE's behavior
668671
fixture.editor.document.insertString(fixture.editor.caretModel.offset, ")")
669672
}
@@ -675,7 +678,7 @@ class CodeWhispererTelemetryTest : CodeWhispererTestBase() {
675678
CodeWhispererCodeCoverageTracker.getInstance(project, CodeWhispererPython.INSTANCE).dispose()
676679

677680
val acceptedTokensSize = "x, y):\n return x + y".length
678-
val totalTokensSize = "$pythonTestLeftContext(".length + acceptedTokensSize
681+
val totalTokensSize = "()".length + acceptedTokensSize
679682
val metricCaptor = argumentCaptor<MetricEvent>()
680683
verify(batcher, atLeastOnce()).enqueue(metricCaptor.capture())
681684
assertEventsContainsFieldsAndCount(

0 commit comments

Comments
 (0)