Skip to content

feat: integrate eppo module with android-sdk-framework (v3 → v4 migration)#252

Open
typotter wants to merge 13 commits intotypo/v4/add-framework-modulefrom
typo/v4/integrate-eppo-with-framework
Open

feat: integrate eppo module with android-sdk-framework (v3 → v4 migration)#252
typotter wants to merge 13 commits intotypo/v4/add-framework-modulefrom
typo/v4/integrate-eppo-with-framework

Conversation

@typotter
Copy link
Copy Markdown
Collaborator

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)

  • Extends AndroidBaseClient from framework instead of v3 BaseEppoClient
  • Removes v3 dependencies
  • Adds framework dependency
  • Removes old storage classes: ConfigurationStore, ConfigCacheFile, BaseCacheFile (now in framework)

New v4 Components

  • OkHttpEppoClient: HTTP client implementation with slf4j logging
  • JacksonConfigurationParser: JSON parsing for v4 FlagConfigResponse format
  • Jackson adapters: Custom serializers/deserializers for EppoValue, FlagConfigResponse, dates

Example App

  • CustomClientActivity: Demonstrates custom HTTP client and parser injection
  • GsonConfigurationParser: Example alternative to Jackson parser
  • HeaderInjectingEppoClient: Example custom HTTP headers

Test Results

  • 46/46 instrumentation tests passing locally
  • ✅ Framework module builds independently
  • ✅ Eppo module builds with v4 dependencies
  • ✅ No breaking API changes for SDK users

Migration Notes

typotter added 12 commits March 24, 2026 09:38
…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.
…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.
@typotter
Copy link
Copy Markdown
Collaborator Author

@copilot review

Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

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 EppoClient to extend AndroidBaseClient (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.

Comment on lines 19 to +20
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
Copy link

Copilot AI Mar 27, 2026

Choose a reason for hiding this comment

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

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

Copilot uses AI. Check for mistakes.
Comment on lines +41 to +44
ConfigCacheFile cacheFile =
new ConfigCacheFile(
getApplication(), cacheFileNameSuffix, "application/x-java-serialized-object");
if (cacheFile.exists()) {
Copy link

Copilot AI Mar 27, 2026

Choose a reason for hiding this comment

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

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.

Copilot uses AI. Check for mistakes.
Comment on lines +90 to +91
// AssignmentDetails<Boolean> details = EppoClient.getInstance()
// .getBooleanAssignmentDetails(experimentKey, subjectId, new Attributes(), false);
Copy link

Copilot AI Mar 27, 2026

Choose a reason for hiding this comment

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

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.

Suggested change
// AssignmentDetails<Boolean> details = EppoClient.getInstance()
// .getBooleanAssignmentDetails(experimentKey, subjectId, new Attributes(), false);

Copilot uses AI. Check for mistakes.
Comment on lines +4 to +5
public static final String INITIAL_SUBJECT_ID = "jsmith123";
public static final String INITIAL_FLAG_KEY = "ai-metric-explores";
Copy link

Copilot AI Mar 27, 2026

Choose a reason for hiding this comment

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

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.

Suggested change
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";

Copilot uses AI. Check for mistakes.
Comment on lines 366 to 370
if (instance.getInitialConfigFuture() != null) {
instance
.getInitialConfigFuture()
.handle(
(success, ex) -> {
Copy link

Copilot AI Mar 27, 2026

Choose a reason for hiding this comment

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

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

Copilot uses AI. Check for mistakes.
Comment on lines +101 to +104
private Request buildRequest(EppoConfigurationRequest request) {
HttpUrl.Builder urlBuilder =
HttpUrl.parse(request.getBaseUrl() + request.getResourcePath()).newBuilder();

Copy link

Copilot AI Mar 27, 2026

Choose a reason for hiding this comment

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

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.

Copilot uses AI. Check for mistakes.
- 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
Copy link

Copilot AI Mar 27, 2026

Choose a reason for hiding this comment

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

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.

Suggested change
run: ./gradlew android-sdk-framework:assemble android-sdk-framework:publish eppo:assemble eppo:publish -Psnapshot
run: ./gradlew android-sdk-framework:assemble eppo:assemble -Psnapshot

Copilot uses AI. Check for mistakes.
Comment on lines +58 to +60
// if (!TextUtils.isEmpty(INITIAL_FLAG_KEY)) {
// EppoClient.getInstance().getStringAssignment(INITIAL_FLAG_KEY, INITIAL_SUBJECT_ID, "");
// }
Copy link

Copilot AI Mar 27, 2026

Choose a reason for hiding this comment

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

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.

Copilot uses AI. Check for mistakes.
Comment on lines +96 to +100
@Nullable CachingConfigurationStore configurationStoreOverride,
String apiKey,
boolean offlineMode,
IAssignmentCache assignmentCache,
boolean ignoreConfigCacheFile) {
boolean ignoreTestConfigCacheFile) {
Copy link

Copilot AI Mar 27, 2026

Choose a reason for hiding this comment

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

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.

Copilot uses AI. Check for mistakes.
Comment on lines +465 to +466
InputStream configStream = assets.open(filepath);
byte[] configBytes = IOUtils.toByteArray(configStream);
Copy link

Copilot AI Mar 27, 2026

Choose a reason for hiding this comment

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

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.

Suggested change
InputStream configStream = assets.open(filepath);
byte[] configBytes = IOUtils.toByteArray(configStream);
byte[] configBytes;
try (InputStream configStream = assets.open(filepath)) {
configBytes = IOUtils.toByteArray(configStream);
}

Copilot uses AI. Check for mistakes.
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