Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion FlagsmithClient/src/main/java/com/flagsmith/Flagsmith.kt
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -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<FlagEvent>) -> 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)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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"
}
}
Comment on lines +50 to +60
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is the Android machinery used to obtain the Flagsmith client version more reliable than iOS'? I'm asking considering the equivalent pull request to iOS (Flagsmith/flagsmith-ios-client#95) falls back to a hardcoded version string when obtaining the lib version fails, and I wonder why the same strategy isn't used here as well. (I'm not asking we implement it)


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 <T : FlagsmithRetrofitService> create(
baseUrl: String,
Expand Down Expand Up @@ -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 }
Expand Down
199 changes: 199 additions & 0 deletions FlagsmithClient/src/test/java/com/flagsmith/UserAgentTests.kt
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is it possible to somehow mock the version so it can be seen in at least one test? All current tests only verify unknown is added in the header.

Original file line number Diff line number Diff line change
@@ -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")
)
}
}
Loading