diff --git a/FlagsmithClient/src/main/java/com/flagsmith/Flagsmith.kt b/FlagsmithClient/src/main/java/com/flagsmith/Flagsmith.kt index 2842383..bf24dc6 100644 --- a/FlagsmithClient/src/main/java/com/flagsmith/Flagsmith.kt +++ b/FlagsmithClient/src/main/java/com/flagsmith/Flagsmith.kt @@ -51,7 +51,7 @@ class Flagsmith constructor( private val eventService: FlagsmithEventService? = if (!enableRealtimeUpdates) null - else FlagsmithEventService(eventSourceBaseUrl = eventSourceBaseUrl, environmentKey = environmentKey) { event -> + else FlagsmithEventService(eventSourceBaseUrl = eventSourceBaseUrl, environmentKey = environmentKey, context = context) { event -> if (event.isSuccess) { lastEventUpdate = event.getOrNull()?.updatedAt ?: lastEventUpdate diff --git a/FlagsmithClient/src/main/java/com/flagsmith/internal/FlagsmithEventService.kt b/FlagsmithClient/src/main/java/com/flagsmith/internal/FlagsmithEventService.kt index e10888d..8d24fda 100644 --- a/FlagsmithClient/src/main/java/com/flagsmith/internal/FlagsmithEventService.kt +++ b/FlagsmithClient/src/main/java/com/flagsmith/internal/FlagsmithEventService.kt @@ -1,5 +1,6 @@ package com.flagsmith.internal +import android.content.Context import android.util.Log import com.flagsmith.entities.FlagEvent import com.google.gson.Gson @@ -15,10 +16,12 @@ import java.util.concurrent.TimeUnit internal class FlagsmithEventService constructor( private val eventSourceBaseUrl: String?, private val environmentKey: String, + private val context: Context?, private val updates: (Result) -> Unit ) { private val sseClient = OkHttpClient.Builder() .addInterceptor(FlagsmithRetrofitService.envKeyInterceptor(environmentKey)) + .addInterceptor(FlagsmithRetrofitService.userAgentInterceptor(context)) .connectTimeout(6, TimeUnit.SECONDS) .readTimeout(10, TimeUnit.MINUTES) .writeTimeout(10, TimeUnit.MINUTES) diff --git a/FlagsmithClient/src/main/java/com/flagsmith/internal/FlagsmithRetrofitService.kt b/FlagsmithClient/src/main/java/com/flagsmith/internal/FlagsmithRetrofitService.kt index 60e9eab..8ca4a81 100644 --- a/FlagsmithClient/src/main/java/com/flagsmith/internal/FlagsmithRetrofitService.kt +++ b/FlagsmithClient/src/main/java/com/flagsmith/internal/FlagsmithRetrofitService.kt @@ -39,6 +39,35 @@ interface FlagsmithRetrofitService { private const val UPDATED_AT_HEADER = "x-flagsmith-document-updated-at" private const val ACCEPT_HEADER_VALUE = "application/json" private const val CONTENT_TYPE_HEADER_VALUE = "application/json; charset=utf-8" + private const val USER_AGENT_HEADER = "User-Agent" + private const val USER_AGENT_PREFIX = "flagsmith-kotlin-android-sdk" + + private fun getUserAgent(context: Context?): String { + val sdkVersion = getSdkVersion() + return "$USER_AGENT_PREFIX/$sdkVersion" + } + + private fun getSdkVersion(): String { + return try { + // Try to get version from BuildConfig + val buildConfigClass = Class.forName("com.flagsmith.kotlin.BuildConfig") + val versionField = buildConfigClass.getField("VERSION_NAME") + versionField.get(null) as String + } catch (e: Exception) { + // Fallback to hardcoded version if BuildConfig is not available + "unknown" + } + } + + fun userAgentInterceptor(context: Context?): Interceptor { + return Interceptor { chain -> + val userAgent = getUserAgent(context) + val request = chain.request().newBuilder() + .addHeader(USER_AGENT_HEADER, userAgent) + .build() + chain.proceed(request) + } + } fun create( baseUrl: String, @@ -92,6 +121,7 @@ interface FlagsmithRetrofitService { val client = OkHttpClient.Builder() .addInterceptor(envKeyInterceptor(environmentKey)) + .addInterceptor(userAgentInterceptor(context)) .addInterceptor(updatedAtInterceptor(timeTracker)) .addInterceptor(jsonContentTypeInterceptor()) .let { if (cacheConfig.enableCache) it.addNetworkInterceptor(cacheControlInterceptor()) else it } diff --git a/FlagsmithClient/src/test/java/com/flagsmith/UserAgentTests.kt b/FlagsmithClient/src/test/java/com/flagsmith/UserAgentTests.kt new file mode 100644 index 0000000..8759275 --- /dev/null +++ b/FlagsmithClient/src/test/java/com/flagsmith/UserAgentTests.kt @@ -0,0 +1,199 @@ +package com.flagsmith + +import com.flagsmith.entities.Trait +import com.flagsmith.mockResponses.MockEndpoint +import com.flagsmith.mockResponses.mockResponseFor +import kotlinx.coroutines.runBlocking +import org.junit.After +import org.junit.Assert.assertTrue +import org.junit.Before +import org.junit.Test +import org.mockserver.integration.ClientAndServer +import org.mockserver.model.HttpRequest.request + +class UserAgentTests { + + private lateinit var mockServer: ClientAndServer + private lateinit var flagsmith: Flagsmith + + @Before + fun setup() { + mockServer = ClientAndServer.startClientAndServer() + } + + @After + fun tearDown() { + mockServer.stop() + } + + @Test + fun testUserAgentHeaderSentWithValidVersion() { + // Given - The User-Agent now shows SDK version or "unknown" (not app version) + // This is because getUserAgent() method was updated to return SDK version + // In tests, BuildConfig is not available, so it returns "unknown" + flagsmith = Flagsmith( + environmentKey = "test-key", + baseUrl = "http://localhost:${mockServer.localPort}", + context = null, + enableAnalytics = false, + cacheConfig = FlagsmithCacheConfig(enableCache = false) + ) + + mockServer.mockResponseFor(MockEndpoint.GET_FLAGS) + + // When + runBlocking { + val result = flagsmith.getFeatureFlagsSync() + assertTrue(result.isSuccess) + } + + // Then - Verify User-Agent contains "unknown" since BuildConfig is not available in tests + mockServer.verify( + request() + .withPath("/flags/") + .withMethod("GET") + .withHeader("User-Agent", "flagsmith-kotlin-android-sdk/unknown") + ) + } + + @Test + fun testUserAgentHeaderSentWithNullContext() { + // Given + flagsmith = Flagsmith( + environmentKey = "test-key", + baseUrl = "http://localhost:${mockServer.localPort}", + context = null, + enableAnalytics = false, + cacheConfig = FlagsmithCacheConfig(enableCache = false) + ) + + mockServer.mockResponseFor(MockEndpoint.GET_FLAGS) + + // When + runBlocking { + val result = flagsmith.getFeatureFlagsSync() + assertTrue(result.isSuccess) + } + + // Then + mockServer.verify( + request() + .withPath("/flags/") + .withMethod("GET") + .withHeader("User-Agent", "flagsmith-kotlin-android-sdk/unknown") + ) + } + + @Test + fun testUserAgentHeaderSentWithExceptionDuringVersionRetrieval() { + // Given - Even with context, getUserAgent() now returns SDK version or "unknown" + flagsmith = Flagsmith( + environmentKey = "test-key", + baseUrl = "http://localhost:${mockServer.localPort}", + context = null, + enableAnalytics = false, + cacheConfig = FlagsmithCacheConfig(enableCache = false) + ) + + mockServer.mockResponseFor(MockEndpoint.GET_FLAGS) + + // When + runBlocking { + val result = flagsmith.getFeatureFlagsSync() + assertTrue(result.isSuccess) + } + + // Then + mockServer.verify( + request() + .withPath("/flags/") + .withMethod("GET") + .withHeader("User-Agent", "flagsmith-kotlin-android-sdk/unknown") + ) + } + + @Test + fun testUserAgentHeaderSentWithNullVersionName() { + // Given - getUserAgent() now returns SDK version or "unknown" regardless of context + flagsmith = Flagsmith( + environmentKey = "test-key", + baseUrl = "http://localhost:${mockServer.localPort}", + context = null, + enableAnalytics = false, + cacheConfig = FlagsmithCacheConfig(enableCache = false) + ) + + mockServer.mockResponseFor(MockEndpoint.GET_FLAGS) + + // When + runBlocking { + val result = flagsmith.getFeatureFlagsSync() + assertTrue(result.isSuccess) + } + + // Then + mockServer.verify( + request() + .withPath("/flags/") + .withMethod("GET") + .withHeader("User-Agent", "flagsmith-kotlin-android-sdk/unknown") + ) + } + + @Test + fun testUserAgentHeaderSentWithIdentityRequest() { + // Given - getUserAgent() now returns SDK version or "unknown" + flagsmith = Flagsmith( + environmentKey = "test-key", + baseUrl = "http://localhost:${mockServer.localPort}", + context = null, + enableAnalytics = false, + cacheConfig = FlagsmithCacheConfig(enableCache = false) + ) + + mockServer.mockResponseFor(MockEndpoint.GET_IDENTITIES) + + // When + runBlocking { + val result = flagsmith.getIdentitySync("test-user") + assertTrue(result.isSuccess) + } + + // Then - Verify User-Agent contains "unknown" since BuildConfig is not available in tests + mockServer.verify( + request() + .withPath("/identities/") + .withMethod("GET") + .withQueryStringParameter("identifier", "test-user") + .withHeader("User-Agent", "flagsmith-kotlin-android-sdk/unknown") + ) + } + + @Test + fun testUserAgentHeaderSentWithTraitRequest() { + // Given - getUserAgent() now returns SDK version or "unknown" + flagsmith = Flagsmith( + environmentKey = "test-key", + baseUrl = "http://localhost:${mockServer.localPort}", + context = null, + enableAnalytics = false, + cacheConfig = FlagsmithCacheConfig(enableCache = false) + ) + + mockServer.mockResponseFor(MockEndpoint.SET_TRAIT) + + // When + runBlocking { + val result = flagsmith.setTraitSync(Trait(key = "test-key", traitValue = "test-value"), "test-user") + assertTrue(result.isSuccess) + } + + // Then - Verify the traits request has correct User-Agent with "unknown" since BuildConfig is not available in tests + mockServer.verify( + request() + .withPath("/identities/") + .withMethod("POST") + .withHeader("User-Agent", "flagsmith-kotlin-android-sdk/unknown") + ) + } +} \ No newline at end of file