Skip to content

Commit 1804a24

Browse files
authored
Use reflection to determine product name and version for Telemetry (#4362)
* Use reflection to determine product name and version for Telemetry * Fetch plugin metadata for metrics/UA dynamically where possible * Add logic to dynamically inject AWS user agent per request
1 parent 87add0a commit 1804a24

File tree

20 files changed

+690
-300
lines changed

20 files changed

+690
-300
lines changed

gradle/libs.versions.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ mockito = "5.11.0"
2222
mockitoKotlin = "5.2.1"
2323
mockk = "1.13.10"
2424
node-gradle = "7.0.1"
25-
telemetryGenerator = "1.0.201"
25+
telemetryGenerator = "1.0.205"
2626
testLogger = "4.0.0"
2727
testRetry = "1.5.2"
2828
# test-only; platform provides slf4j transitively at runtime. <233, 1.7.36; >=233, 2.0.9

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ import java.net.HttpURLConnection
2222
private val logger = getLogger<FeatureDevClient>()
2323
fun uploadArtifactToS3(url: String, fileToUpload: File, checksumSha256: String, contentLength: Long, kmsArn: String?) {
2424
try {
25-
HttpRequests.put(url, APPLICATION_ZIP).userAgent(AwsClientManager.userAgent).tuner {
25+
HttpRequests.put(url, APPLICATION_ZIP).userAgent(AwsClientManager.getUserAgent()).tuner {
2626
it.setRequestProperty("Content-Type", APPLICATION_ZIP)
2727
it.setRequestProperty("Content-Length", contentLength.toString())
2828
it.setRequestProperty(CONTENT_SHA256, checksumSha256)

plugins/amazonq/codetransform/jetbrains-community/src/software/aws/toolkits/jetbrains/services/codemodernizer/client/GumbyClient.kt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -139,7 +139,7 @@ class GumbyClient(private val project: Project) {
139139
* Adapted from [CodeWhispererCodeScanSession]
140140
*/
141141
fun uploadArtifactToS3(url: String, fileToUpload: File, checksum: String, kmsArn: String, shouldStop: () -> Boolean) {
142-
HttpRequests.put(url, APPLICATION_ZIP).userAgent(AwsClientManager.userAgent).tuner {
142+
HttpRequests.put(url, APPLICATION_ZIP).userAgent(AwsClientManager.getUserAgent()).tuner {
143143
it.setRequestProperty(CONTENT_SHA256, checksum)
144144
if (kmsArn.isNotEmpty()) {
145145
it.setRequestProperty(SERVER_SIDE_ENCRYPTION, AWS_KMS)

plugins/core/jetbrains-community/src/software/aws/toolkits/jetbrains/core/AwsClientManager.kt

Lines changed: 10 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,6 @@
33

44
package software.aws.toolkits.jetbrains.core
55

6-
import com.intellij.ide.plugins.PluginManager
76
import com.intellij.openapi.Disposable
87
import com.intellij.openapi.application.ApplicationManager
98
import com.intellij.openapi.application.ApplicationNamesInfo
@@ -30,6 +29,7 @@ import software.aws.toolkits.jetbrains.core.credentials.AwsConnectionManager
3029
import software.aws.toolkits.jetbrains.core.credentials.CredentialManager
3130
import software.aws.toolkits.jetbrains.core.credentials.sso.bearer.BearerTokenProviderListener
3231
import software.aws.toolkits.jetbrains.core.region.AwsRegionProvider
32+
import software.aws.toolkits.jetbrains.services.telemetry.PluginResolver
3333
import software.aws.toolkits.jetbrains.settings.AwsSettings
3434

3535
open class AwsClientManager : ToolkitClientManager(), Disposable {
@@ -63,7 +63,7 @@ open class AwsClientManager : ToolkitClientManager(), Disposable {
6363
)
6464
}
6565

66-
override val userAgent = AwsClientManager.userAgent
66+
override fun userAgent() = getUserAgent()
6767

6868
override fun dispose() {
6969
shutdown()
@@ -87,13 +87,16 @@ open class AwsClientManager : ToolkitClientManager(), Disposable {
8787
@JvmStatic
8888
fun getInstance(): ToolkitClientManager = service()
8989

90-
val userAgent: String by lazy {
91-
val platformName = tryOrNull { ApplicationNamesInfo.getInstance().fullProductNameWithEdition.replace(' ', '-') }
92-
val platformVersion = tryOrNull { ApplicationInfoEx.getInstanceEx().fullVersion.replace(' ', '-') }
93-
val pluginVersion = tryOrNull { PluginManager.getPluginByClass(this::class.java)?.version }
94-
"AWS-Toolkit-For-JetBrains/$pluginVersion $platformName/$platformVersion ClientId/${AwsSettings.getInstance().clientId}"
90+
fun getUserAgent(): String {
91+
val pluginResolver = PluginResolver.fromCurrentThread()
92+
val pluginName = pluginResolver.product.toString().replace(" ", "-")
93+
val pluginVersion = pluginResolver.version
94+
return "$pluginName/$pluginVersion $platformName/$platformVersion ClientId/${AwsSettings.getInstance().clientId}"
9595
}
9696

97+
private val platformName = tryOrNull { ApplicationNamesInfo.getInstance().fullProductNameWithEdition.replace(' ', '-') }
98+
private val platformVersion = tryOrNull { ApplicationInfoEx.getInstanceEx().fullVersion.replace(' ', '-') }
99+
97100
val CUSTOMIZER_EP = ExtensionPointName<ToolkitClientCustomizer>("aws.toolkit.sdk.clientCustomizer")
98101
}
99102
}

plugins/core/jetbrains-community/src/software/aws/toolkits/jetbrains/core/HttpUtils.kt

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -9,17 +9,17 @@ import org.apache.http.entity.ContentType
99
import java.nio.file.Path
1010

1111
fun saveFileFromUrl(url: String, path: Path, indicator: ProgressIndicator? = null) =
12-
HttpRequests.request(url).userAgent(AwsClientManager.userAgent).saveToFile(path.toFile(), indicator)
12+
HttpRequests.request(url).userAgent(AwsClientManager.getUserAgent()).saveToFile(path.toFile(), indicator)
1313

1414
fun readBytesFromUrl(url: String, indicator: ProgressIndicator? = null) =
15-
HttpRequests.request(url).userAgent(AwsClientManager.userAgent).readBytes(indicator)
15+
HttpRequests.request(url).userAgent(AwsClientManager.getUserAgent()).readBytes(indicator)
1616

1717
fun getTextFromUrl(url: String): String =
18-
HttpRequests.request(url).userAgent(AwsClientManager.userAgent).readString()
18+
HttpRequests.request(url).userAgent(AwsClientManager.getUserAgent()).readString()
1919

2020
fun writeJsonToUrl(url: String, jsonString: String, indicator: ProgressIndicator? = null): String =
2121
HttpRequests.post(url, ContentType.APPLICATION_JSON.toString())
22-
.userAgent(AwsClientManager.userAgent)
22+
.userAgent(AwsClientManager.getUserAgent())
2323
.connect { request ->
2424
request.write(jsonString)
2525
request.readString(indicator)

plugins/core/jetbrains-community/src/software/aws/toolkits/jetbrains/services/telemetry/ClientMetadata.kt

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -3,23 +3,28 @@
33

44
package software.aws.toolkits.jetbrains.services.telemetry
55

6-
import com.intellij.ide.plugins.PluginManager
76
import com.intellij.openapi.application.ApplicationInfo
87
import com.intellij.openapi.application.ApplicationNamesInfo
98
import com.intellij.openapi.util.SystemInfo
109
import software.amazon.awssdk.services.toolkittelemetry.model.AWSProduct
1110
import software.aws.toolkits.jetbrains.settings.AwsSettings
1211

1312
data class ClientMetadata(
14-
val productName: AWSProduct = AWSProduct.AWS_TOOLKIT_FOR_JET_BRAINS,
15-
val productVersion: String = PluginManager.getPluginByClass(this::class.java)?.version.toString(),
13+
val awsProduct: AWSProduct,
14+
val awsVersion: String,
1615
val clientId: String = AwsSettings.getInstance().clientId.toString(),
1716
val parentProduct: String = ApplicationNamesInfo.getInstance().fullProductNameWithEdition,
1817
val parentProductVersion: String = ApplicationInfo.getInstance().build.baselineVersion.toString(),
1918
val os: String = SystemInfo.OS_NAME,
2019
val osVersion: String = SystemInfo.OS_VERSION,
2120
) {
2221
companion object {
23-
val DEFAULT_METADATA = ClientMetadata()
22+
fun getDefault(): ClientMetadata {
23+
val pluginResolver = PluginResolver.fromCurrentThread()
24+
return ClientMetadata(
25+
awsProduct = pluginResolver.product,
26+
awsVersion = pluginResolver.version
27+
)
28+
}
2429
}
2530
}

plugins/core/jetbrains-community/src/software/aws/toolkits/jetbrains/services/telemetry/DefaultTelemetryPublisher.kt

Lines changed: 25 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import software.amazon.awssdk.auth.credentials.AnonymousCredentialsProvider
99
import software.amazon.awssdk.regions.Region
1010
import software.amazon.awssdk.services.cognitoidentity.CognitoIdentityClient
1111
import software.amazon.awssdk.services.toolkittelemetry.ToolkitTelemetryClient
12+
import software.amazon.awssdk.services.toolkittelemetry.model.AWSProduct
1213
import software.amazon.awssdk.services.toolkittelemetry.model.MetadataEntry
1314
import software.amazon.awssdk.services.toolkittelemetry.model.MetricDatum
1415
import software.amazon.awssdk.services.toolkittelemetry.model.Sentiment
@@ -20,34 +21,43 @@ import software.aws.toolkits.jetbrains.core.AwsSdkClient
2021
import software.aws.toolkits.jetbrains.core.coroutines.getCoroutineBgContext
2122

2223
class DefaultTelemetryPublisher(
23-
private val clientMetadata: ClientMetadata = ClientMetadata.DEFAULT_METADATA,
24-
private val clientProvider: () -> ToolkitTelemetryClient
24+
private val clientProvider: () -> ToolkitTelemetryClient,
25+
private val clientMetadataProvider: (AWSProduct, String) -> ClientMetadata
2526
) : TelemetryPublisher {
26-
constructor() : this(clientProvider = { createDefaultTelemetryClient() })
27+
constructor() : this(
28+
clientProvider = { createDefaultTelemetryClient() },
29+
clientMetadataProvider = { product, version -> ClientMetadata(product, version) }
30+
)
2731

2832
private val lazyClient = lazy { clientProvider() }
2933
private val client by lazyClient
3034

3135
override suspend fun publish(metricEvents: Collection<MetricEvent>) {
3236
withContext(getCoroutineBgContext()) {
33-
client.postMetrics {
34-
it.awsProduct(clientMetadata.productName)
35-
it.awsProductVersion(clientMetadata.productVersion)
36-
it.clientID(clientMetadata.clientId)
37-
it.os(clientMetadata.os)
38-
it.osVersion(clientMetadata.osVersion)
39-
it.parentProduct(clientMetadata.parentProduct)
40-
it.parentProductVersion(clientMetadata.parentProductVersion)
41-
it.metricData(metricEvents.toMetricData())
42-
}
37+
metricEvents.groupBy { Pair(it.awsProduct, it.awsVersion) }
38+
.forEach { (productName, productVersion), events ->
39+
val clientMetadata = clientMetadataProvider(productName, productVersion)
40+
client.postMetrics {
41+
it.awsProduct(clientMetadata.awsProduct)
42+
it.awsProductVersion(clientMetadata.awsVersion)
43+
it.clientID(clientMetadata.clientId)
44+
it.os(clientMetadata.os)
45+
it.osVersion(clientMetadata.osVersion)
46+
it.parentProduct(clientMetadata.parentProduct)
47+
it.parentProductVersion(clientMetadata.parentProductVersion)
48+
it.metricData(events.toMetricData())
49+
}
50+
}
4351
}
4452
}
4553

4654
override suspend fun sendFeedback(sentiment: Sentiment, comment: String, metadata: Map<String, String>) {
4755
withContext(getCoroutineBgContext()) {
56+
val pluginResolver = PluginResolver.fromCurrentThread()
57+
val clientMetadata = clientMetadataProvider(pluginResolver.product, pluginResolver.version)
4858
client.postFeedback {
49-
it.awsProduct(clientMetadata.productName)
50-
it.awsProductVersion(clientMetadata.productVersion)
59+
it.awsProduct(clientMetadata.awsProduct)
60+
it.awsProductVersion(clientMetadata.awsVersion)
5161
it.os(clientMetadata.os)
5262
it.osVersion(clientMetadata.osVersion)
5363
it.parentProduct(clientMetadata.parentProduct)
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
// Copyright 2024 Amazon.com, Inc. or its affiliates. All Rights Reserved.
2+
// SPDX-License-Identifier: Apache-2.0
3+
4+
package software.aws.toolkits.jetbrains.services.telemetry
5+
6+
import com.intellij.ide.plugins.PluginManagerCore
7+
import software.amazon.awssdk.services.toolkittelemetry.model.AWSProduct
8+
9+
class PluginResolver private constructor(callerStackTrace: Array<StackTraceElement>) {
10+
private val pluginDescriptor by lazy {
11+
callerStackTrace
12+
.reversed()
13+
.filter { it.className.startsWith("software.aws.toolkits") }
14+
.firstNotNullOfOrNull { PluginManagerCore.getPluginDescriptorOrPlatformByClassName(it.className) }
15+
}
16+
17+
val product: AWSProduct
18+
get() = when (pluginDescriptor?.name) {
19+
"amazon.q" -> AWSProduct.AMAZON_Q_FOR_JET_BRAINS
20+
else -> AWSProduct.AWS_TOOLKIT_FOR_JET_BRAINS
21+
}
22+
23+
val version: String
24+
get() = pluginDescriptor?.version ?: "unknown"
25+
26+
companion object {
27+
fun fromCurrentThread() = PluginResolver(Thread.currentThread().stackTrace)
28+
29+
fun fromStackTrace(stackTrace: Array<StackTraceElement>) = PluginResolver(stackTrace)
30+
}
31+
}

plugins/core/jetbrains-community/src/software/aws/toolkits/jetbrains/services/telemetry/TelemetryService.kt

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ package software.aws.toolkits.jetbrains.services.telemetry
66
import com.intellij.openapi.Disposable
77
import com.intellij.openapi.components.service
88
import com.intellij.openapi.project.Project
9+
import software.amazon.awssdk.services.toolkittelemetry.model.AWSProduct
910
import software.amazon.awssdk.services.toolkittelemetry.model.Sentiment
1011
import software.aws.toolkits.core.ConnectionSettings
1112
import software.aws.toolkits.core.telemetry.DefaultMetricEvent
@@ -26,7 +27,9 @@ import java.util.concurrent.atomic.AtomicBoolean
2627

2728
data class MetricEventMetadata(
2829
val awsAccount: String = METADATA_NA,
29-
val awsRegion: String = METADATA_NA
30+
val awsRegion: String = METADATA_NA,
31+
var awsProduct: AWSProduct = AWSProduct.AWS_TOOLKIT_FOR_JET_BRAINS,
32+
var awsVersion: String = METADATA_NA
3033
)
3134

3235
interface TelemetryListener {
@@ -49,6 +52,9 @@ abstract class TelemetryService(private val publisher: TelemetryPublisher, priva
4952
)
5053
else -> MetricEventMetadata()
5154
}
55+
val pluginResolver = PluginResolver.fromCurrentThread()
56+
metricEventMetadata.awsProduct = pluginResolver.product
57+
metricEventMetadata.awsVersion = pluginResolver.version
5258
record(metricEventMetadata, buildEvent)
5359
}
5460

@@ -69,6 +75,9 @@ abstract class TelemetryService(private val publisher: TelemetryPublisher, priva
6975
} else {
7076
MetricEventMetadata()
7177
}
78+
val pluginResolver = PluginResolver.fromCurrentThread()
79+
metricEventMetadata.awsProduct = pluginResolver.product
80+
metricEventMetadata.awsVersion = pluginResolver.version
7281
record(metricEventMetadata, buildEvent)
7382
}
7483

@@ -102,6 +111,8 @@ abstract class TelemetryService(private val publisher: TelemetryPublisher, priva
102111
val builder = DefaultMetricEvent.builder()
103112
builder.awsAccount(metricEventMetadata.awsAccount)
104113
builder.awsRegion(metricEventMetadata.awsRegion)
114+
builder.awsProduct(metricEventMetadata.awsProduct)
115+
builder.awsVersion(metricEventMetadata.awsVersion)
105116

106117
buildEvent(builder)
107118

0 commit comments

Comments
 (0)