Skip to content
Open
Show file tree
Hide file tree
Changes from 3 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"
}
}

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.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Hi @emyller I've tried both PowerMock and MockK and there doesn't seem to be a way that I can find that can sufficiently mock the Class.forName() functionality.

I could refactor getSdkVersion into an interface so that I can mock the dependency into the main Retrofit class but again it doesn't really test any extra code and adds more complexity.

I'm a lot happier with the code now that we're falling back to release-please as per the iOS SDK and the updated function looks pretty bullet proof and as tested as I can get it really.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Please check out the updated implementation and tests @emyller and let me know what you think

Copy link

Choose a reason for hiding this comment

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

I think the fate of this thread will lie on this other thread.

On the good news, I believe this becomes much simpler. See example. Let me know if you have any questions.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Should be sorted now @emyller please check, thanks

Copy link

@emyller emyller Nov 18, 2025

Choose a reason for hiding this comment

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

Thanks @gazreese. Sorry I didn't make it clear in my comment above. What I really meant by "much simpler" is that we may not need these tests at all. From my perspective, testUserAgentHeaderIsPersistentAcrossRequests already checks the UA header is added in the API handler exactly as expected.

As pointed out in the other thread, it may even suffice as the only test in this PR. The others feel more of the same, but I'd love to hear your thoughts here.

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