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
- Use React Native 0.81+ with New Architecture enabled (
newArchEnabled=true)
- Use
DatadogProvider component with onInitialization callback
- In the callback, call
DdFlags.enable() followed by feature flag evaluation via OpenFeature provider
- 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()
Description
On Android with New Architecture (TurboModules) enabled,
DdFlagsImplementationcapturessdkCore = 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), beforeDatadogProviderhas initialized the native Datadog SDK viaDdSdkReactNative._initializeFromDatadogProvider().This means
sdkCoreholds a reference to an uninitialized/no-op SDK core. WhenFlags.enable(config, sdkCore)andFlagsClient.Builder(name, sdkCore).build()later use this stale reference, the Flags feature is effectively registered on a no-op core. TheFlagsClientdegrades to no-op mode and returns only default values for all flag evaluations.Steps to Reproduce
newArchEnabled=true)DatadogProvidercomponent withonInitializationcallbackDdFlags.enable()followed by feature flag evaluation via OpenFeature providerExpected Behavior
DdFlags.enable()registers the Flags feature with the fully initialized Datadog SDK core. SubsequentFlagsClient.Builder.build()calls create functional clients that return actual flag values from the Datadog backend.Actual Behavior
On Android cold start, logcat shows:
All feature flag evaluations return default values. The issue does not occur:
Datadog.getInstance()handles the singleton differently)DdFlagsis aglobalThissingleton that persists across JS reloads with already-populated flag cachesRoot Cause
In
DdFlagsImplementation.kt(line 28-30):With TurboModules, this constructor runs when the JS runtime first references the
DdFlagsnative module (during import/require evaluation), which is beforeDatadogProvidertriggers native SDK initialization. The capturedsdkCoreis a no-op instance.Both
enable()andgetClient()use this stalesdkCore:Proposed Fix
Change
sdkCorefrom a constructor-capturedvalto a computed property that resolvesDatadog.getInstance()at access time:This ensures that when
enable()andgetClient()are called (which always happens after Datadog SDK initialization), they use the correct, initialized SDK core.Configuration
@datadog/mobile-react-native: 3.4.0 (usesdd-sdk-android-flags:3.8.0in build.gradle, resolved to 3.10.0 at build time)newArchEnabled=true)Workaround
Apply the proposed fix: