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"
}
}

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
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")
)
}
}
Copy link
Member

Choose a reason for hiding this comment

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

Missing Test Coverage: SSE and Analytics

Great test coverage for REST API calls! However, the User-Agent interceptor is also added to:

  • SSE client (Server-Sent Events) in FlagsmithEventService.kt:24
  • Analytics requests

Recommendation: Add tests to verify User-Agent is sent with SSE and analytics requests:

@Test
fun testUserAgentHeaderSentWithSSEConnection() {
    // Test that User-Agent is included when connecting to SSE endpoint
}

@Test
fun testUserAgentHeaderSentWithAnalyticsRequest() {
    // Test that User-Agent is included when posting analytics
}

This ensures complete coverage of all network request types.

Copy link
Member

Choose a reason for hiding this comment

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

This will be a total pain, I think we can leave this