From d06ae115c93652585811827fe4e92340273d2304 Mon Sep 17 00:00:00 2001 From: Florian Dreier Date: Thu, 29 Jan 2026 09:53:29 +0100 Subject: [PATCH] TS-45214 Send user-agent including the version --- CHANGELOG.md | 1 + .../ConfigurationViaTeamscale.java | 4 +++- .../jacoco/agent/options/AgentOptions.java | 3 ++- .../upload/teamscale/TeamscaleUploader.java | 4 +++- .../jacoco/agent/util/AgentUtils.java | 5 +++++ .../teamscale/test_impacted/BuildVersion.kt | 12 ++++++++++ .../engine/options/TestEngineOptions.kt | 5 ++++- .../teamscale/test_impacted/app.properties | 1 + .../client/HttpRedirectSystemTest.kt | 2 +- .../com/teamscale/client/TeamscaleClient.kt | 8 ++++--- .../client/TeamscaleServiceGenerator.kt | 22 ++++++++++++------- ...eamscaleServiceGeneratorProxyServerTest.kt | 6 +++-- .../teamscale/config/ServerConfiguration.kt | 7 +++++- .../com/teamscale/maven/BuildVersion.java | 12 ++++++++++ .../maven/upload/CoverageUploadMojo.java | 14 +++++++----- .../com/teamscale/maven/app.properties | 1 + 16 files changed, 83 insertions(+), 24 deletions(-) create mode 100644 impacted-test-engine/src/main/kotlin/com/teamscale/test_impacted/BuildVersion.kt create mode 100644 impacted-test-engine/src/main/resources/com/teamscale/test_impacted/app.properties create mode 100644 teamscale-maven-plugin/src/main/java/com/teamscale/maven/BuildVersion.java create mode 100644 teamscale-maven-plugin/src/main/resources/com/teamscale/maven/app.properties diff --git a/CHANGELOG.md b/CHANGELOG.md index 8029d784d..69eb0dff5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,7 @@ We use [semantic versioning](http://semver.org/): - PATCH version when you make backwards compatible bug fixes. # Next version +- _teamscale-client_: User-Agent header now includes the specific component performing the request (e.g., "Teamscale Gradle Plugin", "Teamscale Maven Plugin") and version number - _maven-plugin_: Added maven properties for `runImpacted` and `runAllTests` configuration parameters - _maven-plugin_: The warning "Both baselineRevision and baselineCommit are set but only one of them is needed" was displayed incorrectly - [fix] _maven-plugin_: Coverage upload used "Now" instead of auto-resolved Git revision when no commit/revision was configured diff --git a/agent/src/main/java/com/teamscale/jacoco/agent/configuration/ConfigurationViaTeamscale.java b/agent/src/main/java/com/teamscale/jacoco/agent/configuration/ConfigurationViaTeamscale.java index 059489e2f..d4e59a7e6 100644 --- a/agent/src/main/java/com/teamscale/jacoco/agent/configuration/ConfigurationViaTeamscale.java +++ b/agent/src/main/java/com/teamscale/jacoco/agent/configuration/ConfigurationViaTeamscale.java @@ -9,6 +9,7 @@ import com.teamscale.client.ProfilerRegistration; import com.teamscale.client.TeamscaleServiceGenerator; import com.teamscale.jacoco.agent.logging.LoggingUtils; +import com.teamscale.jacoco.agent.util.AgentUtils; import com.teamscale.report.util.ILogger; import okhttp3.HttpUrl; import okhttp3.ResponseBody; @@ -57,7 +58,8 @@ public ConfigurationViaTeamscale(ITeamscaleService teamscaleClient, ProfilerRegi String userName, String userAccessToken) throws AgentOptionReceiveException { ITeamscaleService teamscaleClient = TeamscaleServiceGenerator - .createService(ITeamscaleService.class, url, userName, userAccessToken, LONG_TIMEOUT, LONG_TIMEOUT); + .createService(ITeamscaleService.class, url, userName, userAccessToken, AgentUtils.USER_AGENT, + LONG_TIMEOUT, LONG_TIMEOUT); try { ProcessInformation processInformation = new ProcessInformationRetriever(logger).getProcessInformation(); Response response = teamscaleClient.registerProfiler(configurationId, diff --git a/agent/src/main/java/com/teamscale/jacoco/agent/options/AgentOptions.java b/agent/src/main/java/com/teamscale/jacoco/agent/options/AgentOptions.java index b30cf2cbd..4a19f3e3f 100644 --- a/agent/src/main/java/com/teamscale/jacoco/agent/options/AgentOptions.java +++ b/agent/src/main/java/com/teamscale/jacoco/agent/options/AgentOptions.java @@ -402,7 +402,8 @@ private void validateTestwiseCoverageConfig(Validator validator) { if (teamscaleServer.isConfiguredForSingleProjectTeamscaleUpload() || !requireSingleProjectUploadConfig && teamscaleServer.isConfiguredForServerConnection()) { return new TeamscaleClient(teamscaleServer.url.toString(), teamscaleServer.userName, - teamscaleServer.userAccessToken, teamscaleServer.project); + teamscaleServer.userAccessToken, teamscaleServer.project, + AgentUtils.USER_AGENT); } return null; } diff --git a/agent/src/main/java/com/teamscale/jacoco/agent/upload/teamscale/TeamscaleUploader.java b/agent/src/main/java/com/teamscale/jacoco/agent/upload/teamscale/TeamscaleUploader.java index d8867507c..39517e38e 100644 --- a/agent/src/main/java/com/teamscale/jacoco/agent/upload/teamscale/TeamscaleUploader.java +++ b/agent/src/main/java/com/teamscale/jacoco/agent/upload/teamscale/TeamscaleUploader.java @@ -11,6 +11,7 @@ import com.teamscale.jacoco.agent.logging.LoggingUtils; import com.teamscale.jacoco.agent.upload.IUploadRetry; import com.teamscale.jacoco.agent.upload.IUploader; +import com.teamscale.jacoco.agent.util.AgentUtils; import com.teamscale.jacoco.agent.util.Benchmark; import com.teamscale.report.jacoco.CoverageFile; import org.slf4j.Logger; @@ -135,7 +136,8 @@ private boolean tryUploading(CoverageFile coverageFile, TeamscaleServer teamscal // Cannot be executed in the constructor as this causes issues in WildFly server // (See #100) ITeamscaleService api = TeamscaleServiceGenerator.createService(ITeamscaleService.class, - teamscaleServer.url, teamscaleServer.userName, teamscaleServer.userAccessToken); + teamscaleServer.url, teamscaleServer.userName, teamscaleServer.userAccessToken, + AgentUtils.USER_AGENT); ITeamscaleServiceKt.uploadReport(api, teamscaleServer.project, teamscaleServer.commit, teamscaleServer.revision, teamscaleServer.repository, teamscaleServer.partition, EReportFormat.JACOCO, diff --git a/agent/src/main/java/com/teamscale/jacoco/agent/util/AgentUtils.java b/agent/src/main/java/com/teamscale/jacoco/agent/util/AgentUtils.java index 88c3251f8..e055ee8c1 100644 --- a/agent/src/main/java/com/teamscale/jacoco/agent/util/AgentUtils.java +++ b/agent/src/main/java/com/teamscale/jacoco/agent/util/AgentUtils.java @@ -1,6 +1,7 @@ package com.teamscale.jacoco.agent.util; import com.teamscale.client.FileSystemUtils; +import com.teamscale.client.TeamscaleServiceGenerator; import com.teamscale.jacoco.agent.PreMain; import com.teamscale.jacoco.agent.configuration.ProcessInformationRetriever; @@ -18,11 +19,15 @@ public class AgentUtils { /** Version of this program. */ public static final String VERSION; + /** User-Agent header value for HTTP requests. */ + public static final String USER_AGENT; + private static Path mainTempDirectory = null; static { ResourceBundle bundle = ResourceBundle.getBundle("com.teamscale.jacoco.agent.app"); VERSION = bundle.getString("version"); + USER_AGENT = TeamscaleServiceGenerator.buildUserAgent("Teamscale Java Profiler", VERSION); } /** diff --git a/impacted-test-engine/src/main/kotlin/com/teamscale/test_impacted/BuildVersion.kt b/impacted-test-engine/src/main/kotlin/com/teamscale/test_impacted/BuildVersion.kt new file mode 100644 index 000000000..3827b24c9 --- /dev/null +++ b/impacted-test-engine/src/main/kotlin/com/teamscale/test_impacted/BuildVersion.kt @@ -0,0 +1,12 @@ +package com.teamscale.test_impacted + +import java.util.* + +/** Provides access to the Impacted Test Engine version at runtime. */ +object BuildVersion { + + private val bundle = ResourceBundle.getBundle("com.teamscale.test_impacted.app") + + /** The version of the Teamscale Impacted Test Engine. */ + val VERSION: String = bundle.getString("version") +} diff --git a/impacted-test-engine/src/main/kotlin/com/teamscale/test_impacted/engine/options/TestEngineOptions.kt b/impacted-test-engine/src/main/kotlin/com/teamscale/test_impacted/engine/options/TestEngineOptions.kt index 5df315aa3..43301f4af 100644 --- a/impacted-test-engine/src/main/kotlin/com/teamscale/test_impacted/engine/options/TestEngineOptions.kt +++ b/impacted-test-engine/src/main/kotlin/com/teamscale/test_impacted/engine/options/TestEngineOptions.kt @@ -2,6 +2,8 @@ package com.teamscale.test_impacted.engine.options import com.teamscale.client.CommitDescriptor import com.teamscale.client.TeamscaleClient +import com.teamscale.client.TeamscaleServiceGenerator +import com.teamscale.test_impacted.BuildVersion import com.teamscale.test_impacted.engine.ImpactedTestEngineConfiguration import com.teamscale.test_impacted.engine.TestDataWriter import com.teamscale.test_impacted.engine.TestEngineRegistry @@ -122,7 +124,8 @@ class TestEngineOptions( serverOptions.userName, serverOptions.userAccessToken, serverOptions.project, - File(reportDirectory, "server-request.txt") + File(reportDirectory, "server-request.txt"), + userAgent = TeamscaleServiceGenerator.buildUserAgent("Teamscale Impacted Test Engine", BuildVersion.VERSION) ) return ImpactedTestsProvider( client, baseline, baselineRevision, endCommit, endRevision, repository, partition, diff --git a/impacted-test-engine/src/main/resources/com/teamscale/test_impacted/app.properties b/impacted-test-engine/src/main/resources/com/teamscale/test_impacted/app.properties new file mode 100644 index 000000000..c27fe3a75 --- /dev/null +++ b/impacted-test-engine/src/main/resources/com/teamscale/test_impacted/app.properties @@ -0,0 +1 @@ +version=%VERSION_TOKEN_REPLACED_DURING_BUILD% diff --git a/system-tests/http-redirect-test/src/test/kotlin/com/teamscale/client/HttpRedirectSystemTest.kt b/system-tests/http-redirect-test/src/test/kotlin/com/teamscale/client/HttpRedirectSystemTest.kt index 82ddbfa7d..6b3e0336d 100644 --- a/system-tests/http-redirect-test/src/test/kotlin/com/teamscale/client/HttpRedirectSystemTest.kt +++ b/system-tests/http-redirect-test/src/test/kotlin/com/teamscale/client/HttpRedirectSystemTest.kt @@ -33,7 +33,7 @@ class HttpRedirectSystemTest { private fun checkCustomUserAgent(teamscaleMockServer: TeamscaleMockServer) { val collectedUserAgents = teamscaleMockServer.collectedUserAgents - assertThat(collectedUserAgents).containsExactly(TeamscaleServiceGenerator.USER_AGENT) + assertThat(collectedUserAgents).allMatch { it.matches(Regex("Teamscale Java Profiler/\\d+\\.\\d+.*")) } } companion object { diff --git a/teamscale-client/src/main/kotlin/com/teamscale/client/TeamscaleClient.kt b/teamscale-client/src/main/kotlin/com/teamscale/client/TeamscaleClient.kt index 645576efe..4691c9bf3 100644 --- a/teamscale-client/src/main/kotlin/com/teamscale/client/TeamscaleClient.kt +++ b/teamscale-client/src/main/kotlin/com/teamscale/client/TeamscaleClient.kt @@ -27,13 +27,14 @@ open class TeamscaleClient { user: String, accessToken: String, projectId: String?, + userAgent: String, readTimeout: Duration = HttpUtils.DEFAULT_READ_TIMEOUT, writeTimeout: Duration = HttpUtils.DEFAULT_WRITE_TIMEOUT ) { val url = baseUrl?.toHttpUrlOrNull() ?: throw IllegalArgumentException("Invalid URL: $baseUrl") this.projectId = projectId service = TeamscaleServiceGenerator.createService( - ITeamscaleService::class.java, url, user, accessToken, readTimeout, writeTimeout + ITeamscaleService::class.java, url, user, accessToken, userAgent, readTimeout, writeTimeout ) } @@ -46,12 +47,13 @@ open class TeamscaleClient { projectId: String?, logfile: File?, readTimeout: Duration = HttpUtils.DEFAULT_READ_TIMEOUT, - writeTimeout: Duration = HttpUtils.DEFAULT_WRITE_TIMEOUT + writeTimeout: Duration = HttpUtils.DEFAULT_WRITE_TIMEOUT, + userAgent: String ) { val url = baseUrl?.toHttpUrlOrNull() ?: throw IllegalArgumentException("Invalid URL: $baseUrl") this.projectId = projectId service = TeamscaleServiceGenerator.createServiceWithRequestLogging( - ITeamscaleService::class.java, url, user, accessToken, logfile, readTimeout, writeTimeout + ITeamscaleService::class.java, url, user, accessToken, logfile, readTimeout, writeTimeout, userAgent ) } diff --git a/teamscale-client/src/main/kotlin/com/teamscale/client/TeamscaleServiceGenerator.kt b/teamscale-client/src/main/kotlin/com/teamscale/client/TeamscaleServiceGenerator.kt index e5683422e..54d925131 100644 --- a/teamscale-client/src/main/kotlin/com/teamscale/client/TeamscaleServiceGenerator.kt +++ b/teamscale-client/src/main/kotlin/com/teamscale/client/TeamscaleServiceGenerator.kt @@ -1,6 +1,9 @@ package com.teamscale.client -import okhttp3.* +import okhttp3.HttpUrl +import okhttp3.Interceptor +import okhttp3.OkHttpClient +import okhttp3.Response import retrofit2.Retrofit import retrofit2.converter.jackson.JacksonConverterFactory import java.io.File @@ -9,8 +12,9 @@ import java.time.Duration /** Helper class for generating a teamscale compatible service. */ object TeamscaleServiceGenerator { - /** Custom user agent of the requests, used to monitor API traffic. */ - const val USER_AGENT = "Teamscale Java Profiler" + /** Builds the User-Agent string for the given tool name and version. */ + @JvmStatic + fun buildUserAgent(toolName: String, version: String) = "$toolName/$version" /** * Generates a [Retrofit] instance for the given service, which uses basic auth to authenticate against the @@ -23,11 +27,12 @@ object TeamscaleServiceGenerator { baseUrl: HttpUrl, username: String, accessToken: String, + userAgent: String, readTimeout: Duration = HttpUtils.DEFAULT_READ_TIMEOUT, writeTimeout: Duration = HttpUtils.DEFAULT_WRITE_TIMEOUT, vararg interceptors: Interceptor ) = createServiceWithRequestLogging( - serviceClass, baseUrl, username, accessToken, null, readTimeout, writeTimeout, *interceptors + serviceClass, baseUrl, username, accessToken, null, readTimeout, writeTimeout, userAgent, *interceptors ) /** @@ -42,6 +47,7 @@ object TeamscaleServiceGenerator { logfile: File?, readTimeout: Duration, writeTimeout: Duration, + userAgent: String, vararg interceptors: Interceptor ): S = HttpUtils.createRetrofit( { retrofitBuilder -> @@ -52,7 +58,7 @@ object TeamscaleServiceGenerator { okHttpBuilder.addInterceptors(*interceptors) .addInterceptor(HttpUtils.getBasicAuthInterceptor(username, accessToken)) .addInterceptor(AcceptJsonInterceptor()) - .addNetworkInterceptor(CustomUserAgentInterceptor()) + .addNetworkInterceptor(CustomUserAgentInterceptor(userAgent)) logfile?.let { okHttpBuilder.addInterceptor(FileLoggingInterceptor(it)) } }, readTimeout, writeTimeout @@ -79,12 +85,12 @@ object TeamscaleServiceGenerator { } /** - * Sets the custom user agent [.USER_AGENT] header on all requests. + * Sets the custom user agent header on all requests. */ - class CustomUserAgentInterceptor : Interceptor { + class CustomUserAgentInterceptor(private val userAgent: String) : Interceptor { @Throws(IOException::class) override fun intercept(chain: Interceptor.Chain): Response { - val newRequest = chain.request().newBuilder().header("User-Agent", USER_AGENT).build() + val newRequest = chain.request().newBuilder().header("User-Agent", userAgent).build() return chain.proceed(newRequest) } } diff --git a/teamscale-client/src/test/kotlin/com/teamscale/client/TeamscaleServiceGeneratorProxyServerTest.kt b/teamscale-client/src/test/kotlin/com/teamscale/client/TeamscaleServiceGeneratorProxyServerTest.kt index 2e91476a6..b88e50adb 100644 --- a/teamscale-client/src/test/kotlin/com/teamscale/client/TeamscaleServiceGeneratorProxyServerTest.kt +++ b/teamscale-client/src/test/kotlin/com/teamscale/client/TeamscaleServiceGeneratorProxyServerTest.kt @@ -1,6 +1,7 @@ package com.teamscale.client import com.teamscale.client.HttpUtils.PROXY_AUTHORIZATION_HTTP_HEADER +import com.teamscale.client.TeamscaleServiceGenerator.buildUserAgent import com.teamscale.client.TeamscaleServiceGenerator.createService import okhttp3.HttpUrl.Companion.toHttpUrl import okhttp3.mockwebserver.MockResponse @@ -61,7 +62,8 @@ internal class TeamscaleServiceGeneratorProxyServerTest { val service = createService( ITeamscaleService::class.java, "http://localhost:1337".toHttpUrl(), - "someUser", "someAccesstoken" + "someUser", "someAccesstoken", + userAgent = buildUserAgent("Test Tool", "1.0.0") ) // First time Retrofit/OkHttp tires without proxy auth. @@ -91,4 +93,4 @@ internal class TeamscaleServiceGeneratorProxyServerTest { mockProxyServer?.shutdown() mockProxyServer?.close() } -} \ No newline at end of file +} diff --git a/teamscale-gradle-plugin/src/main/kotlin/com/teamscale/config/ServerConfiguration.kt b/teamscale-gradle-plugin/src/main/kotlin/com/teamscale/config/ServerConfiguration.kt index 075046afc..090befee0 100644 --- a/teamscale-gradle-plugin/src/main/kotlin/com/teamscale/config/ServerConfiguration.kt +++ b/teamscale-gradle-plugin/src/main/kotlin/com/teamscale/config/ServerConfiguration.kt @@ -1,6 +1,8 @@ package com.teamscale.config import com.teamscale.client.TeamscaleClient +import com.teamscale.client.TeamscaleServiceGenerator +import com.teamscale.utils.BuildVersion import org.gradle.api.GradleException import org.gradle.api.provider.Property import java.io.Serializable @@ -33,5 +35,8 @@ abstract class ServerConfiguration : Serializable { } } - fun toClient() = TeamscaleClient(url.get(), userName.get(), userAccessToken.get(), project.get()) + fun toClient() = TeamscaleClient( + url.get(), userName.get(), userAccessToken.get(), project.get(), + userAgent = TeamscaleServiceGenerator.buildUserAgent("Teamscale Gradle Plugin", BuildVersion.pluginVersion) + ) } diff --git a/teamscale-maven-plugin/src/main/java/com/teamscale/maven/BuildVersion.java b/teamscale-maven-plugin/src/main/java/com/teamscale/maven/BuildVersion.java new file mode 100644 index 000000000..4eae8bf13 --- /dev/null +++ b/teamscale-maven-plugin/src/main/java/com/teamscale/maven/BuildVersion.java @@ -0,0 +1,12 @@ +package com.teamscale.maven; + +import java.util.ResourceBundle; + +/** Provides access to the Maven plugin version at runtime. */ +public class BuildVersion { + + private static final ResourceBundle BUNDLE = ResourceBundle.getBundle("com.teamscale.maven.app"); + + /** The version of the Teamscale Maven plugin. */ + public static final String VERSION = BUNDLE.getString("version"); +} diff --git a/teamscale-maven-plugin/src/main/java/com/teamscale/maven/upload/CoverageUploadMojo.java b/teamscale-maven-plugin/src/main/java/com/teamscale/maven/upload/CoverageUploadMojo.java index 38e233bbb..7a5b2a221 100644 --- a/teamscale-maven-plugin/src/main/java/com/teamscale/maven/upload/CoverageUploadMojo.java +++ b/teamscale-maven-plugin/src/main/java/com/teamscale/maven/upload/CoverageUploadMojo.java @@ -3,6 +3,8 @@ import com.teamscale.client.CommitDescriptor; import com.teamscale.client.EReportFormat; import com.teamscale.client.TeamscaleClient; +import com.teamscale.client.TeamscaleServiceGenerator; +import com.teamscale.maven.BuildVersion; import com.teamscale.maven.DependencyUtils; import com.teamscale.maven.TeamscaleMojoBase; import com.teamscale.maven.tia.TestwiseCoverageReportMojo; @@ -32,9 +34,9 @@ import java.util.stream.Collectors; /** - * Run this goal after the Jacoco and Testwise Coverage report generation to upload them to a configured Teamscale instance. - * The configuration can be specified in the root Maven project. - * The goal should only be run in the aggregator module. + * Run this goal after the Jacoco and Testwise Coverage report generation to upload them to a configured Teamscale + * instance. The configuration can be specified in the root Maven project. The goal should only be run in the aggregator + * module. *

* Offers the following functionality: *

    @@ -130,7 +132,8 @@ public void execute() throws MojoFailureException, MojoExecutionException { getLog().debug("Skipping since skip is set to true"); return; } - teamscaleClient = new TeamscaleClient(teamscaleUrl, username, accessToken, projectId); + teamscaleClient = new TeamscaleClient(teamscaleUrl, username, accessToken, projectId, + TeamscaleServiceGenerator.buildUserAgent("Teamscale Maven Plugin", BuildVersion.VERSION)); getLog().debug("Resolving end commit"); resolveCommitOrRevision(); getLog().debug("Parsing Jacoco plugin configurations"); @@ -257,7 +260,8 @@ private void uploadCoverage(String partition, Map> reportsByF getLog().info( String.format("Uploading %d report for project %s to %s", reportsByFormat.values().stream().mapToLong( Collection::size).sum(), projectId, partition)); - teamscaleClient.uploadReports(reportsByFormat, CommitDescriptor.parse(resolvedCommit), resolvedRevision, repository, + teamscaleClient.uploadReports(reportsByFormat, CommitDescriptor.parse(resolvedCommit), resolvedRevision, + repository, partition, COVERAGE_UPLOAD_MESSAGE); } diff --git a/teamscale-maven-plugin/src/main/resources/com/teamscale/maven/app.properties b/teamscale-maven-plugin/src/main/resources/com/teamscale/maven/app.properties new file mode 100644 index 000000000..c27fe3a75 --- /dev/null +++ b/teamscale-maven-plugin/src/main/resources/com/teamscale/maven/app.properties @@ -0,0 +1 @@ +version=%VERSION_TOKEN_REPLACED_DURING_BUILD%