Skip to content

Add native FF&E configuration flow for React Native - #1317

Draft
leoromanovsky wants to merge 18 commits into
developfrom
leo/native-ffe-rn-poc
Draft

Add native FF&E configuration flow for React Native#1317
leoromanovsky wants to merge 18 commits into
developfrom
leo/native-ffe-rn-poc

Conversation

@leoromanovsky

@leoromanovskyleoromanovsky commented Jun 30, 2026

Copy link
Copy Markdown
Contributor

Motivation

We need a React Native delivery path for offline initialization and dynamic configuration that can move quickly for a customer evaluation without first landing, reviewing, releasing, and consuming upstream iOS and Android SDK changes. This branch keeps the implementation native-first: React Native exposes the RFC-shaped rules/offline configuration API surface, while Kotlin and Swift own configuration parsing, evaluator state, rules fetch, persistence, and SDK side effects in RN-local code that can later be extracted into the iOS and Android SDKs.

Changes

  • Adds the native rules/offline configuration API surface in React Native: parse/serialize config wire, fetch rules config, save/load, explicit activation, dynamic context, typed evaluations, and debug state.
  • Implements RN-local Kotlin and Swift UFC evaluators that parse configuration once into native models and evaluate from native state, avoiding JSON deserialization on every flag evaluation.
  • Reuses existing native Flags SDK side-effect hooks for exposures, flag-evaluation EVP emission, and RUM feature-flag annotation instead of adding a JS side-effect pipeline.
  • Adds shared ffe-system-test-data parity tests plus a new-architecture Android smoke that runs the offline corpus from React Native JS through the native bridge in CI.

Decisions

  • Phase 1 is RN-local but native-first so React Native can move now, while the Kotlin/Swift pieces remain shaped for later extraction into the iOS and Android SDKs.
  • The existing DdFlags / FlagsClient / DatadogOpenFeatureProvider precompute client remains unchanged; this PR adds a separate rules/offline path.
  • Fetch and load are intentionally non-mutating. Active evaluation state changes only through explicit setConfiguration.
  • Shared fixtures are the cross-repo correctness contract for this branch and the future iOS/Android extraction.

React Native API Example

import{CoreConfiguration,DdSdkReactNative,TrackingConsent,}from'@datadog/mobile-react-native';constsdkConfig=newCoreConfiguration('<client-token>','staging',TrackingConsent.GRANTED,{rumConfiguration: {applicationId: '<rum-application-id>',},},);awaitDdSdkReactNative.initialize(sdkConfig);// Offline initialization from a bundled rules ConfigurationWire string.constofflineConfig=awaitDdSdkReactNative.configurationFromString(bundledRulesConfigurationWire,);awaitDdSdkReactNative.setConfiguration(offlineConfig);awaitDdSdkReactNative.setEvaluationContext({targetingKey: user.id,attributes: {plan: user.plan,},});constcheckoutEnabled=awaitDdSdkReactNative.resolveBooleanEvaluation('checkout.enabled',false,);// Later, refresh the rules config without changing active evaluation state.constpreviousWire=awaitDdSdkReactNative.configurationToString(offlineConfig);constfetchedConfig=awaitDdSdkReactNative.fetchRulesConfiguration({endpoint: 'https://app.datadoghq.com/api/v2/feature-flagging/config/rules-based',headers: {'dd-client-token': '<client-token>',},flagQueryParams: {dd_env: 'staging',},previousConfigurationWire: previousWire,});awaitDdSdkReactNative.saveConfiguration(fetchedConfig,{slot: 'default'});// Activate the refreshed config only when the app is ready to switch.awaitDdSdkReactNative.setConfiguration(fetchedConfig);// Evaluations after activation use the refreshed rules config and current context.constrefreshedCheckoutEnabled=awaitDdSdkReactNative.resolveBooleanEvaluation('checkout.enabled',false,);

Flow

flowchart TB
RN[React Native app] --> API[New native rules/offline config APIs]
API --> Kotlin[Kotlin RN-local FF&E core]
API --> Swift[Swift RN-local FF&E core]
Kotlin --> ParsedK[Parsed UFC model]
Swift --> ParsedS[Parsed UFC model]
Kotlin --> NativeSDK[Existing native Flags SDK hooks]
Swift --> NativeSDK
NativeSDK --> EVP[EVP exposures and flag evaluations]
NativeSDK --> RUM[RUM feature-flag annotations]
Kotlin -. later extract .-> AndroidSDK[Android SDK library]
Swift -. later extract .-> IosSDK[iOS SDK library]
Loading
sequenceDiagram
participant JS as React Native JS
participant Bridge as Native bridge
participant Core as Native FF&E core
participant HTTP as Rules config API
participant Disk as Native storage
JS->>Bridge: configurationFromString(bundled wire)
Bridge->>Core: parse UFC once
JS->>Bridge: setConfiguration(config)
JS->>Bridge: setEvaluationContext(context A)
JS->>Bridge: resolve*Evaluation(flag, default)
Bridge-->>JS: value, reason, metadata
JS->>Bridge: setEvaluationContext(context B)
JS->>Bridge: resolve*Evaluation(flag, default)
JS->>Bridge: fetchRulesConfiguration(options)
Bridge->>HTTP: GET rules-based config
HTTP-->>Bridge: ConfigurationWire
Bridge-->>JS: fetched config, active state unchanged
JS->>Bridge: saveConfiguration(fetched config)
Bridge->>Disk: persist wire
JS->>Bridge: setConfiguration(fetched config)
JS->>Bridge: resolve*Evaluation(flag, default)
Loading
flowchart LR
CI[test:native-android] --> Metro[Metro]
CI --> Emulator[Android emulator]
Metro --> App[New architecture example app]
Emulator --> App
App --> Runner[offline fixture runner]
Runner --> Bridge[RN bridge]
Bridge --> Core[Kotlin native FFE core]
Core --> Result[233 shared fixture cases pass]
Loading

Validation

  • yarn tsc -p example-new-architecture/tsconfig.json --noEmit
  • yarn run lint passes with 0 errors and the existing 10 warnings.
  • ANDROID_HOME=/opt/homebrew/share/android-commandlinetools ANDROID_SDK_ROOT=/opt/homebrew/share/android-commandlinetools JAVA_HOME=/Users/leo.romanovsky/.sdkman/candidates/java/17.0.13-tem ./gradlew --no-daemon clean build -PDdSdkReactNative_minSdkVersion=24 from packages/core/android.
  • xcodebuild -workspace example-new-architecture/ios/DdSdkReactNativeExample.xcworkspace -scheme DatadogSDKReactNative-Unit-Tests test -destination 'platform=iOS Simulator,id=DACA5ADA-1149-4987-9ED2-1F76A786B332' passes, 193 tests including the Swift shared-corpus evaluator test.
  • ANDROID_HOME=/opt/homebrew/share/android-commandlinetools ANDROID_SDK_ROOT=/opt/homebrew/share/android-commandlinetools JAVA_HOME=/Users/leo.romanovsky/.sdkman/candidates/java/17.0.13-tem ./example-new-architecture/scripts/native-ffe-offline-android-smoke.sh passes and renders Native FFE offline fixture pass: 233 cases across 30 files.
  • This same Android smoke is wired into test:native-android, so CI now exercises the offline corpus from React Native JS through the native Android bridge without credentials.

@datadog-official

datadog-officialBot commented Jun 30, 2026

Copy link
Copy Markdown

Tests

🎉 All green!

🧪 All tests passed
❄️ No new flaky tests detected

This comment will be updated automatically if new data arrives.
🔗 Commit SHA: f423aa9 | Docs | Datadog PR Page | Give us feedback!

Sign up for freeto 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.

1 participant

@leoromanovsky