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

@leoromanovsky leoromanovsky 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';

const sdkConfig = new CoreConfiguration(
  '<client-token>',
  'staging',
  TrackingConsent.GRANTED,
  {
    rumConfiguration: {
      applicationId: '<rum-application-id>',
    },
  },
);

await DdSdkReactNative.initialize(sdkConfig);

// Offline initialization from a bundled rules ConfigurationWire string.
const offlineConfig = await DdSdkReactNative.configurationFromString(
  bundledRulesConfigurationWire,
);
await DdSdkReactNative.setConfiguration(offlineConfig);

await DdSdkReactNative.setEvaluationContext({
  targetingKey: user.id,
  attributes: {
    plan: user.plan,
  },
});

const checkoutEnabled = await DdSdkReactNative.resolveBooleanEvaluation(
  'checkout.enabled',
  false,
);

// Later, refresh the rules config without changing active evaluation state.
const previousWire = await DdSdkReactNative.configurationToString(offlineConfig);
const fetchedConfig = await DdSdkReactNative.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,
});

await DdSdkReactNative.saveConfiguration(fetchedConfig, {slot: 'default'});

// Activate the refreshed config only when the app is ready to switch.
await DdSdkReactNative.setConfiguration(fetchedConfig);

// Evaluations after activation use the refreshed rules config and current context.
const refreshedCheckoutEnabled = await DdSdkReactNative.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-official Bot 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 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.

1 participant