From 79319b9d18b5c79219ec090e8b061770224912ab Mon Sep 17 00:00:00 2001 From: Todd Anderson <127344469+tanderson-ld@users.noreply.github.com> Date: Thu, 4 Jun 2026 09:44:38 -0400 Subject: [PATCH 1/4] chore: advertise fdv2 capability in .sdk_metadata.json (#368) ## Summary - Adds `"fdv2": { "introduced": "5.13" }` to `.sdk_metadata.json` so downstream sdk-meta tooling lists the Android Client SDK as FDv2-capable. - Inserted alphabetically between `experimentation` and `flagChanges`. Mirrors the cross-SDK convention used by Python (#375 / metadata in 9.13), .NET (#221 / metadata in 8.11), Java (#120 / metadata in 7.11), Go (#342), Node (#1066). Tracking: [SDK-2443](https://launchdarkly.atlassian.net/browse/SDK-2443). ## Test plan - [x] JSON file parses (validated by the diff being a clean one-line add). - [ ] CI passes on this branch. ## Note on the version pin The `introduced` version assumes the EAP ships as `5.13`. If the umbrella branch advances past that minor before EAP cut, update this value to match the actual release version before merging `sdk-2429-android-fdv2-eap`. [SDK-2443]: https://launchdarkly.atlassian.net/browse/SDK-2443?atlOrigin=eyJpIjoiNWRkNTljNzYxNjVmNDY3MDlhMDU5Y2ZhYzA5YTRkZjUiLCJwIjoiZ2l0aHViLWNvbS1KU1cifQ --- > [!NOTE] > **Low Risk** > Metadata-only JSON change with no effect on SDK runtime behavior. > > **Overview** > Registers **FDv2** as a supported Android Client SDK feature in `.sdk_metadata.json`, with `"introduced": "5.13"`, so sdk-meta and related tooling can treat this SDK as FDv2-capable (aligned with other LaunchDarkly client SDKs). > > The new entry is placed alphabetically in the `features` map between `experimentation` and `flagChanges`. No runtime or SDK code changes in this PR. > > Reviewed by [Cursor Bugbot](https://cursor.com/bugbot) for commit e1dd7d1b7a07bcc76ed54da840e80af75a5b8439. Bugbot is set up for automated code reviews on this repo. Configure [here](https://www.cursor.com/dashboard/bugbot). --- .sdk_metadata.json | 1 + 1 file changed, 1 insertion(+) diff --git a/.sdk_metadata.json b/.sdk_metadata.json index 0c7130f8..1a43c3ba 100644 --- a/.sdk_metadata.json +++ b/.sdk_metadata.json @@ -15,6 +15,7 @@ "bigSegments": { "introduced": "1.0" }, "contexts": { "introduced": "4.0" }, "experimentation": { "introduced": "2.9" }, + "fdv2": { "introduced": "5.13" }, "flagChanges": { "introduced": "1.0" }, "hooks": { "introduced": "5.8" }, "inlineContextCustomEvents": { "introduced": "5.7" }, From 3736a33601255aa1af62167d9334125d3deb148f Mon Sep 17 00:00:00 2001 From: Todd Anderson <127344469+tanderson-ld@users.noreply.github.com> Date: Thu, 4 Jun 2026 09:44:53 -0400 Subject: [PATCH 2/4] chore: enable FDv2 contract tests via sdk-test-harness v3.1.0-alpha.6 (#367) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Summary - Uncomments the v3 contract-test invocation in the Makefile and pins it to sdk-test-harness `v3.1.0-alpha.6` — the first published alpha that includes [sdk-test-harness PR #325](https://github.com/launchdarkly/sdk-test-harness/pull/325) ("updates so FDv2 tests can run against client side sdks"). - Moves the `-stop-service-at-end` flag from the v2 invocation to the v3 invocation (v3 is now the final harness run). - Deletes the obsolete "Uncomment this..." instruction block. Targets the `sdk-2429-android-fdv2-eap` umbrella branch. Tracking: [SDK-2442](https://launchdarkly.atlassian.net/browse/SDK-2442). ## Test plan - [x] `make build-contract-tests` succeeds. - [x] `make start-contract-test-service` installs and starts the APK on the emulator. - [x] `make run-contract-tests` runs both harnesses cleanly: - v2 (FDv1): 798 total, 14 skipped, 784 ran — all passed. - v3 (FDv2, `v3.1.0-alpha.6`): 784 total, 21 skipped, 763 ran — all passed. - [x] Test service stops cleanly after v3 completes. - [ ] CI passes on this branch. [SDK-2442]: https://launchdarkly.atlassian.net/browse/SDK-2442?atlOrigin=eyJpIjoiNWRkNTljNzYxNjVmNDY3MDlhMDU5Y2ZhYzA5YTRkZjUiLCJwIjoiZ2l0aHViLWNvbS1KU1cifQ --- > [!NOTE] > **Low Risk** > Makefile-only CI/contract-test wiring with no production SDK or runtime behavior changes. > > **Overview** > **`make run-contract-tests` now runs both harnesses in sequence:** v2 (FDv1) unchanged except **`-stop-service-at-end` is removed**, then a new **v3** step pinned to **`sdk-test-harness/v3.1.0-alpha.6`** with **`SUPPRESSION_FILE_FDV2`**, **`TEST_HARNESS_PARAMS_V3`**, and **`-stop-service-at-end`** so the contract test service stops after the final run. > > The old commented-out v3 block and “uncomment when ready” note are deleted; the Makefile comment still documents that only the last harness should pass **`-stop-service-at-end`**. > > Reviewed by [Cursor Bugbot](https://cursor.com/bugbot) for commit 54f04b5441bdf4095c91abea831dec1b1f7620bf. Bugbot is set up for automated code reviews on this repo. Configure [here](https://www.cursor.com/dashboard/bugbot). --- Makefile | 18 +++++------------- 1 file changed, 5 insertions(+), 13 deletions(-) diff --git a/Makefile b/Makefile index a3ef4777..ed9965b7 100644 --- a/Makefile +++ b/Makefile @@ -18,19 +18,11 @@ run-contract-tests: @echo "Running SDK contract test v2..." @curl $${GITHUB_TOKEN:+ -H "Authorization: Token $${GITHUB_TOKEN}"} \ -s https://raw.githubusercontent.com/launchdarkly/sdk-test-harness/v2/downloader/run.sh \ - | VERSION=v2 PARAMS="-url http://localhost:8001 -host 10.0.2.2 -debug -stop-service-at-end -skip-from $(SUPPRESSION_FILE) $(TEST_HARNESS_PARAMS_V2)" sh - -# Uncomment this, update v3 version, and replace existing run-contract-tests once sdk-test-harness releases a version that includes FDv2 client contract tests. -# -# run-contract-tests: -# @echo "Running SDK contract test v2..." -# @curl $${GITHUB_TOKEN:+ -H "Authorization: Token $${GITHUB_TOKEN}"} \ -# -s https://raw.githubusercontent.com/launchdarkly/sdk-test-harness/v2/downloader/run.sh \ -# | VERSION=v2 PARAMS="-url http://localhost:8001 -host 10.0.2.2 -debug -skip-from $(SUPPRESSION_FILE) $(TEST_HARNESS_PARAMS_V2)" sh -# @echo "Running SDK contract test v3..." -# @curl $${GITHUB_TOKEN:+ -H "Authorization: Token $${GITHUB_TOKEN}"} \ -# -s https://raw.githubusercontent.com/launchdarkly/sdk-test-harness/v3.0.0-alpha.4/downloader/run.sh \ -# | VERSION=v3.0.0-alpha.4 PARAMS="-url http://localhost:8001 -host 10.0.2.2 -debug -stop-service-at-end -skip-from $(SUPPRESSION_FILE_FDV2) $(TEST_HARNESS_PARAMS_V3)" sh + | VERSION=v2 PARAMS="-url http://localhost:8001 -host 10.0.2.2 -debug -skip-from $(SUPPRESSION_FILE) $(TEST_HARNESS_PARAMS_V2)" sh + @echo "Running SDK contract test v3..." + @curl $${GITHUB_TOKEN:+ -H "Authorization: Token $${GITHUB_TOKEN}"} \ + -s https://raw.githubusercontent.com/launchdarkly/sdk-test-harness/v3.1.0-alpha.6/downloader/run.sh \ + | VERSION=v3.1.0-alpha.6 PARAMS="-url http://localhost:8001 -host 10.0.2.2 -debug -stop-service-at-end -skip-from $(SUPPRESSION_FILE_FDV2) $(TEST_HARNESS_PARAMS_V3)" sh contract-tests: build-contract-tests start-emulator start-contract-test-service run-contract-tests From 1bff68981edcd0a6fe3ba16f2d92ef60377483a6 Mon Sep 17 00:00:00 2001 From: Todd Anderson <127344469+tanderson-ld@users.noreply.github.com> Date: Thu, 4 Jun 2026 09:45:14 -0400 Subject: [PATCH 3/4] chore: re-publicize FDv2 data system entry points and lock entry bases (#361) - Make Components.dataSystem() and LDConfig.Builder.dataSystem() public again so customer code can opt in to FDv2 for the EAP. PR #351 had hidden these while the surface was being finalized. - Add package-private no-arg constructors to InitializerEntry and SynchronizerEntry so external code cannot subclass them; the SDK's concrete entry types in the same package continue to compile. - Remove InternalDataSystemAccess (a pure pass-through wrapper) and switch contract-tests SdkClientEntity to call the now-public Components.dataSystem() / LDConfig.Builder.dataSystem() directly. SDK-2438 --- > [!NOTE] > **Medium Risk** > Expands the public API for an unstable EAP data path (FDv2 vs legacy dataSource), but runtime wiring is unchanged aside from visibility and contract-test call sites. > > **Overview** > Re-opens the **early-access FDv2 data system** configuration surface for customer opt-in by making **`Components.dataSystem()`** and **`LDConfig.Builder.dataSystem()`** **public** again (reversing the package-private hiding from PR #351). > > **`InternalDataSystemAccess`** is **removed** as a pass-through; contract-tests now call the public **`Components.dataSystem()`** / **`builder.dataSystem(...)`** APIs directly. > > **`InitializerEntry`** and **`SynchronizerEntry`** gain **package-private no-arg constructors** so code outside the SDK cannot subclass those abstract bases; in-package concrete entry types (e.g. polling/streaming entries) are unchanged. > > Reviewed by [Cursor Bugbot](https://cursor.com/bugbot) for commit 24eb4c0ebfcabc6b33597240374995872c374484. Bugbot is set up for automated code reviews on this repo. Configure [here](https://www.cursor.com/dashboard/bugbot). --- .../sdk/android/InternalDataSystemAccess.java | 28 ------------------- .../launchdarkly/sdktest/SdkClientEntity.java | 7 ++--- .../launchdarkly/sdk/android/Components.java | 2 +- .../launchdarkly/sdk/android/LDConfig.java | 2 +- .../integrations/InitializerEntry.java | 3 ++ .../integrations/SynchronizerEntry.java | 3 ++ 6 files changed, 11 insertions(+), 34 deletions(-) delete mode 100644 contract-tests/src/main/java/com/launchdarkly/sdk/android/InternalDataSystemAccess.java diff --git a/contract-tests/src/main/java/com/launchdarkly/sdk/android/InternalDataSystemAccess.java b/contract-tests/src/main/java/com/launchdarkly/sdk/android/InternalDataSystemAccess.java deleted file mode 100644 index 792a0ea2..00000000 --- a/contract-tests/src/main/java/com/launchdarkly/sdk/android/InternalDataSystemAccess.java +++ /dev/null @@ -1,28 +0,0 @@ -package com.launchdarkly.sdk.android; - -import com.launchdarkly.sdk.android.integrations.DataSystemBuilder; - -/** - * For LaunchDarkly internal use only. Do not use this class or its methods unless - * you are maintaining LaunchDarkly-produced code for this SDK (for example the contract-tests - * harness). - *

- * Forwards to the package-private data system entry points on {@link LDConfig.Builder} and - * {@link Components}. Application developers outside LaunchDarkly must not depend on this type; - * it is not part of the supported public Android SDK API and will be removed in a future release. - */ -public final class InternalDataSystemAccess { - - private InternalDataSystemAccess() { - } - - public static DataSystemBuilder newBuilder() { - return Components.dataSystem(); - } - - public static LDConfig.Builder applyToConfig( - LDConfig.Builder builder, - DataSystemBuilder dataSystem) { - return builder.dataSystem(dataSystem); - } -} diff --git a/contract-tests/src/main/java/com/launchdarkly/sdktest/SdkClientEntity.java b/contract-tests/src/main/java/com/launchdarkly/sdktest/SdkClientEntity.java index 36280fb9..f9627704 100644 --- a/contract-tests/src/main/java/com/launchdarkly/sdktest/SdkClientEntity.java +++ b/contract-tests/src/main/java/com/launchdarkly/sdktest/SdkClientEntity.java @@ -12,7 +12,6 @@ import com.launchdarkly.sdk.android.ConnectionMode; import com.launchdarkly.sdk.android.DataSystemComponents; import com.launchdarkly.sdk.android.LaunchDarklyException; -import com.launchdarkly.sdk.android.InternalDataSystemAccess; import com.launchdarkly.sdk.android.LDClient; import com.launchdarkly.sdk.android.LDConfig; @@ -394,13 +393,13 @@ private LDConfig buildSdkConfig(SdkConfigParams params, LDLogAdapter logAdapter, private void configureDataSystem(LDConfig.Builder builder, SdkConfigDataSystemParams dataSystem) { if (Boolean.TRUE.equals(dataSystem.useDefaultDataSystem)) { - InternalDataSystemAccess.applyToConfig(builder, InternalDataSystemAccess.newBuilder()); + builder.dataSystem(Components.dataSystem()); return; } SdkConfigConnectionModeConfig connModeConfig = dataSystem.connectionModeConfig; - DataSystemBuilder dsBuilder = InternalDataSystemAccess.newBuilder(); + DataSystemBuilder dsBuilder = Components.dataSystem(); // at the time of writing this, we did not have contract tests that could test platform state changes, // disabling automatic mode simplifies the behavior being tested @@ -425,7 +424,7 @@ private void configureDataSystem(LDConfig.Builder builder, SdkConfigDataSystemPa dsBuilder.customizeConnectionMode(ConnectionMode.STREAMING, buildConnectionModeBuilder(topLevel)); } - InternalDataSystemAccess.applyToConfig(builder, dsBuilder); + builder.dataSystem(dsBuilder); } private static boolean hasTopLevelDataSystemPipelines(SdkConfigDataSystemParams dataSystem) { diff --git a/launchdarkly-android-client-sdk/src/main/java/com/launchdarkly/sdk/android/Components.java b/launchdarkly-android-client-sdk/src/main/java/com/launchdarkly/sdk/android/Components.java index f1c2aadf..20d72483 100644 --- a/launchdarkly-android-client-sdk/src/main/java/com/launchdarkly/sdk/android/Components.java +++ b/launchdarkly-android-client-sdk/src/main/java/com/launchdarkly/sdk/android/Components.java @@ -304,7 +304,7 @@ public static PluginsConfigurationBuilder plugins() { * @see DataSystemComponents * @see LDConfig.Builder#dataSystem(DataSystemBuilder) */ - static DataSystemBuilder dataSystem() { + public static DataSystemBuilder dataSystem() { return new DataSystemBuilder(); } diff --git a/launchdarkly-android-client-sdk/src/main/java/com/launchdarkly/sdk/android/LDConfig.java b/launchdarkly-android-client-sdk/src/main/java/com/launchdarkly/sdk/android/LDConfig.java index d5f1af13..5b66347f 100644 --- a/launchdarkly-android-client-sdk/src/main/java/com/launchdarkly/sdk/android/LDConfig.java +++ b/launchdarkly-android-client-sdk/src/main/java/com/launchdarkly/sdk/android/LDConfig.java @@ -458,7 +458,7 @@ public Builder dataSource(ComponentConfigurer dataSourceConfigurer) * @see Components#dataSystem() * @see DataSystemBuilder */ - Builder dataSystem(DataSystemBuilder dataSystemBuilder) { + public Builder dataSystem(DataSystemBuilder dataSystemBuilder) { this.dataSystemBuilder = dataSystemBuilder; this.dataSource = null; return this; diff --git a/launchdarkly-android-client-sdk/src/main/java/com/launchdarkly/sdk/android/integrations/InitializerEntry.java b/launchdarkly-android-client-sdk/src/main/java/com/launchdarkly/sdk/android/integrations/InitializerEntry.java index 1c38cc00..385ecc61 100644 --- a/launchdarkly-android-client-sdk/src/main/java/com/launchdarkly/sdk/android/integrations/InitializerEntry.java +++ b/launchdarkly-android-client-sdk/src/main/java/com/launchdarkly/sdk/android/integrations/InitializerEntry.java @@ -7,4 +7,7 @@ * This type is part of the early-access data system API and is not stable. */ public abstract class InitializerEntry { + + InitializerEntry() { + } } diff --git a/launchdarkly-android-client-sdk/src/main/java/com/launchdarkly/sdk/android/integrations/SynchronizerEntry.java b/launchdarkly-android-client-sdk/src/main/java/com/launchdarkly/sdk/android/integrations/SynchronizerEntry.java index aa279264..6c856bb3 100644 --- a/launchdarkly-android-client-sdk/src/main/java/com/launchdarkly/sdk/android/integrations/SynchronizerEntry.java +++ b/launchdarkly-android-client-sdk/src/main/java/com/launchdarkly/sdk/android/integrations/SynchronizerEntry.java @@ -7,4 +7,7 @@ * This type is part of the early-access data system API and is not stable. */ public abstract class SynchronizerEntry { + + SynchronizerEntry() { + } } From 1b2998d930963db4457ac5575ac60be4e8c61ba4 Mon Sep 17 00:00:00 2001 From: Todd Anderson Date: Fri, 5 Jun 2026 10:54:43 -0400 Subject: [PATCH 4/4] chore: remove FDv2EntryConverter tests for now-unreachable fallback PR #361 (SDK-2438) added package-private no-arg constructors to InitializerEntry and SynchronizerEntry to prevent external code from constructing unknown entry types. The two FDv2EntryConverterTest cases that exercised the converter's IllegalArgumentException fallback did so via anonymous subclasses of those bases, which is no longer permitted from outside the integrations package -- breaking the build for any caller that lives in com.launchdarkly.sdk.android. The IllegalArgumentException fallback in FDv2EntryConverter remains as defense-in-depth for SDK-internal callers that might add a new entry type without wiring it through the converter. The two unit tests are dropped because their construction pattern is now structurally impossible from this package. SDK-2438 --- .../sdk/android/FDv2EntryConverterTest.java | 24 ------------------- 1 file changed, 24 deletions(-) diff --git a/launchdarkly-android-client-sdk/src/test/java/com/launchdarkly/sdk/android/FDv2EntryConverterTest.java b/launchdarkly-android-client-sdk/src/test/java/com/launchdarkly/sdk/android/FDv2EntryConverterTest.java index bb1f6693..7065d6d7 100644 --- a/launchdarkly-android-client-sdk/src/test/java/com/launchdarkly/sdk/android/FDv2EntryConverterTest.java +++ b/launchdarkly-android-client-sdk/src/test/java/com/launchdarkly/sdk/android/FDv2EntryConverterTest.java @@ -65,18 +65,6 @@ public void cacheInitializer_buildWithPlainInputs_emptyChangeSet() throws Except assertEquals(ChangeSetType.None, result.getChangeSet().getType()); } - @Test - public void unsupportedInitializer_throwsIllegalArgumentException() { - InitializerEntry unsupported = new InitializerEntry() {}; - try { - FDv2EntryConverter.toInitializerBuilder(unsupported); - org.junit.Assert.fail("expected IllegalArgumentException"); - } catch (IllegalArgumentException e) { - assertTrue(e.getMessage().contains("Unsupported InitializerEntry")); - assertTrue(e.getMessage().contains(unsupported.getClass().getName())); - } - } - @Test public void pollingSynchronizer_converts() { DataSourceBuilder builder = FDv2EntryConverter.toSynchronizerBuilder( @@ -91,18 +79,6 @@ public void streamingSynchronizer_converts() { assertNotNull(builder); } - @Test - public void unsupportedSynchronizer_throwsIllegalArgumentException() { - SynchronizerEntry unsupported = new SynchronizerEntry() {}; - try { - FDv2EntryConverter.toSynchronizerBuilder(unsupported); - org.junit.Assert.fail("expected IllegalArgumentException"); - } catch (IllegalArgumentException e) { - assertTrue(e.getMessage().contains("Unsupported SynchronizerEntry")); - assertTrue(e.getMessage().contains(unsupported.getClass().getName())); - } - } - @Test public void toInitializerBuilders_preservesOrderAndKinds() { List entries = Arrays.asList(