Skip to content

[Android] DdFlagsImplementation captures stale SdkCore at TurboModule construction time — flags always return defaults on cold start #1301

@ArturTimokhin

Description

@ArturTimokhin

Description

On Android with New Architecture (TurboModules) enabled, DdFlagsImplementation captures sdkCore = Datadog.getInstance() as a constructor parameter default value. With TurboModules, the native module is instantiated when first accessed from JavaScript — which occurs during JS bundle evaluation (import time), before DatadogProvider has initialized the native Datadog SDK via DdSdkReactNative._initializeFromDatadogProvider().

This means sdkCore holds a reference to an uninitialized/no-op SDK core. When Flags.enable(config, sdkCore) and FlagsClient.Builder(name, sdkCore).build() later use this stale reference, the Flags feature is effectively registered on a no-op core. The FlagsClient degrades to no-op mode and returns only default values for all flag evaluations.

Steps to Reproduce

  1. Use React Native 0.81+ with New Architecture enabled (newArchEnabled=true)
  2. Use DatadogProvider component with onInitialization callback
  3. In the callback, call DdFlags.enable() followed by feature flag evaluation via OpenFeature provider
  4. Cold start the app on Android

Expected Behavior

DdFlags.enable() registers the Flags feature with the fully initialized Datadog SDK core. Subsequent FlagsClient.Builder.build() calls create functional clients that return actual flag values from the Datadog backend.

Actual Behavior

On Android cold start, logcat shows:

[no-op]: Failed to create FlagsClient named 'default': Flags feature must be enabled first. Call Flags.enable() before creating clients. Operating in no-op mode.

All feature flag evaluations return default values. The issue does not occur:

  • On iOS (where Datadog.getInstance() handles the singleton differently)
  • On Android after hot reload ("r" in Metro) — because DdFlags is a globalThis singleton that persists across JS reloads with already-populated flag caches

Root Cause

In DdFlagsImplementation.kt (line 28-30):

class DdFlagsImplementation(
    private val sdkCore: SdkCore = Datadog.getInstance(),  // captured at construction time
) {

With TurboModules, this constructor runs when the JS runtime first references the DdFlags native module (during import/require evaluation), which is before DatadogProvider triggers native SDK initialization. The captured sdkCore is a no-op instance.

Both enable() and getClient() use this stale sdkCore:

fun enable(...) {
    Flags.enable(flagsConfig, sdkCore)  // registers on no-op core
}

private fun getClient(name: String): FlagsClient = 
    clients.getOrPut(name) { FlagsClient.Builder(name, sdkCore).build() }  // builds against no-op core

Proposed Fix

Change sdkCore from a constructor-captured val to a computed property that resolves Datadog.getInstance() at access time:

class DdFlagsImplementation() {
    private val sdkCore: SdkCore get() = Datadog.getInstance()
    // ...
}

This ensures that when enable() and getClient() are called (which always happens after Datadog SDK initialization), they use the correct, initialized SDK core.

Configuration

  • React Native: 0.81.5
  • Expo SDK: 54
  • @datadog/mobile-react-native: 3.4.0 (uses dd-sdk-android-flags:3.8.0 in build.gradle, resolved to 3.10.0 at build time)
  • Android New Architecture: Enabled (newArchEnabled=true)
  • Android minSdk: 29
  • Kotlin: 1.8.10

Workaround

Apply the proposed fix:

--- a/node_modules/@datadog/mobile-react-native/android/src/main/kotlin/com/datadog/reactnative/DdFlagsImplementation.kt
+++ b/node_modules/@datadog/mobile-react-native/android/src/main/kotlin/com/datadog/reactnative/DdFlagsImplementation.kt
@@ -25,9 +25,8 @@
 /**
  * The entry point to use Datadog's Flags feature.
  */
-class DdFlagsImplementation(
-    private val sdkCore: SdkCore = Datadog.getInstance(),
-) {
+class DdFlagsImplementation() {
+    private val sdkCore: SdkCore get() = Datadog.getInstance()
     private val clients: MutableMap<String, FlagsClient> = mutableMapOf()

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions