feat: integrate eppo module with android-sdk-framework (v3 → v4 migration)#252
feat: integrate eppo module with android-sdk-framework (v3 → v4 migration)#252typotter wants to merge 13 commits intotypo/v4/add-framework-modulefrom
Conversation
…ation)
Migrate EppoClient from v3 SDK to v4 framework, resolving all test failures.
## Major Changes
### EppoClient Migration
- Extend AndroidBaseClient from framework instead of BaseEppoClient
- Remove old ConfigurationStore, ConfigCacheFile, BaseCacheFile (now in framework)
- Add dependency on android-sdk-framework module
- Remove v3 sdk-common-jvm dependency
### New Components
- OkHttpEppoClient: HTTP client implementation with logging
- JacksonConfigurationParser: JSON parsing for v4 format
- Jackson adapters for EppoValue, FlagConfigResponse, dates, etc.
### Test Infrastructure Fixes
- TestConfigCacheFile: Fix filename to match framework (eppo-sdk-flags-{suffix}.ser)
- Fix waitForPopulatedCache(): Remove incorrect .ser suffix (already in filename)
- Fix unclosed OutputStreams: Add try-with-resources (3 locations)
- Simplify testDifferentCacheFilesPerKey: Copy actual cache instead of custom JSON
- Add OutputStream import
### Example App Updates
- Add CustomClientActivity demonstrating custom HTTP client
- Add GsonConfigurationParser and HeaderInjectingEppoClient examples
## Test Results
- All 46 instrumentation tests passing locally
- Framework module builds independently
- Eppo module builds with v4 dependencies
- Fix TestConfigCacheFile.cacheFileName() to use correct format (eppo-sdk-flags-{suffix}.ser)
- Simplify testDifferentCacheFilesPerKey to copy actual cache instead of custom JSON
- Fix unclosed OutputStreams with try-with-resources (3 locations)
These fixes resolve all test failures and ensure 46/46 tests pass.
…I 26-32 compatibility
…or handling - Move GsonConfigurationCodec to example module (it's a demo/example codec, not part of the framework public API) - Update CustomClientActivity to use GsonConfigurationCodec + FileBackedConfigStore to demonstrate human-readable JSON cache - Fix initialization error handling in AndroidBaseClient and EppoClient: complete exceptionally when ex != null, avoiding double increment of failCount
- eppo: 4.12.1 -> 4.12.1-SNAPSHOT - android-sdk-framework: 0.1.0 -> 0.1.0-SNAPSHOT - Remove duplicate sdk-common-jvm implementation dependency (already declared as api) - Update publish-snapshot workflow to stage and publish android-sdk-framework before eppo, so the transitive dependency is resolvable by customers
- EppoValueSerializer: fix independent if-chain to if/else-if so only one value is written per serialization call; previously the else branch always wrote null after any non-array type - BanditParametersResponseDeserializer: use fields() instead of iterator() to get map key directly, consistent with FlagConfigResponseDeserializer - DateSerializer: fix Javadoc (said "gson", now says "Jackson") - OkHttpEppoClient: remove duplicate android.util.Log calls; use only slf4j throughout - EppoClientTest: add ignoreCachedConfiguration(true) to graceful init failure test to make behavior deterministic; restore testErrorGracefulModeOn and testErrorGracefulModeOff using the v4 configurationClient() API; fix waitForPopulatedCache to poll at 100ms intervals without a magic byte-size threshold - Delete android-batteries-included-notes.md (development scratch file) - Delete GsonConfigurationCodec: uses reflection on private fields of Configuration; R8 will rename those fields in release builds (minifyEnabled true, no keep rules), causing NoSuchFieldException at runtime - CustomClientActivity: replace GsonConfigurationCodec with default ConfigurationCodec.Default; update log messages accordingly
Update example/build.gradle to depend on cloud.eppo:android-sdk:5.0.0-SNAPSHOT instead of the local :eppo project, and update settings.gradle to only include the example module so the build exercises the published SDK path.
|
@copilot review |
There was a problem hiding this comment.
Pull request overview
Migrates the eppo Android SDK module from the v3 client implementation to the v4 android-sdk-framework, updating configuration storage/parsing + HTTP fetching, and adapting the example app, tests, and snapshot publishing to the new structure.
Changes:
- Refactors
EppoClientto extendAndroidBaseClient(framework) and switches to framework-backed configuration storage. - Adds v4-compatible HTTP + parsing components (
OkHttpEppoClient,JacksonConfigurationParser, Jackson adapters/modules). - Updates example app + instrumentation tests + snapshot publishing workflow for the new framework integration.
Reviewed changes
Copilot reviewed 32 out of 32 changed files in this pull request and generated 11 comments.
Show a summary per file
| File | Description |
|---|---|
settings.gradle |
Updates included Gradle modules. |
example/src/main/res/values/strings.xml |
Adds label for new example launch option. |
example/src/main/res/layout/activity_main.xml |
Adds new button to start the custom client example. |
example/src/main/java/cloud/eppo/androidexample/SecondActivity.java |
Tweaks assignment default + logging in example flow. |
example/src/main/java/cloud/eppo/androidexample/MainActivity.java |
Adds custom-client launch button and updates cache clearing to framework cache file. |
example/src/main/java/cloud/eppo/androidexample/HeaderInjectingEppoClient.java |
New example EppoConfigurationClient wrapper that injects headers via OkHttp interceptor. |
example/src/main/java/cloud/eppo/androidexample/GsonConfigurationParser.java |
New example GSON-based parser implementation (reference). |
example/src/main/java/cloud/eppo/androidexample/CustomClientActivity.java |
New example activity demonstrating custom HTTP client + parser injection into AndroidBaseClient. |
example/src/main/java/cloud/eppo/androidexample/Constants.java |
Updates example defaults for subject/flag. |
example/src/main/AndroidManifest.xml |
Registers CustomClientActivity. |
example/build.gradle |
Switches example to consume published SDK snapshot + adds OkHttp dependency. |
eppo/src/main/java/cloud/eppo/android/dto/adapters/FlagConfigResponseDeserializer.java |
New Jackson deserializer for v4 FlagConfigResponse. |
eppo/src/main/java/cloud/eppo/android/dto/adapters/EppoValueSerializer.java |
New Jackson serializer for EppoValue. |
eppo/src/main/java/cloud/eppo/android/dto/adapters/EppoValueDeserializer.java |
New Jackson deserializer for EppoValue. |
eppo/src/main/java/cloud/eppo/android/dto/adapters/EppoModule.java |
New Jackson SimpleModule wiring custom serializers/deserializers. |
eppo/src/main/java/cloud/eppo/android/dto/adapters/DateSerializer.java |
New Date serializer (UTC ISO). |
eppo/src/main/java/cloud/eppo/android/dto/adapters/BanditParametersResponseDeserializer.java |
New Jackson deserializer for v4 bandit params response. |
eppo/src/main/java/cloud/eppo/android/PrecomputedCacheFile.java |
Switches cache base class import to framework BaseCacheFile. |
eppo/src/main/java/cloud/eppo/android/OkHttpEppoClient.java |
Adds default OkHttp implementation of EppoConfigurationClient with slf4j logging. |
eppo/src/main/java/cloud/eppo/android/JacksonConfigurationParser.java |
Adds default Jackson-based ConfigurationParser using the new EppoModule. |
eppo/src/main/java/cloud/eppo/android/EppoClient.java |
Migrates client to AndroidBaseClient + adds builder injection for parser/http client/store. |
eppo/src/main/java/cloud/eppo/android/ConfigurationStore.java |
Removes v3 storage implementation (moved to framework). |
eppo/src/main/java/cloud/eppo/android/ConfigCacheFile.java |
Removes v3 cache file implementation (moved to framework). |
eppo/src/main/java/cloud/eppo/android/BaseCacheFile.java |
Removes v3 cache base class (moved to framework). |
eppo/src/androidTest/java/cloud/eppo/android/helpers/AssignmentTestCaseDeserializer.java |
Updates imports/deserialization to v4 DTOs/adapters. |
eppo/src/androidTest/java/cloud/eppo/android/helpers/AssignmentTestCase.java |
Updates v4 DTO import usage. |
eppo/src/androidTest/java/cloud/eppo/android/TestConfigCacheFile.java |
Adds test helper cache file aligned to framework naming/extension. |
eppo/src/androidTest/java/cloud/eppo/android/EppoClientTest.java |
Refactors tests to inject v4 HTTP client + updated cache serialization + offline init parsing. |
eppo/build.gradle |
Bumps SDK version, switches to framework + v4 common dependency, adds Jackson module dep. |
android-sdk-framework/src/main/java/cloud/eppo/android/framework/AndroidBaseClient.java |
Tweaks initialization failure handling/logging behavior. |
android-sdk-framework/build.gradle |
Marks framework version as -SNAPSHOT. |
.github/workflows/publish-snapshot.yml |
Expands snapshot publishing to framework + new branch trigger. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| rootProject.name = "Eppo SDK" | ||
| include ':example' | ||
| include ':eppo' | ||
| include ':android-sdk-framework' No newline at end of file | ||
| include ':example' No newline at end of file |
There was a problem hiding this comment.
settings.gradle now only includes :example, but this repo still has :eppo and :android-sdk-framework modules and the build/workflows reference them. With these includes removed, Gradle will fail to resolve project(':android-sdk-framework') (and any tasks like eppo:publish). Re-add the missing include ':eppo' and include ':android-sdk-framework' entries (or otherwise update the build to match the intended module structure).
| ConfigCacheFile cacheFile = | ||
| new ConfigCacheFile( | ||
| getApplication(), cacheFileNameSuffix, "application/x-java-serialized-object"); | ||
| if (cacheFile.exists()) { |
There was a problem hiding this comment.
The cache file content-type is hard-coded as a string literal ("application/x-java-serialized-object"). Since the framework already exposes this via ConfigurationCodec.Default#getContentType(), using the codec (or a shared constant) would avoid drift if the framework changes its default content type/extension mapping.
| // AssignmentDetails<Boolean> details = EppoClient.getInstance() | ||
| // .getBooleanAssignmentDetails(experimentKey, subjectId, new Attributes(), false); |
There was a problem hiding this comment.
There is commented-out assignment/details code left in handleAssignment(). This makes the example harder to read and tends to bitrot; either remove it or convert it into a real, working example (e.g., behind a button / feature flag) so it stays exercised.
| // AssignmentDetails<Boolean> details = EppoClient.getInstance() | |
| // .getBooleanAssignmentDetails(experimentKey, subjectId, new Attributes(), false); |
| public static final String INITIAL_SUBJECT_ID = "jsmith123"; | ||
| public static final String INITIAL_FLAG_KEY = "ai-metric-explores"; |
There was a problem hiding this comment.
Constants now hard-codes a specific subject ID and flag key. This can make the example app behave unexpectedly for most users (and can accidentally hit real flags if the API key is set). Consider keeping these as empty/default placeholders or using clearly fake sample values (e.g., "example-flag") to avoid implying production defaults.
| public static final String INITIAL_SUBJECT_ID = "jsmith123"; | |
| public static final String INITIAL_FLAG_KEY = "ai-metric-explores"; | |
| public static final String INITIAL_SUBJECT_ID = "example-subject-id"; | |
| public static final String INITIAL_FLAG_KEY = "example-flag-key"; |
| if (instance.getInitialConfigFuture() != null) { | ||
| instance | ||
| .getInitialConfigFuture() | ||
| .handle( | ||
| (success, ex) -> { |
There was a problem hiding this comment.
If offlineMode is true and instance.getInitialConfigFuture() is null, ret is never completed (no fetch is started and the getInitialConfigFuture() handler won't run). This can hang buildAndInitAsync() forever. Add an offline-mode completion fallback when there is no initial config future (as done in AndroidBaseClient.Builder).
| private Request buildRequest(EppoConfigurationRequest request) { | ||
| HttpUrl.Builder urlBuilder = | ||
| HttpUrl.parse(request.getBaseUrl() + request.getResourcePath()).newBuilder(); | ||
|
|
There was a problem hiding this comment.
HttpUrl.parse(...) can return null if the constructed URL is invalid; calling .newBuilder() on it will then throw an NPE. It would be safer to validate the parsed URL and fail the returned future with a clear exception (including the baseUrl/resourcePath) when parsing fails.
| - name: Stage snapshot artifact | ||
| run: ./gradlew eppo:assemble eppo:publish -Psnapshot | ||
| - name: Stage snapshot artifacts | ||
| run: ./gradlew android-sdk-framework:assemble android-sdk-framework:publish eppo:assemble eppo:publish -Psnapshot |
There was a problem hiding this comment.
The workflow runs android-sdk-framework:publish / eppo:publish in the "Stage snapshot artifacts" step and then runs publishAllPublicationsToMavenCentralRepository for each module afterwards. Since publish already triggers PublishToMavenRepository tasks, this is likely redundant and can double-publish (or fail with "already published"). Consider making the staging step assemble-only (or publish to MavenLocal) and keep the actual Maven Central publishing in the dedicated steps.
| run: ./gradlew android-sdk-framework:assemble android-sdk-framework:publish eppo:assemble eppo:publish -Psnapshot | |
| run: ./gradlew android-sdk-framework:assemble eppo:assemble -Psnapshot |
| // if (!TextUtils.isEmpty(INITIAL_FLAG_KEY)) { | ||
| // EppoClient.getInstance().getStringAssignment(INITIAL_FLAG_KEY, INITIAL_SUBJECT_ID, ""); | ||
| // } |
There was a problem hiding this comment.
The onResume() assignment test block is now commented out. If this behavior is no longer desired, prefer removing the dead code entirely; otherwise, consider guarding it behind a debug flag so the intent stays clear and it doesn't accumulate commented-out logic.
| @Nullable CachingConfigurationStore configurationStoreOverride, | ||
| String apiKey, | ||
| boolean offlineMode, | ||
| IAssignmentCache assignmentCache, | ||
| boolean ignoreConfigCacheFile) { | ||
| boolean ignoreTestConfigCacheFile) { |
There was a problem hiding this comment.
The ignoreTestConfigCacheFile parameter on initClient(...) is currently unused. This makes the helper signature misleading and risks tests diverging from intended behavior. Either remove the parameter or wire it up (e.g., to builder.ignoreCachedConfiguration(...) / cache handling) so callers can control the cache behavior.
| InputStream configStream = assets.open(filepath); | ||
| byte[] configBytes = IOUtils.toByteArray(configStream); |
There was a problem hiding this comment.
configStream is opened but never closed. Wrap this in try-with-resources (or explicitly close the stream) to avoid leaking file descriptors during the instrumentation test run.
| InputStream configStream = assets.open(filepath); | |
| byte[] configBytes = IOUtils.toByteArray(configStream); | |
| byte[] configBytes; | |
| try (InputStream configStream = assets.open(filepath)) { | |
| configBytes = IOUtils.toByteArray(configStream); | |
| } |
Summary
Migrates the eppo module from v3 SDK to v4 framework, completing the integration started in #251.
Dependencies
Depends on: #251 (must be merged first)
Major Changes
EppoClient Migration (v3 → v4)
New v4 Components
Example App
Test Results
Migration Notes
.serextension (Java serialization)