Skip to content

Conversation

@polatolu
Copy link
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

    • Adds a dynamic User-Agent header to network requests that reflects the SDK/app version when available.
    • Real-time updates initialization now accepts Android context for more reliable behavior on devices.
  • Tests

    • New unit tests validating User-Agent composition and that the header is sent across various request types and failure scenarios.

@coderabbitai
Copy link

coderabbitai bot commented Oct 13, 2025

Walkthrough

Passes optional Android Context into SSE and HTTP clients to enable dynamic User-Agent injection: FlagsmithEventService constructor now accepts Context; Flagsmith passes Context when realtime updates enabled; Retrofit gains a user-agent interceptor and tests validate header behavior.

Changes

Cohort / File(s) Summary
Client realtime wiring
FlagsmithClient/src/main/java/com/flagsmith/Flagsmith.kt
When enableRealtimeUpdates is true, constructs FlagsmithEventService with the context argument; no other control-flow changes.
Event service construction
FlagsmithClient/src/main/java/com/flagsmith/internal/FlagsmithEventService.kt
Constructor signature updated to accept Context? and stores it; OkHttp builder for SSE connections now adds userAgentInterceptor(context).
Retrofit user-agent interceptor
FlagsmithClient/src/main/java/com/flagsmith/internal/FlagsmithRetrofitService.kt
Adds USER_AGENT_* helpers, getUserAgent(context) and getSdkVersion() helpers, and public userAgentInterceptor(context): Interceptor; wires interceptor into Retrofit/OkHttp builder to inject User-Agent header.
Unit tests for User-Agent
FlagsmithClient/src/test/java/com/flagsmith/UserAgentTests.kt
New test class with multiple tests using MockServer to assert User-Agent header composition and presence across endpoints and scenarios (null context, missing version, exceptions).

Sequence Diagram(s)

sequenceDiagram
    autonumber
    actor App
    participant Flagsmith
    participant EventSvc as FlagsmithEventService
    participant Retrofit as FlagsmithRetrofitService
    participant OkHttp as OkHttpClient
    participant UA as userAgentInterceptor
    participant SSE as SSE Server
    participant API as Flagsmith API

    App->>Flagsmith: init(..., context, enableRealtimeUpdates=true)
    Flagsmith->>EventSvc: new FlagsmithEventService(..., context, updates)
    Flagsmith->>Retrofit: build client (context)

    EventSvc->>OkHttp: addInterceptor(userAgentInterceptor(context))
    Retrofit->>OkHttp: addInterceptor(userAgentInterceptor(context))
    OkHttp->>UA: Intercept(request)
    UA->>UA: compute User-Agent (from Context / BuildConfig or "unknown")
    UA->>OkHttp: forward request with User-Agent header

    EventSvc->>SSE: open EventSource (with headers)
    SSE-->>EventSvc: send events
    EventSvc-->>Flagsmith: updates(Result<FlagEvent>)

    OkHttp->>API: HTTP requests include User-Agent
    API-->>OkHttp: responses
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Poem

I nibble bytes and polish paws,
I tuck a User-Agent in my cause.
With Context snug and headers bright,
I hop through SSEs in the night.
Carrot-coded cheers take flight. 🥕

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 title "feat: Send a standard user-agent header" directly and accurately describes the primary change across all modified files in the pull request. The changeset implements User-Agent header functionality through modifications to the Flagsmith client, event service, and Retrofit service, along with corresponding tests to verify the feature works correctly. The title is concise, specific, and clearly communicates the main objective without vague terminology, making it easy for teammates reviewing history to understand the purpose of the change.
✨ 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

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

@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.

@gazreese gazreese changed the title implementation and testing feat: Send a standard user-agent header Oct 15, 2025
Copy link

@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
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
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
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
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
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
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
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

Copy link

@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.

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.

3 participants