Skip to content

feat: Send a standard user-agent header#1

Open
polatolu wants to merge 7 commits intomainfrom
feature/send-user-agent-header
Open

feat: Send a standard user-agent header#1
polatolu wants to merge 7 commits intomainfrom
feature/send-user-agent-header

Conversation

@polatolu
Copy link
Copy Markdown
Collaborator

@polatolu polatolu commented Oct 13, 2025

Description

Added a User-Agent HTTP header to network calls. Uses the package's version at runtime, if it is not available sends it as "unknown"

Also added a PR template, once this merged new PRs will come with this PR template.

Regression Test Recommendations

  • Test that User-Agent header is present in all HTTP requests with correct format: flagsmith-swift-ios-sdk/
  • Test fallback to "unknown" when version is not discoverable at runtime
  • Validate User-Agent appears in both REST API calls and SSE connections

Type of Change

  • ✨ New feature (non-breaking change which adds functionality)
  • 🛠️ Bug fix (non-breaking change which fixes an issue)
  • ❌ Breaking change (fix or feature that would cause existing functionality to change)
  • 🧹 Code refactor
  • ✅ Build configuration change
  • 📝 Documentation
  • 🗑️ Chore

Summary by CodeRabbit

  • New Features

    • Added User-Agent header to all API requests for improved SDK identification and analytics tracking.
  • Tests

    • Added comprehensive test coverage for User-Agent header formation and persistence across request types.

@coderabbitai
Copy link
Copy Markdown

coderabbitai bot commented Oct 13, 2025

Walkthrough

This pull request implements User-Agent header support for HTTP requests by adding interceptors to the Flagsmith SDK. The User-Agent is formatted as "flagsmith-kotlin-android-sdk/{version}" and attached to all outgoing requests. Comprehensive test coverage validates header presence and format across various API endpoints.

Changes

Cohort / File(s) Change Summary
User-Agent Interceptor Implementation
FlagsmithClient/src/main/java/com/flagsmith/internal/FlagsmithRetrofitService.kt
Introduces USER\_AGENT\_HEADER and USER\_AGENT\_PREFIX constants, adds private helper methods getSdkVersion() and getUserAgent() (with hardcoded "1.8.0" version), introduces public userAgentInterceptor() method returning an Interceptor that injects the User-Agent header into requests, and integrates the interceptor into the Retrofit client construction pipeline.
Event Service Integration
FlagsmithClient/src/main/java/com/flagsmith/internal/FlagsmithEventService.kt
Adds userAgentInterceptor() to the OkHttpClient.Builder after envKeyInterceptor to apply User-Agent headers to event requests.
User-Agent Test Coverage
FlagsmithClient/src/test/java/com/flagsmith/UserAgentTests.kt
New comprehensive test class with MockServer setup validating User-Agent header format ("flagsmith-kotlin-android-sdk/1.8.0") across multiple request types: GET /flags/, null context handling, identity requests, and trait POST requests. Includes testUserAgentFormat() validation and runBlocking synchronous API call execution.
SDK Version Retrieval Tests
FlagsmithClient/src/test/java/com/flagsmith/internal/SdkVersionRetrievalTest.kt
New test class using MockWebServer to validate User-Agent interceptor functionality, including header presence, version format validation (semantic versioning or valid identifier), and persistence across multiple requests.
Build Configuration
FlagsmithClient/build.gradle.kts
Adds testImplementation dependency: com.squareup.okhttp3:mockwebserver:4.11.0 for HTTP mock testing.
Release Configuration
release-please-config.json
Adds extra-files array including FlagsmithRetrofitService.kt, UserAgentTests.kt, and SdkVersionRetrievalTest.kt for release tagging automation.

Sequence Diagram

sequenceDiagram
    participant Client as HTTP Client
    participant UI as UserAgentInterceptor
    participant Net as Network Request
    participant Server as Server

    Client->>UI: Request
    rect rgb(200, 220, 255)
        Note over UI: Intercept Request
        UI->>UI: Get SDK version (1.8.0)
        UI->>UI: Compose User-Agent<br/>flagsmith-kotlin-android-sdk/1.8.0
        UI->>UI: Add header to request
    end
    UI->>Net: Forward with header
    Net->>Server: HTTP Request<br/>(includes User-Agent)
    Server->>Net: Response
    Net->>Client: Return Response
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~22 minutes

  • FlagsmithRetrofitService.kt: Review User-Agent composition logic, hardcoded version handling ("1.8.0"), and interceptor chain integration
  • Test files (UserAgentTests.kt, SdkVersionRetrievalTest.kt): Verify comprehensive coverage of all request types and header validation patterns across MockServer scenarios
  • Consistency verification: Ensure version extraction logic in getSdkVersion() aligns with actual release process expectations

Poem

🐰 A header hops through the network's way,
With version tags the SDK will say,
Each request dressed in "flagsmith-android" attire,
From FlagsmithRetrofit's interceptor fire!
🚀✨

Pre-merge checks and finishing touches

❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. You can run @coderabbitai generate docstrings to improve docstring coverage.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The pull request title accurately describes the main change: adding User-Agent header support to HTTP requests in the Flagsmith Kotlin Android SDK.
✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch feature/send-user-agent-header

📜 Recent review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 2d6527f and c1c4edc.

📒 Files selected for processing (3)
  • FlagsmithClient/src/main/java/com/flagsmith/internal/FlagsmithEventService.kt (1 hunks)
  • FlagsmithClient/src/main/java/com/flagsmith/internal/FlagsmithRetrofitService.kt (2 hunks)
  • FlagsmithClient/src/test/java/com/flagsmith/internal/SdkVersionRetrievalTest.kt (1 hunks)
🔇 Additional comments (8)
FlagsmithClient/src/main/java/com/flagsmith/internal/FlagsmithEventService.kt (1)

22-22: LGTM! User-Agent interceptor correctly integrated into SSE client.

The User-Agent interceptor is properly added to the SSE OkHttpClient configuration, ensuring that Server-Sent Events connections include the SDK identification header alongside the environment key.

FlagsmithClient/src/main/java/com/flagsmith/internal/FlagsmithRetrofitService.kt (4)

42-43: LGTM! Constants appropriately defined.

The User-Agent header name and SDK prefix are correctly defined as private constants with clear, standard naming.


45-48: LGTM! User-Agent string composition is clear and correct.

The function properly formats the User-Agent string by combining the SDK prefix with the version.


50-54: LGTM! Version management correctly configured for automation.

The hardcoded version with release-please markers is the correct approach for automated SDK version management. The version will be automatically updated by the release-please tooling.


118-118: LGTM! Interceptor properly integrated into the HTTP client.

The User-Agent interceptor is correctly added to the OkHttpClient builder, ensuring all Retrofit API calls include the SDK identification header.

FlagsmithClient/src/test/java/com/flagsmith/internal/SdkVersionRetrievalTest.kt (3)

24-30: LGTM! Test constants properly aligned with implementation.

The test constants correctly mirror the implementation values and use release-please markers to ensure version synchronization between tests and production code.


43-69: LGTM! Test effectively validates User-Agent header format.

The test properly verifies that the interceptor adds a User-Agent header with the expected prefix format. The use of MockWebServer and resource management with .use is appropriate.


104-140: LGTM! Test effectively validates User-Agent persistence.

The test properly verifies that the User-Agent header remains consistent across multiple requests and matches the expected format with the SDK version. The test structure is clear and assertions are appropriate.


Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between a9ecc05 and 5cea695.

📒 Files selected for processing (4)
  • FlagsmithClient/src/main/java/com/flagsmith/Flagsmith.kt (1 hunks)
  • FlagsmithClient/src/main/java/com/flagsmith/internal/FlagsmithEventService.kt (2 hunks)
  • FlagsmithClient/src/main/java/com/flagsmith/internal/FlagsmithRetrofitService.kt (2 hunks)
  • FlagsmithClient/src/test/java/com/flagsmith/UserAgentTests.kt (1 hunks)
🔇 Additional comments (6)
FlagsmithClient/src/main/java/com/flagsmith/Flagsmith.kt (1)

54-54: LGTM! Context parameter correctly wired for realtime updates.

The change properly passes the context parameter to FlagsmithEventService when realtime updates are enabled, which is necessary for the new User-Agent header functionality.

FlagsmithClient/src/main/java/com/flagsmith/internal/FlagsmithEventService.kt (2)

19-19: LGTM! Context parameter properly added.

The context parameter is correctly added to enable User-Agent header injection for SSE connections.


24-24: LGTM! User-Agent interceptor properly wired for SSE client.

The userAgentInterceptor is correctly added to the OkHttpClient builder, ensuring the User-Agent header is present in SSE connections alongside the environment key header.

FlagsmithClient/src/main/java/com/flagsmith/internal/FlagsmithRetrofitService.kt (3)

42-53: LGTM! User-Agent construction is robust.

The implementation correctly:

  • Handles null context with safe navigation operators
  • Falls back to "unknown" when versionName is null or an exception occurs
  • Uses appropriate error handling for defensive coding

55-63: LGTM! Interceptor follows standard OkHttp patterns.

The interceptor correctly adds the User-Agent header to each request using the standard chain-based approach.


117-117: LGTM! User-Agent interceptor properly wired for REST API client.

The interceptor is correctly added to the OkHttpClient builder, ensuring the User-Agent header is present in all REST API calls.

Comment thread FlagsmithClient/src/test/java/com/flagsmith/UserAgentTests.kt
@gazreese gazreese changed the title implementation and testing feat: Send a standard user-agent header Oct 15, 2025
Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 0

🧹 Nitpick comments (2)
FlagsmithClient/src/test/java/com/flagsmith/UserAgentTests.kt (2)

47-175: Consider extracting common mock setup to reduce duplication.

The first four tests repeat similar mock setup code for Context, PackageManager, and PackageInfo. While test clarity is important, extracting this into a helper method could improve maintainability.

Example refactor:

private fun setupMockContext(versionName: String?) {
    mockPackageInfo.versionName = versionName
    Mockito.`when`(mockContext.packageManager).thenReturn(mockPackageManager)
    Mockito.`when`(mockContext.packageName).thenReturn("com.test.app")
    Mockito.`when`(mockPackageManager.getPackageInfo(mockContext.packageName, 0))
        .thenReturn(mockPackageInfo)
}

private fun setupMockContextWithException() {
    Mockito.`when`(mockContext.packageManager).thenReturn(mockPackageManager)
    Mockito.`when`(mockContext.packageName).thenReturn("com.test.app")
    Mockito.`when`(mockPackageManager.getPackageInfo(mockContext.packageName, 0))
        .thenThrow(PackageManager.NameNotFoundException("Package not found"))
}

Then use: setupMockContext("2.4.1") or setupMockContextWithException() in each test.


47-244: Add SSE User-Agent header test or document interceptor usage.
FlagsmithEventService’s sseClient is already configured with userAgentInterceptor(context), so SSE connections include the User-Agent header—but there’s no test covering this. Either add a unit test that mocks the SSE endpoint and verifies the User-Agent on the SSE request, or add a comment in FlagsmithEventService.kt explaining that the interceptor ensures User-Agent is applied to SSE streams.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 5cea695 and b184964.

📒 Files selected for processing (1)
  • FlagsmithClient/src/test/java/com/flagsmith/UserAgentTests.kt (1 hunks)
🔇 Additional comments (1)
FlagsmithClient/src/test/java/com/flagsmith/UserAgentTests.kt (1)

1-245: Excellent improvements! Tests now properly validate User-Agent header injection.

The test suite has been completely rewritten to address the critical issues flagged in the previous review. The tests now:

  • Create real Flagsmith instances with properly mocked Android context
  • Execute actual API calls (getFeatureFlagsSync, getIdentitySync, setTraitSync)
  • Use MockServer to capture and verify HTTP requests with the expected User-Agent header
  • Cover key scenarios: valid version, null context, exception handling, and null version name

This is a significant improvement over the previous literal string equality assertions that provided no actual test coverage.

Copy link
Copy Markdown
Member

@gazreese gazreese left a comment

Choose a reason for hiding this comment

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

Comprehensive Review Summary

Thank you for this well-implemented feature! The code quality is high with excellent test coverage. However, I've identified a few important issues that should be addressed.

🔴 Critical Issues

1. Semantic Confusion: APP Version vs SDK Version (HIGH PRIORITY)

The User-Agent header uses the format flagsmith-kotlin-android-sdk/<version>, but <version> is actually the host application's version, not the SDK version. This is semantically incorrect and potentially misleading for server-side analytics.

Recommended fix: Include both SDK and app versions:

private fun getUserAgent(context: Context?): String {
    val sdkVersion = BuildConfig.VERSION_NAME ?: "unknown"
    return try {
        val packageInfo = context?.packageManager?.getPackageInfo(context.packageName, 0)
        val appVersion = packageInfo?.versionName ?: "unknown"
        "$USER_AGENT_PREFIX/$sdkVersion (app/$appVersion)"
    } catch (e: Exception) {
        "$USER_AGENT_PREFIX/$sdkVersion"
    }
}
// Result: flagsmith-kotlin-android-sdk/1.8.0 (app/2.4.1)

2. Documentation Error in PR Description

The PR description states the format as flagsmith-swift-ios-sdk/<version> (appears to be copy-pasted from iOS SDK). Should be flagsmith-kotlin-android-sdk/<version>.


🟡 Important Recommendations

3. Missing Test Coverage for SSE

While the User-Agent interceptor is correctly added to the SSE client, there are no tests validating User-Agent is sent with SSE connections or analytics requests.

4. Performance Optimization

getUserAgent() is called on every HTTP request. Should compute once when the interceptor is created:

fun userAgentInterceptor(context: Context?): Interceptor {
    val userAgent = getUserAgent(context)  // Compute once, not per-request
    return Interceptor { chain ->
        val request = chain.request().newBuilder()
            .addHeader(USER_AGENT_HEADER, userAgent)
            .build()
        chain.proceed(request)
    }
}

5. Missing PR Template File

PR description mentions adding a PR template, but no template file was added to the repository (.github/PULL_REQUEST_TEMPLATE.md).

6. Overly Broad Exception Handling

Consider catching specific exceptions instead of generic Exception:

} catch (e: PackageManager.NameNotFoundException) {
    "$USER_AGENT_PREFIX/unknown"
} catch (e: Exception) {
    Log.w("FlagsmithRetrofitService", "Unexpected error getting version", e)
    "$USER_AGENT_PREFIX/unknown"
}

✅ Positive Aspects

  • Excellent test coverage with 6 comprehensive test cases covering edge cases
  • Proper null safety throughout the implementation
  • Non-breaking change with consistent implementation across all HTTP clients
  • Clean code structure with good separation of concerns
  • Proper test isolation using MockServer and Mockito

📋 Recommendation

APPROVE with recommended fixes - The version semantics issue (#1) should be addressed before merge. The other recommendations would improve robustness and maintainability but are not blockers.

Great work overall! 🚀

private fun getUserAgent(context: Context?): String {
return try {
val packageInfo = context?.packageManager?.getPackageInfo(context.packageName, 0)
val version = packageInfo?.versionName ?: "unknown"
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Critical Issue: Semantic confusion between APP and SDK versions

This retrieves the host application's version (via context.packageName), not the SDK version. However, the User-Agent prefix is flagsmith-kotlin-android-sdk, which suggests it should contain the SDK version.

This is misleading for server-side analytics and support teams who might expect to see the SDK version for debugging SDK-specific issues.

Recommendation: Include both versions for clarity:

private fun getUserAgent(context: Context?): String {
    val sdkVersion = BuildConfig.VERSION_NAME ?: "unknown"
    return try {
        val packageInfo = context?.packageManager?.getPackageInfo(context.packageName, 0)
        val appVersion = packageInfo?.versionName ?: "unknown"
        "$USER_AGENT_PREFIX/$sdkVersion (app/$appVersion)"
    } catch (e: Exception) {
        "$USER_AGENT_PREFIX/$sdkVersion"
    }
}
// Result: flagsmith-kotlin-android-sdk/1.8.0 (app/2.4.1)

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Hi @polatolu I think this needs to be looked at. The previous implementation got the package version in various ways depending on Cocoapods or SPM, and if we don't find those I'd default to 'unknown'

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

I agree, I'll address this now.

val version = packageInfo?.versionName ?: "unknown"
"$USER_AGENT_PREFIX/$version"
} catch (e: Exception) {
"$USER_AGENT_PREFIX/unknown"
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Overly broad exception handling

Catching all Exception types is too broad and might hide unexpected runtime exceptions that could indicate bugs.

Recommendation: Catch specific exceptions:

} catch (e: PackageManager.NameNotFoundException) {
    "$USER_AGENT_PREFIX/unknown"
} catch (e: Exception) {
    // Log unexpected exceptions for debugging
    Log.w("FlagsmithRetrofitService", "Unexpected error getting version", e)
    "$USER_AGENT_PREFIX/unknown"
}

This helps identify unexpected errors during development while still providing graceful fallback.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Honestly I think this is fine in an SDK

.withHeader("User-Agent", "flagsmith-kotlin-android-sdk/$expectedAppVersion")
)
}
} No newline at end of file
Copy link
Copy Markdown
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
Copy Markdown
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

Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 0

♻️ Duplicate comments (1)
FlagsmithClient/src/main/java/com/flagsmith/internal/FlagsmithRetrofitService.kt (1)

62-70: Consider caching the User-Agent string as previously suggested.

The User-Agent string is recomputed on every request, even though it never changes during the app's lifecycle. A previous reviewer suggested computing it once when the interceptor is created, which remains a valid optimization.

Apply this diff to cache the User-Agent string:

 fun userAgentInterceptor(context: Context?): Interceptor {
+    val userAgent = getUserAgent(context)
     return Interceptor { chain ->
-        val userAgent = getUserAgent(context)
         val request = chain.request().newBuilder()
             .addHeader(USER_AGENT_HEADER, userAgent)
             .build()
         chain.proceed(request)
     }
 }
🧹 Nitpick comments (1)
FlagsmithClient/src/main/java/com/flagsmith/internal/FlagsmithRetrofitService.kt (1)

45-48: Consider removing unused Context parameter.

The context parameter is passed but never used in this function. Since you've correctly resolved the semantic confusion from previous reviews (this now gets SDK version only, not app version), the Context is no longer needed here.

Apply this diff to remove the unused parameter:

-private fun getUserAgent(context: Context?): String {
+private fun getUserAgent(): String {
     val sdkVersion = getSdkVersion()
     return "$USER_AGENT_PREFIX/$sdkVersion"
 }

 fun userAgentInterceptor(context: Context?): Interceptor {
     return Interceptor { chain ->
-        val userAgent = getUserAgent(context)
+        val userAgent = getUserAgent()
         val request = chain.request().newBuilder()
             .addHeader(USER_AGENT_HEADER, userAgent)
             .build()
         chain.proceed(request)
     }
 }
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between b184964 and e7b671f.

📒 Files selected for processing (2)
  • FlagsmithClient/src/main/java/com/flagsmith/internal/FlagsmithRetrofitService.kt (2 hunks)
  • FlagsmithClient/src/test/java/com/flagsmith/UserAgentTests.kt (1 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
  • FlagsmithClient/src/test/java/com/flagsmith/UserAgentTests.kt
🧰 Additional context used
🪛 detekt (1.23.8)
FlagsmithClient/src/main/java/com/flagsmith/internal/FlagsmithRetrofitService.kt

[warning] 56-56: The caught exception is swallowed. The original exception could be lost.

(detekt.exceptions.SwallowedException)

🔇 Additional comments (3)
FlagsmithClient/src/main/java/com/flagsmith/internal/FlagsmithRetrofitService.kt (3)

42-43: LGTM!

The constants are correctly defined for User-Agent header injection.


124-124: LGTM!

The User-Agent interceptor is correctly wired into the OkHttpClient builder.


50-60: No issues found—the BuildConfig class name is correct.

The build.gradle.kts file defines namespace = "com.flagsmith.kotlin", which is where Gradle generates the BuildConfig class. The reflection call in getSdkVersion() correctly targets com.flagsmith.kotlin.BuildConfig. The kover configuration also confirms this: "${android.namespace}.BuildConfig". In tests, BuildConfig is unavailable, so the fallback to "unknown" is expected and correct.

Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🧹 Nitpick comments (3)
FlagsmithClient/src/main/java/com/flagsmith/internal/FlagsmithRetrofitService.kt (1)

45-48: Unused Context parameter in getUserAgent can be removed for clarity

getUserAgent(context: Context?) ignores its context argument and only delegates to getSdkVersion(). Since the current implementation does not need Android Context, keeping the parameter is slightly misleading.

Consider simplifying to:

-        private fun getUserAgent(context: Context?): String {
-            val sdkVersion = getSdkVersion()
-            return "$USER_AGENT_PREFIX/$sdkVersion"
-        }
+        private fun getUserAgent(): String {
+            val sdkVersion = getSdkVersion()
+            return "$USER_AGENT_PREFIX/$sdkVersion"
+        }

and updating userAgentInterceptor accordingly. This keeps the API surface tighter and avoids suggesting that app-level context influences the header.

FlagsmithClient/src/test/java/com/flagsmith/internal/SdkVersionRetrievalTest.kt (1)

23-32: Good coverage of fallback/version behavior; consider making the fallback test explicit

These tests nicely validate User-Agent format, fallback version, and consistency using MockWebServer, and tying EXPECTED_FALLBACK_VERSION to release-please avoids drift.

One minor improvement: testFallbackVersionIsUsedWhenBuildConfigNotAvailable currently assumes that BuildConfig is not present in the unit-test environment. If that assumption ever changes (e.g., different Gradle setup), the test semantics could flip. Longer term, it might be more robust to explicitly simulate the “no BuildConfig / invalid VERSION_NAME” path (e.g., by refactoring getSdkVersion to accept an injectable lookup helper) rather than relying on classpath behavior.

Also applies to: 73-100

FlagsmithClient/src/test/java/com/flagsmith/UserAgentTests.kt (1)

39-189: User-Agent integration tests are solid; minor duplication could be reduced

These tests give good end-to-end coverage that the expected User-Agent is sent for flags, identities, and trait writes, and that the format matches flagsmith-kotlin-android-sdk/{version} with the version kept in sync via release-please.

testUserAgentHeaderSentWithGetFlags and testUserAgentHeaderSentWithNullContext currently exercise the same scenario (null context with GET /flags). If you want to trim repetition, you could fold the “null context” assertion into a single test or parameterize the setup, but it’s fine to keep as-is if the explicit naming helps readability.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between e7b671f and 6416cb7.

📒 Files selected for processing (5)
  • FlagsmithClient/build.gradle.kts (1 hunks)
  • FlagsmithClient/src/main/java/com/flagsmith/internal/FlagsmithRetrofitService.kt (2 hunks)
  • FlagsmithClient/src/test/java/com/flagsmith/UserAgentTests.kt (1 hunks)
  • FlagsmithClient/src/test/java/com/flagsmith/internal/SdkVersionRetrievalTest.kt (1 hunks)
  • release-please-config.json (1 hunks)
🔇 Additional comments (2)
FlagsmithClient/build.gradle.kts (1)

85-87: MockWebServer test dependency is correctly scoped and version-aligned

Using mockwebserver:4.11.0 as a testImplementation alongside okhttp-sse:4.11.0 is consistent and keeps runtime deps unchanged. No issues here.

release-please-config.json (1)

11-16: extra-files wiring keeps User-Agent version single-sourced

Including the Retrofit service and the two version-sensitive test files in extra-files ensures release automation keeps the SDK/User-Agent version consistent across code and tests. Good maintainability improvement.

Comment on lines +75 to +83
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)
}
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Override existing User-Agent instead of adding a second header

userAgentInterceptor currently uses .addHeader(USER_AGENT_HEADER, userAgent), which will append an additional User-Agent header on top of OkHttp’s default rather than replacing it. Many servers and log/analytics stacks expect a single User-Agent value, so this can lead to ambiguity about which one is honored.

Prefer overriding the header:

-        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 userAgentInterceptor(context: Context?): Interceptor {
+            val userAgent = getUserAgent(context)
+            return Interceptor { chain ->
+                val request = chain.request().newBuilder()
+                    .header(USER_AGENT_HEADER, userAgent)
+                    .build()
+                chain.proceed(request)
+            }
+        }

This still guarantees your custom User-Agent is sent, but without duplicating the header.

Also applies to: 137-138

🤖 Prompt for AI Agents
In
FlagsmithClient/src/main/java/com/flagsmith/internal/FlagsmithRetrofitService.kt
around lines 75-83 (and similarly at 137-138), the interceptor currently uses
addHeader(USER_AGENT_HEADER, userAgent) which appends a second User-Agent
header; replace addHeader with header(USER_AGENT_HEADER, userAgent) (or use
removeHeader then add) so the existing User-Agent is overridden rather than
duplicated, ensuring only a single User-Agent is sent.

Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 2

♻️ Duplicate comments (2)
FlagsmithClient/src/main/java/com/flagsmith/internal/FlagsmithRetrofitService.kt (2)

56-64: Performance: User-Agent computed on every request.

The getUserAgent(context) call on line 58 executes for every HTTP request, even though the User-Agent string never changes during the app's lifecycle. This is unnecessary overhead.

Compute the User-Agent once when the interceptor is created:

         fun userAgentInterceptor(context: Context?): Interceptor {
+            val userAgent = getUserAgent(context)  // Computed ONCE
             return Interceptor { chain ->
-                val userAgent = getUserAgent(context)
                 val request = chain.request().newBuilder()
                     .addHeader(USER_AGENT_HEADER, userAgent)
                     .build()
                 chain.proceed(request)
             }
         }

This optimization eliminates redundant calls and improves performance.


60-60: Replace addHeader with header to avoid duplicate User-Agent headers.

Using .addHeader(USER_AGENT_HEADER, userAgent) will append an additional User-Agent header on top of OkHttp's default, potentially causing multiple User-Agent headers to be sent. Many servers and analytics tools expect a single User-Agent value.

Use .header() to replace the existing header:

                 val request = chain.request().newBuilder()
-                    .addHeader(USER_AGENT_HEADER, userAgent)
+                    .header(USER_AGENT_HEADER, userAgent)
                     .build()

This ensures only one User-Agent header is sent with each request.

🧹 Nitpick comments (2)
FlagsmithClient/src/test/java/com/flagsmith/internal/SdkVersionRetrievalTest.kt (2)

15-30: Class docs and version constant are clear; consider centralizing test version configuration

The doc comment and companion object make the intent around EXPECTED_SDK_VERSION and the USER_AGENT_PREFIX very explicit. Given that other tests in the suite also assert against "flagsmith-kotlin-android-sdk/$EXPECTED_SDK_VERSION", you might want to centralize these into a shared test constant (e.g., a small TestConstants object) to avoid divergence between files when release-please updates the version or if the prefix ever changes. This is purely optional but can reduce future maintenance.


43-69: User-Agent prefix assertion looks good; consider using a timed takeRequest to avoid potential hangs

The structure of this test is solid and the assertion on the User-Agent prefix is appropriate. The only robustness tweak I’d suggest is using takeRequest(timeout, unit) rather than the blocking takeRequest() so that, if for some reason the request is never sent, the test fails with a clear assertion instead of hanging until the test runner times out. For example:

val recordedRequest = mockServer.takeRequest(5, java.util.concurrent.TimeUnit.SECONDS)
assertNotNull("Expected a request to be recorded", recordedRequest)
val userAgent = recordedRequest!!.getHeader("User-Agent")

This makes failures easier to diagnose in CI without changing the core behavior of the test.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 6416cb7 and 2d6527f.

📒 Files selected for processing (3)
  • FlagsmithClient/src/main/java/com/flagsmith/internal/FlagsmithRetrofitService.kt (2 hunks)
  • FlagsmithClient/src/test/java/com/flagsmith/UserAgentTests.kt (1 hunks)
  • FlagsmithClient/src/test/java/com/flagsmith/internal/SdkVersionRetrievalTest.kt (1 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
  • FlagsmithClient/src/test/java/com/flagsmith/UserAgentTests.kt
🔇 Additional comments (4)
FlagsmithClient/src/main/java/com/flagsmith/internal/FlagsmithRetrofitService.kt (3)

42-43: Constants are well-defined.

The User-Agent header name and prefix are correctly defined and follow standard HTTP conventions.


50-54: SDK version approach is appropriate.

Using a hardcoded version string with release-please markers is a standard pattern for SDK versioning that allows automated version management during releases.


118-118: Interceptor correctly wired into OkHttpClient.

The User-Agent interceptor is properly added to the OkHttpClient builder chain.

FlagsmithClient/src/test/java/com/flagsmith/internal/SdkVersionRetrievalTest.kt (1)

104-140: Good coverage of persistence and exact value of User-Agent

This test does a nice job of verifying both that the User-Agent header is stable across multiple requests and that it matches the exact expected value "${USER_AGENT_PREFIX}/${EXPECTED_SDK_VERSION}". That gives strong coupling between the interceptor behavior, getSdkVersion(), and the release-please–managed version. Just be aware that any intentional changes to the prefix or version derivation will require updating this assertion, which is an acceptable tradeoff for the safety it provides.

Comment thread FlagsmithClient/src/main/java/com/flagsmith/internal/FlagsmithRetrofitService.kt Outdated
Comment on lines +71 to +102
@Test
fun testVersionFormatIsValid() {
// Given - Create a client with the user agent interceptor
val interceptor = FlagsmithRetrofitService.userAgentInterceptor(null)
val client = OkHttpClient.Builder()
.addInterceptor(interceptor)
.build()

mockServer.enqueue(MockResponse().setResponseCode(200).setBody("{}"))

// When - Make a request
val request = Request.Builder()
.url(mockServer.url("/"))
.build()

client.newCall(request).execute().use { response ->
// Then - Verify version format is semantic versioning compatible
val recordedRequest = mockServer.takeRequest()
val userAgent = recordedRequest.getHeader("User-Agent")!!
val version = userAgent.substringAfter("$USER_AGENT_PREFIX/")

assertTrue("Version should not be empty", version.isNotEmpty())
assertTrue("Version should not contain whitespace", version.trim() == version)

// Version should match semantic versioning pattern (X.Y.Z) or be a valid identifier
val semverPattern = Regex("^\\d+\\.\\d+\\.\\d+.*$")
assertTrue(
"Version should follow semantic versioning or be a valid identifier: $version",
semverPattern.matches(version) || version.matches(Regex("^[a-zA-Z0-9._-]+$"))
)
}
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

Whitespace assertion doesn’t fully match the message; tighten the check

The intent of

assertTrue("Version should not contain whitespace", version.trim() == version)

is that the version “should not contain whitespace”, but this only rejects leading/trailing whitespace. Internal whitespace (e.g. "1.8.0 beta") would still pass.

You can align the assertion with the message by rejecting any whitespace character:

-            assertTrue("Version should not contain whitespace", version.trim() == version)
+            assertTrue(
+                "Version should not contain whitespace",
+                !version.any { it.isWhitespace() }
+            )

The rest of the format checks (semantic-version‑compatible or a simple identifier like "unknown") look reasonable.


I'll verify this review comment by inspecting the actual test file and confirming the whitespace assertion logic.


---

🏁 Script executed:

```shell
#!/bin/bash
# Find and inspect the test file
fd -t f "SdkVersionRetrievalTest.kt" --exec cat {} \;

Length of output: 5074


🏁 Script executed:

#!/bin/bash
# Also search for similar whitespace patterns in the codebase
rg -n "trim()" --type kotlin -C 3 | head -50

Length of output: 3772


🏁 Script executed:

#!/bin/bash
# Check for isWhitespace usage in Kotlin files
rg -n "isWhitespace()" --type kotlin

Length of output: 0


🏁 Script executed:

#!/bin/bash
# Verify Kotlin String has 'any' method with isWhitespace
rg -n "\.any\s*{" --type kotlin -C 2 | head -30

Length of output: 81


I need to verify that the suggested Kotlin syntax is valid and standard. Let me check the Kotlin documentation.


---

🌐 Web query:

```web
Kotlin String any method isWhitespace character

💡 Result:

Use Char.isWhitespace() inside String.any { ... }.

Example:
val s = "hello world"
val hasWs = s.any { it.isWhitespace() } // true

Notes:

  • Char.isWhitespace() checks Unicode whitespace categories. [1]
  • CharSequence.any(predicate) returns true if any character matches the predicate. [2]
  • If you want to test whether the whole string is empty or only whitespace, use isBlank(). [3]

Sources:
[1] Kotlin Char.isWhitespace docs.
[2] Kotlin CharSequence.any docs.
[3] Kotlin CharSequence.isBlank docs.


Whitespace assertion doesn't fully match the message; tighten the check

The assertion version.trim() == version only rejects leading/trailing whitespace, but the message states "should not contain whitespace"—implying any whitespace. Internal whitespace like "1.8.0 beta" would incorrectly pass.

Align the assertion with the message by rejecting any whitespace character:

-            assertTrue("Version should not contain whitespace", version.trim() == version)
+            assertTrue(
+                "Version should not contain whitespace",
+                !version.any { it.isWhitespace() }
+            )

The rest of the format checks (semantic versioning or simple identifiers) are reasonable.

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
@Test
fun testVersionFormatIsValid() {
// Given - Create a client with the user agent interceptor
val interceptor = FlagsmithRetrofitService.userAgentInterceptor(null)
val client = OkHttpClient.Builder()
.addInterceptor(interceptor)
.build()
mockServer.enqueue(MockResponse().setResponseCode(200).setBody("{}"))
// When - Make a request
val request = Request.Builder()
.url(mockServer.url("/"))
.build()
client.newCall(request).execute().use { response ->
// Then - Verify version format is semantic versioning compatible
val recordedRequest = mockServer.takeRequest()
val userAgent = recordedRequest.getHeader("User-Agent")!!
val version = userAgent.substringAfter("$USER_AGENT_PREFIX/")
assertTrue("Version should not be empty", version.isNotEmpty())
assertTrue("Version should not contain whitespace", version.trim() == version)
// Version should match semantic versioning pattern (X.Y.Z) or be a valid identifier
val semverPattern = Regex("^\\d+\\.\\d+\\.\\d+.*$")
assertTrue(
"Version should follow semantic versioning or be a valid identifier: $version",
semverPattern.matches(version) || version.matches(Regex("^[a-zA-Z0-9._-]+$"))
)
}
}
@Test
fun testVersionFormatIsValid() {
// Given - Create a client with the user agent interceptor
val interceptor = FlagsmithRetrofitService.userAgentInterceptor(null)
val client = OkHttpClient.Builder()
.addInterceptor(interceptor)
.build()
mockServer.enqueue(MockResponse().setResponseCode(200).setBody("{}"))
// When - Make a request
val request = Request.Builder()
.url(mockServer.url("/"))
.build()
client.newCall(request).execute().use { response ->
// Then - Verify version format is semantic versioning compatible
val recordedRequest = mockServer.takeRequest()
val userAgent = recordedRequest.getHeader("User-Agent")!!
val version = userAgent.substringAfter("$USER_AGENT_PREFIX/")
assertTrue("Version should not be empty", version.isNotEmpty())
assertTrue(
"Version should not contain whitespace",
!version.any { it.isWhitespace() }
)
// Version should match semantic versioning pattern (X.Y.Z) or be a valid identifier
val semverPattern = Regex("^\\d+\\.\\d+\\.\\d+.*$")
assertTrue(
"Version should follow semantic versioning or be a valid identifier: $version",
semverPattern.matches(version) || version.matches(Regex("^[a-zA-Z0-9._-]+$"))
)
}
}
🤖 Prompt for AI Agents
In
FlagsmithClient/src/test/java/com/flagsmith/internal/SdkVersionRetrievalTest.kt
around lines 71 to 102, the whitespace assertion currently only checks
leading/trailing spaces but the message says "should not contain whitespace";
update the assertion to reject any whitespace character within the version
(e.g., assertTrue("Version should not contain whitespace", !version.any {
it.isWhitespace() } ) or equivalent regex check), and ensure the assertion
message remains accurate.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants