Skip to content

Commit 2a8fd52

Browse files
authored
telemetry(amazonq): Add diff telemetry for generated and accepted changes. (#5116)
The purpose of this change is to introduce telemetry for generated and accepted code within Feature Dev (`/dev`). This change performs a diff when code is generated or accepted, and records generated code. This change handles duplicate generation/acceptance within a given conversation by tracking an identifier of generated changes for a given file based on a hash of the before/after content.
1 parent dfe6af0 commit 2a8fd52

File tree

14 files changed

+206
-5
lines changed

14 files changed

+206
-5
lines changed

plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonqFeatureDev/clients/FeatureDevClient.kt

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -89,6 +89,42 @@ class FeatureDevClient(
8989
requestBuilder.userContext(featureDevUserContext)
9090
}
9191

92+
fun sendFeatureDevCodeGenerationEvent(
93+
conversationId: String,
94+
linesOfCodeGenerated: Int,
95+
charactersOfCodeGenerated: Int,
96+
): SendTelemetryEventResponse =
97+
bearerClient().sendTelemetryEvent { requestBuilder ->
98+
requestBuilder.telemetryEvent { telemetryEventBuilder ->
99+
telemetryEventBuilder.featureDevCodeGenerationEvent {
100+
it
101+
.conversationId(conversationId)
102+
.linesOfCodeGenerated(linesOfCodeGenerated)
103+
.charactersOfCodeGenerated(charactersOfCodeGenerated)
104+
}
105+
}
106+
requestBuilder.optOutPreference(getTelemetryOptOutPreference())
107+
requestBuilder.userContext(featureDevUserContext)
108+
}
109+
110+
fun sendFeatureDevCodeAcceptanceEvent(
111+
conversationId: String,
112+
linesOfCodeAccepted: Int,
113+
charactersOfCodeAccepted: Int,
114+
): SendTelemetryEventResponse =
115+
bearerClient().sendTelemetryEvent { requestBuilder ->
116+
requestBuilder.telemetryEvent { telemetryEventBuilder ->
117+
telemetryEventBuilder.featureDevCodeAcceptanceEvent {
118+
it
119+
.conversationId(conversationId)
120+
.linesOfCodeAccepted(linesOfCodeAccepted)
121+
.charactersOfCodeAccepted(charactersOfCodeAccepted)
122+
}
123+
}
124+
requestBuilder.optOutPreference(getTelemetryOptOutPreference())
125+
requestBuilder.userContext(featureDevUserContext)
126+
}
127+
92128
fun createTaskAssistConversation(): CreateTaskAssistConversationResponse =
93129
bearerClient().createTaskAssistConversation(
94130
CreateTaskAssistConversationRequest.builder().build(),

plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonqFeatureDev/session/CodeGenerationState.kt

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,9 @@ import software.aws.toolkits.jetbrains.services.amazonqFeatureDev.ThrottlingExce
2020
import software.aws.toolkits.jetbrains.services.amazonqFeatureDev.messages.sendAnswerPart
2121
import software.aws.toolkits.jetbrains.services.amazonqFeatureDev.messages.sendUpdatePlaceholder
2222
import software.aws.toolkits.jetbrains.services.amazonqFeatureDev.util.CancellationTokenSource
23+
import software.aws.toolkits.jetbrains.services.amazonqFeatureDev.util.getChangeIdentifier
24+
import software.aws.toolkits.jetbrains.services.amazonqFeatureDev.util.getDiffMetrics
25+
import software.aws.toolkits.jetbrains.services.amazonqFeatureDev.util.readFileToString
2326
import software.aws.toolkits.jetbrains.services.cwc.controller.chat.telemetry.getStartUrl
2427
import software.aws.toolkits.resources.message
2528
import software.aws.toolkits.telemetry.AmazonqTelemetry
@@ -40,6 +43,7 @@ class CodeGenerationState(
4043
override var codeGenerationTotalIterationCount: Int? = null,
4144
var currentCodeGenerationId: String? = "EMPTY_CURRENT_CODE_GENERATION_ID",
4245
override var token: CancellationTokenSource?,
46+
override var diffMetricsProcessed: DiffMetricsProcessed,
4347
) : SessionState {
4448
override val phase = SessionStatePhase.CODEGEN
4549

@@ -81,6 +85,41 @@ class CodeGenerationState(
8185
codeGenerationRemainingIterationCount = codeGenerationResult.codeGenerationRemainingIterationCount
8286
codeGenerationTotalIterationCount = codeGenerationResult.codeGenerationTotalIterationCount
8387

88+
runCatching {
89+
var insertedLines = 0
90+
var insertedCharacters = 0
91+
codeGenerationResult.newFiles.forEach { file ->
92+
// FIXME: Ideally, the before content should be read from the uploaded context instead of from disk, to avoid drift
93+
val before = config.repoContext.selectedSourceFolder
94+
.toNioPath()
95+
.resolve(file.zipFilePath)
96+
.toFile()
97+
.let { f ->
98+
if (f.exists() && f.canRead()) {
99+
readFileToString(f)
100+
} else {
101+
""
102+
}
103+
}
104+
105+
val changeIdentifier = getChangeIdentifier(file.zipFilePath, before, file.fileContent)
106+
107+
if (!diffMetricsProcessed.generated.contains(changeIdentifier)) {
108+
val diffMetrics = getDiffMetrics(before, file.fileContent)
109+
insertedLines += diffMetrics.insertedLines
110+
insertedCharacters += diffMetrics.insertedCharacters
111+
diffMetricsProcessed.generated.add(changeIdentifier)
112+
}
113+
}
114+
if (insertedLines > 0) {
115+
config.featureDevService.sendFeatureDevCodeGenerationEvent(
116+
conversationId = config.conversationId,
117+
linesOfCodeGenerated = insertedLines,
118+
charactersOfCodeGenerated = insertedCharacters,
119+
)
120+
}
121+
}.onFailure { /* Noop on diff telemetry failure */ }
122+
84123
val nextState =
85124
PrepareCodeGenerationState(
86125
tabID = tabID,
@@ -95,6 +134,7 @@ class CodeGenerationState(
95134
codeGenerationRemainingIterationCount = codeGenerationRemainingIterationCount,
96135
codeGenerationTotalIterationCount = codeGenerationTotalIterationCount,
97136
token = this.token,
137+
diffMetricsProcessed = diffMetricsProcessed,
98138
)
99139

100140
// It is not needed to interact right away with the PrepareCodeGeneration.

plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonqFeatureDev/session/ConversationNotStartedState.kt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ class ConversationNotStartedState(
1212
override var codeGenerationRemainingIterationCount: Int?,
1313
override var codeGenerationTotalIterationCount: Int?,
1414
override var currentIteration: Int?,
15+
override var diffMetricsProcessed: DiffMetricsProcessed,
1516
) : SessionState {
1617
override val phase = SessionStatePhase.INIT
1718

plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonqFeatureDev/session/PrepareCodeGenerationState.kt

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@ class PrepareCodeGenerationState(
3434
private var messenger: MessagePublisher,
3535
override var codeGenerationRemainingIterationCount: Int? = null,
3636
override var codeGenerationTotalIterationCount: Int? = null,
37+
override var diffMetricsProcessed: DiffMetricsProcessed,
3738
) : SessionState {
3839
override val phase = SessionStatePhase.CODEGEN
3940
override suspend fun interact(action: SessionStateAction): SessionStateInteraction {
@@ -74,7 +75,8 @@ class PrepareCodeGenerationState(
7475
currentIteration = this.currentIteration,
7576
repositorySize = zipFileLength.toDouble(),
7677
messenger = messenger,
77-
token = this.token
78+
token = this.token,
79+
diffMetricsProcessed = diffMetricsProcessed
7880
)
7981
} catch (e: Exception) {
8082
result = Result.Failed

plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonqFeatureDev/session/Session.kt

Lines changed: 50 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,9 +18,13 @@ import software.aws.toolkits.jetbrains.services.amazonqFeatureDev.messages.sendA
1818
import software.aws.toolkits.jetbrains.services.amazonqFeatureDev.messages.updateFileComponent
1919
import software.aws.toolkits.jetbrains.services.amazonqFeatureDev.util.CancellationTokenSource
2020
import software.aws.toolkits.jetbrains.services.amazonqFeatureDev.util.FeatureDevService
21+
import software.aws.toolkits.jetbrains.services.amazonqFeatureDev.util.getChangeIdentifier
22+
import software.aws.toolkits.jetbrains.services.amazonqFeatureDev.util.getDiffMetrics
23+
import software.aws.toolkits.jetbrains.services.amazonqFeatureDev.util.readFileToString
2124
import software.aws.toolkits.jetbrains.services.amazonqFeatureDev.util.resolveAndCreateOrUpdateFile
2225
import software.aws.toolkits.jetbrains.services.amazonqFeatureDev.util.resolveAndDeleteFile
2326
import software.aws.toolkits.jetbrains.services.cwc.controller.ReferenceLogController
27+
import java.util.HashSet
2428

2529
class Session(val tabID: String, val project: Project) {
2630
var context: FeatureDevSessionContext
@@ -45,7 +49,15 @@ class Session(val tabID: String, val project: Project) {
4549
context = FeatureDevSessionContext(project, MAX_PROJECT_SIZE_BYTES)
4650
proxyClient = FeatureDevClient.getInstance(project)
4751
featureDevService = FeatureDevService(proxyClient, project)
48-
_state = ConversationNotStartedState("", tabID, null, 0, CODE_GENERATION_RETRY_LIMIT, 0)
52+
_state = ConversationNotStartedState(
53+
approach = "",
54+
tabID = tabID,
55+
token = null,
56+
codeGenerationRemainingIterationCount = 0,
57+
codeGenerationTotalIterationCount = CODE_GENERATION_RETRY_LIMIT,
58+
currentIteration = 0,
59+
diffMetricsProcessed = DiffMetricsProcessed(HashSet(), HashSet())
60+
)
4961
isAuthenticating = false
5062
codegenRetries = CODE_GENERATION_RETRY_LIMIT
5163
}
@@ -86,6 +98,7 @@ class Session(val tabID: String, val project: Project) {
8698
uploadId = "", // There is no code gen uploadId so far
8799
messenger = messenger,
88100
token = CancellationTokenSource(),
101+
diffMetricsProcessed = sessionState.diffMetricsProcessed,
89102
)
90103
}
91104

@@ -110,6 +123,42 @@ class Session(val tabID: String, val project: Project) {
110123
val selectedSourceFolder = context.selectedSourceFolder.toNioPath()
111124
val newFilePaths = filePaths.filter { !it.rejected && !it.changeApplied }
112125
val newDeletedFiles = deletedFiles.filter { !it.rejected && !it.changeApplied }
126+
127+
runCatching {
128+
var insertedLines = 0
129+
var insertedCharacters = 0
130+
filePaths.forEach { file ->
131+
// FIXME: Ideally, the before content should be read from the uploaded context instead of from disk, to avoid drift
132+
val before = selectedSourceFolder
133+
.resolve(file.zipFilePath)
134+
.toFile()
135+
.let { f ->
136+
if (f.exists() && f.canRead()) {
137+
readFileToString(f)
138+
} else {
139+
""
140+
}
141+
}
142+
143+
val changeIdentifier = getChangeIdentifier(file.zipFilePath, before, file.fileContent)
144+
145+
if (_state?.diffMetricsProcessed?.accepted?.contains(changeIdentifier) != true) {
146+
val diffMetrics = getDiffMetrics(before, file.fileContent)
147+
insertedLines += diffMetrics.insertedLines
148+
insertedCharacters += diffMetrics.insertedCharacters
149+
_state?.diffMetricsProcessed?.accepted?.add(changeIdentifier)
150+
}
151+
}
152+
153+
if (insertedLines > 0) {
154+
featureDevService.sendFeatureDevCodeAcceptanceEvent(
155+
conversationId = conversationId,
156+
linesOfCodeAccepted = insertedLines,
157+
charactersOfCodeAccepted = insertedCharacters,
158+
)
159+
}
160+
}.onFailure { /* Noop on diff telemetry failure */ }
161+
113162
newFilePaths.forEach {
114163
resolveAndCreateOrUpdateFile(selectedSourceFolder, it.zipFilePath, it.fileContent)
115164
it.changeApplied = true

plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonqFeatureDev/session/SessionState.kt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,5 +13,6 @@ interface SessionState {
1313
var codeGenerationTotalIterationCount: Int?
1414
var currentIteration: Int?
1515
var approach: String
16+
var diffMetricsProcessed: DiffMetricsProcessed
1617
suspend fun interact(action: SessionStateAction): SessionStateInteraction
1718
}

plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonqFeatureDev/session/SessionStateTypes.kt

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -77,3 +77,8 @@ data class CodeGenerationStreamResult(
7777
data class ExportTaskAssistResultArchiveStreamResult(
7878
var code_generation_result: CodeGenerationStreamResult,
7979
)
80+
81+
data class DiffMetricsProcessed(
82+
var accepted: HashSet<String>,
83+
var generated: HashSet<String>,
84+
)

plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonqFeatureDev/util/DiffMetrics.kt

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,9 @@ import com.intellij.diff.comparison.ComparisonManager
77
import com.intellij.diff.comparison.ComparisonPolicy
88
import com.intellij.diff.fragments.LineFragment
99
import com.intellij.openapi.progress.EmptyProgressIndicator
10+
import io.ktor.utils.io.core.toByteArray
11+
import java.nio.charset.Charset
12+
import java.security.MessageDigest
1013

1114
data class DiffMetrics(
1215
val insertedLines: Int,
@@ -37,7 +40,7 @@ fun getDiffMetrics(before: String, after: String): DiffMetrics {
3740
val fragments = comparisonManager.compareLines(
3841
before,
3942
after,
40-
ComparisonPolicy.DEFAULT,
43+
ComparisonPolicy.IGNORE_WHITESPACES,
4144
EmptyProgressIndicator()
4245
)
4346

@@ -71,3 +74,11 @@ fun getDiffMetrics(before: String, after: String): DiffMetrics {
7174
insertedCharacters = accCharCount,
7275
)
7376
}
77+
78+
fun getChangeIdentifier(filePath: String, before: String, after: String): String {
79+
val hash = MessageDigest.getInstance("SHA-1")
80+
hash.update(filePath.toByteArray(Charset.forName("UTF-8")))
81+
hash.update(before.toByteArray(Charset.forName("UTF-8")))
82+
hash.update(after.toByteArray(Charset.forName("UTF-8")))
83+
return hash.digest().joinToString("") { "%02x".format(it) }
84+
}

plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonqFeatureDev/util/FeatureDevService.kt

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -215,4 +215,32 @@ class FeatureDevService(val proxyClient: FeatureDevClient, val project: Project)
215215
logger.warn(e) { "$FEATURE_NAME: failed to send feature dev telemetry" }
216216
}
217217
}
218+
219+
fun sendFeatureDevCodeGenerationEvent(conversationId: String, linesOfCodeGenerated: Int, charactersOfCodeGenerated: Int) {
220+
val sendFeatureDevTelemetryEventResponse: SendTelemetryEventResponse
221+
try {
222+
sendFeatureDevTelemetryEventResponse = proxyClient
223+
.sendFeatureDevCodeGenerationEvent(conversationId, linesOfCodeGenerated, charactersOfCodeGenerated)
224+
val requestId = sendFeatureDevTelemetryEventResponse.responseMetadata().requestId()
225+
logger.debug {
226+
"$FEATURE_NAME: successfully sent feature dev code generation telemetry: ConversationId: $conversationId RequestId: $requestId"
227+
}
228+
} catch (e: Exception) {
229+
logger.warn(e) { "$FEATURE_NAME: failed to send feature dev code generation telemetry" }
230+
}
231+
}
232+
233+
fun sendFeatureDevCodeAcceptanceEvent(conversationId: String, linesOfCodeAccepted: Int, charactersOfCodeAccepted: Int) {
234+
val sendFeatureDevTelemetryEventResponse: SendTelemetryEventResponse
235+
try {
236+
sendFeatureDevTelemetryEventResponse = proxyClient
237+
.sendFeatureDevCodeAcceptanceEvent(conversationId, linesOfCodeAccepted, charactersOfCodeAccepted)
238+
val requestId = sendFeatureDevTelemetryEventResponse.responseMetadata().requestId()
239+
logger.debug {
240+
"$FEATURE_NAME: successfully sent feature dev code acceptance telemetry: ConversationId: $conversationId RequestId: $requestId"
241+
}
242+
} catch (e: Exception) {
243+
logger.warn(e) { "$FEATURE_NAME: failed to send feature dev code acceptance telemetry" }
244+
}
245+
}
218246
}

plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonqFeatureDev/util/FileUtils.kt

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,10 @@ package software.aws.toolkits.jetbrains.services.amazonqFeatureDev.util
66
import com.intellij.openapi.fileChooser.FileChooser
77
import com.intellij.openapi.fileChooser.FileChooserDescriptorFactory
88
import com.intellij.openapi.project.Project
9+
import com.intellij.openapi.vfs.CharsetToolkit
910
import com.intellij.openapi.vfs.VirtualFile
11+
import java.io.File
12+
import java.nio.charset.Charset
1013
import java.nio.file.Path
1114
import kotlin.io.path.createDirectories
1215
import kotlin.io.path.deleteIfExists
@@ -33,3 +36,9 @@ fun selectFolder(project: Project, openOn: VirtualFile): VirtualFile? {
3336
val fileChooserDescriptor = FileChooserDescriptorFactory.createSingleFolderDescriptor()
3437
return FileChooser.chooseFile(fileChooserDescriptor, project, openOn)
3538
}
39+
40+
fun readFileToString(file: File): String {
41+
val charsetToolkit = CharsetToolkit(file.readBytes(), Charset.forName("UTF-8"), false)
42+
val charset = charsetToolkit.guessEncoding(4096)
43+
return file.readText(charset)
44+
}

0 commit comments

Comments
 (0)